GetX第三篇-依赖注入

8,458 阅读8分钟

为什么要使用依赖注入

依赖注入是什么

本来接受各种参数来构造一个对象,现在只接受一个参数——已经实例化的对象。

依赖注入的目的

依赖注入是为了将依赖组件的配置和使用分离开,以降低使用者与依赖之间的耦合度。

依赖注入的好处

实现依赖项注入可为您带来以下优势:

  • 重用代码 更容易换掉依赖项的实现。由于控制反转,代码重用得以改进,并且类不再控制其依赖项的创建方式,而是支持任何配置。
  • 易于重构 依赖项的创建分离,可以在创建对象时或编译时进行检查、修改,一处修改,使用处不需修改。
  • 易于测试 类不管理其依赖项,因此在测试时,您可以传入不同的实现以测试所有不同用例。

举个例子

老王的玛莎拉蒂需要换个v8引擎,他是自己拼装个引擎呢还是去改装店买一个呢? 如果自己拼装个,引擎的构造更新了,他需要学习改进自己的技术,买新零件,而直接买一个成品,就是依赖注入。

class Car(private val engineParts: String,val enginePiston: String) {

    fun start() {
        val engine= Engine(engineParts,enginePiston)
        engine.start()
    }
}

class Engine(private val engineParts: String,val enginePiston: String){
}

上面代码中的 Engine 类如果构造方法变动了,也需要去 Car 类里更改。而使用依赖注入就不需要改动 Car 类。

手动实现依赖注入通常有两种,构造函数传入和字段传入。 构造方法:

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

字段传入:

class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

上面虽然实现了依赖注入,但是增加了样板代码,如果注入实例多,也很麻烦。Android 上有 DaggerHilt 实现自动注入, GetX 也给我们提供了 Binding 类实现。

使用依赖注入

Get有一个简单而强大的依赖管理器,它允许你只用1行代码就能检索到 Controller 或者需要依赖的类,不需要提供上下文,不需要在 inheritedWidget 的子节点。

注入依赖:

Get.put<PutController>(PutController());

获取依赖:

Get.find<PutController>();

就是这么简单。

Get.put()

这是个立即注入内存的注入方法。调用后已经注入到内存中。

Get.put<S>(
  // 必备:要注入的类。
  // 注:" S "意味着它可以是任何类型的类。
  S dependency

  // 可选:想要注入多个相同类型的类时,可以用这个方法。
  // 比如有两个购物车实例,就需要使用标签区分不同的实例。
  // 必须是唯一的字符串。
  String tag,

  // 可选:默认情况下,get会在实例不再使用后进行销毁
  // (例如:一个已经销毁的视图的Controller)
  // 如果需要这个实例在整个应用生命周期中都存在,就像一个sharedPreferences的实例。
  // 默认值为false
  bool permanent = false,

  // 可选:允许你在测试中使用一个抽象类后,用另一个抽象类代替它,然后再进行测试。
  // 默认为false
  bool overrideAbstract = false,

  // 可选:允许你使用函数而不是依赖(dependency)本身来创建依赖。
  // 这个不常用
  InstanceBuilderCallback<S> builder,
)

permanent是代表是否不销毁。通常Get.put()的实例的生命周期和 put 所在的 Widget 生命周期绑定,如果在全局 (main 方法里)put,那么这个实例就一直存在。如果在一个 Widget 里 put ,那么这个那么这个 Widget 从内存中删除,这个实例也会被销毁。注意,这里是删除,并不是dispose,具体看上一篇最后的部分。

Get.lazyPut

懒加载一个依赖,只有在使用时才会被实例化。适用于不确定是否会被使用的依赖或者计算高昂的依赖。类似 Kotlin 的 Lazy 代理。

  Get.lazyPut<LazyController>(() => LazyController());

LazyController 在这时候并不会被创建,而是等到你使用的时候才会被 initialized,也就是执行下面这句话的时候才 initialized

Get.find<LazyController>();

在使用后,使用时的 Wdiget 的生命周期结束,也就是这个 Widgetdispose,这个实例就会被销毁。

如果在一个 Widget 里 find,然后退出这个 widget,此时这个实例也被销毁,再进入另一个路由的 Widget,再次 find,GetX会打印错误信息,提醒没有 put 。及时全局注入,也一样。可以理解为, Get.lazyPut 注入的实例的生命周期是和在Get.find时的上下文所绑定。

如果想每次 find 获取到不同的实例,可以借助fenix参数。

Get.lazyPut<S>(
  // 必须:当你的类第一次被调用时,将被执行的方法。
  InstanceBuilderCallback builder,
  
  // 可选:和Get.put()一样,当你想让同一个类有多个不同的实例时,就会用到它。
  // 必须是唯一的
  String tag,

  // 可选:下次使用时是否重建,
  // 当不使用时,实例会被丢弃,但当再次需要使用时,Get会重新创建实例,
  // 就像 bindings api 中的 "SmartManagement.keepFactory "一样。
  // 默认值为false
  bool fenix = false
  
)

Get.putAsync

注入一个异步创建的实例。比如SharedPreferences

  Get.putAsync<SharedPreferences>(() async {
    final sp = await SharedPreferences.getInstance();
    return sp;
  });

作用域参考Get.put

Get.create

这个方法可以创建很多实例。很少用到。可以当做Get.put

Bindings类

上面实现了依赖注入和使用,但是和前面讲的手动注入一样,为了生命周期和使用的 Widget 绑定,需要在 Widget 里注入和使用,并没有完全解耦。要实现自动注入,我们就需要这个类。

这个包最大的区别之一,也许就是可以将路由、状态管理器和依赖管理器完全集成。 当一个路由从Stack中移除时,所有与它相关的控制器、变量和对象的实例都会从内存中移除。如果你使用的是流或定时器,它们会自动关闭,我们不必担心这些。Bindings 类是一个解耦依赖注入的类,同时 "Binding " 路由到状态管理器和依赖管理器。 这使得 GetX 可以知道当使用某个控制器时,哪个页面正在显示,并知道在哪里以及如何销毁它。 此外,Bindings 类将允许我们利用 SmartManager 配置控制。

  • 创建一个类并实现Binding
class InjectSimpleBinding implements Bindings {}

因为Bindings是抽象方法,所以要ide会提示要实现dependencies。在里面注入我们需要的实例:

class InjectSimpleBinding implements Bindings {
  @override
  void dependencies() {
    Get.lazyPut<Api>(() => Api());
    Get.lazyPut<InjectSimpleController>(() => InjectSimpleController());
  }
}
  • 通知路由,我们要使用该 Binding 来建立路由管理器、依赖关系和状态之间的连接。

这里有两种方式,如果使用的是命名路由表:

    GetPage(
      name: Routes.INJECT,
      page: () => InjectSimplePage(),
      binding:InjectSimpleBinding(),
    ),

如果是直接跳转:

Get.to(InjectSimplePage(), binding: InjectSimpleBinding());

现在,我们不必再担心应用程序的内存管理,Get将为我们做这件事。

上面我们注入依赖解耦了,但是获取还是略显不方便,GetX 也为我们考虑到了。GetView完美的搭配 Bindings。

class InjectSimplePage extends GetView<InjectSimpleController> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('MyPage')),
      body: Center(
        child: Obx(() => Text(controller.obj.toString())),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          controller.getAge();
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

这里完全没有Get.find,但是可以直接使用controller,因为GetView里封装好了:

abstract class GetView<T> extends StatelessWidget {
  const GetView({Key key}) : super(key: key);

  final String tag = null;

  T get controller => GetInstance().find<T>(tag: tag);

  @override
  Widget build(BuildContext context);
}

不在需要 StatelessWidget 和 StatefulWidget。这也是开发最常用的模式,推荐大家使用。

当然,也许有时候觉得每次声明一个 Bingings 类也很麻烦,那么可以使用 BindingsBuilder ,这样就可以简单地使用一个函数来实例化任何想要注入的东西。

  GetPage(
    name: '/details',
    page: () => DetailsView(),
    binding: BindingsBuilder(() => {
      Get.lazyPut<DetailsController>(() => DetailsController());
    }),

就是这么简单,Bingings 都不需要创建。两种方式都可以,大家根据自己的编码习惯选择最适合的风格。

Bindings的工作原理

Bindings 会创建过渡性工厂,在点击进入另一个页面的那一刻,这些工厂就会被创建,一旦路由过渡动画发生,就会被销毁。 工厂占用的内存很少,它们并不持有实例,而是一个具有我们想要的那个类的 "形状"的函数。 这在内存上的成本很低,但由于这个库的目的是用最少的资源获得最大的性能,所以Get连工厂都默认删除。

智能管理

GetX 默认情况下会将未使用的控制器从内存中移除。 但是如果想改变GetX控制类的销毁方式怎么办呢,可以用SmartManagement 类设置不同的行为。

如何改变

如果想改变这个配置(通常不需要),就用这个方法。

void main () {
  runApp(
    GetMaterialApp(
      smartManagement: SmartManagement.onlyBuilders //这里
      home: Home(),
    )
  )
}
  • SmartManagement.full 这是默认的。销毁那些没有被使用的、没有被设置为永久的类。在大多数情况下,我们都使用这个,不需要更改。

  • SmartManagement.onlyBuilders 使用该选项,只有在init:中启动的控制器或用Get.lazyPut()加载到Binding中的控制器才会被销毁。

    如果使用Get.put()或Get.putAsync()或任何其他方法,SmartManagement 没有权限也就是不能移除这个依赖。

  • SmartManagement.keepFactory 就像SmartManagement.full一样,当它不再被使用时,它将删除它的依赖关系,但它将保留它们的工厂,这意味着如果再次需要该实例,它将重新创建该依赖关系。