GetX第三篇-依赖注入
为什么要使用依赖注入
依赖注入是什么
本来接受各种参数来构造一个对象,现在只接受一个参数——已经实例化的对象。
依赖注入的目的
依赖注入是为了将依赖组件的配置和使用分离开,以降低使用者与依赖之间的耦合度。
依赖注入的好处
实现依赖项注入可为您带来以下优势:
- 重用代码
更容易换掉依赖项的实现。由于控制反转,代码重用得以改进,并且类不再控制其依赖项的创建方式,而是支持任何配置。 - 易于重构
依赖项的创建分离,可以在创建对象时或编译时进行检查、修改,一处修改,使用处不需修改。 - 易于测试
类不管理其依赖项,因此在测试时,您可以传入不同的实现以测试所有不同用例。
举个例子
老王的玛莎拉蒂需要换个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 上有 Dagger 和Hilt 实现自动注入, GetX 也给我们提供了 Binding 类实现。
使用依赖注入
Get有一个简单而强大的依赖管理器,它允许你只用1行代码就能检索到 Controller 或者需要依赖的类,不需要提供上下文,不需要在 inheritedWidget 的子节点。
注入依赖:
Get.put(PutController());
获取依赖:
Get.find();
就是这么简单。
Get.put()
这是个立即注入内存的注入方法。调用后已经注入到内存中。
Get.put(
// 必备:要注入的类。
// 注:" S "意味着它可以是任何类型的类。
S dependency
// 可选:想要注入多个相同类型的类时,可以用这个方法。
// 比如有两个购物车实例,就需要使用标签区分不同的实例。
// 必须是唯一的字符串。
String tag,
// 可选:默认情况下,get会在实例不再使用后进行销毁
// (例如:一个已经销毁的视图的Controller)
// 如果需要这个实例在整个应用生命周期中都存在,就像一个sharedPreferences的实例。
// 默认值为false
bool permanent = false,
// 可选:允许你在测试中使用一个抽象类后,用另一个抽象类代替它,然后再进行测试。
// 默认为false
bool overrideAbstract = false,
// 可选:允许你使用函数而不是依赖(dependency)本身来创建依赖。
// 这个不常用
InstanceBuilderCallback builder,
)
permanent
是代表是否不销毁。通常Get.put()
的实例的生命周期和 put 所在的 Widget 生命周期绑定,如果在全局 (main 方法里)put,那么这个实例就一直存在。如果在一个 Widget 里 put ,那么这个那么这个 Widget 从内存中删除,这个实例也会被销毁。注意,这里是删除,并不是dispose
,具体看上一篇最后的部分。
Get.lazyPut
懒加载一个依赖,只有在使用时才会被实例化。适用于不确定是否会被使用的依赖或者计算高昂的依赖。类似 Kotlin 的 Lazy 代理。
Get.lazyPut(() => LazyController());
LazyController 在这时候并不会被创建,而是等到你使用的时候才会被 initialized
,也就是执行下面这句话的时候才 initialized
:
Get.find();
在使用后,使用时的 Wdiget 的生命周期结束,也就是这个 Widgetdispose
,这个实例就会被销毁。
如果在一个 Widget 里 find,然后退出这个 widget,此时这个实例也被销毁,再进入另一个路由的 Widget,再次 find,GetX会打印错误信息,提醒没有 put 。及时全局注入,也一样。可以理解为,Get.lazyPut
注入的实例的生命周期是和在Get.find
时的上下文所绑定。
如果想每次 find 获取到不同的实例,可以借助fenix
参数。
Get.lazyPut(
// 必须:当你的类第一次被调用时,将被执行的方法。
InstanceBuilderCallback builder,
// 可选:和Get.put()一样,当你想让同一个类有多个不同的实例时,就会用到它。
// 必须是唯一的
String tag,
// 可选:下次使用时是否重建,
// 当不使用时,实例会被丢弃,但当再次需要使用时,Get会重新创建实例,
// 就像 bindings api 中的 "SmartManagement.keepFactory "一样。
// 默认值为false
bool fenix = false
)
Get.putAsync
注入一个异步创建的实例。比如SharedPreferences
。
Get.putAsync(() 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());
Get.lazyPut(() => InjectSimpleController());
}
}
- 通知路由,我们要使用该 Binding 来建立路由管理器、依赖关系和状态之间的连接。
这里有两种方式,如果使用的是命名路由表:
GetPage(
name: Routes.INJECT,
page: () => InjectSimplePage(),
binding:InjectSimpleBinding(),
),
如果是直接跳转:
Get.to(InjectSimplePage(), binding: InjectSimpleBinding());
现在,我们不必再担心应用程序的内存管理,Get将为我们做这件事。
上面我们注入依赖解耦了,但是获取还是略显不方便,GetX 也为我们考虑到了。GetView
完美的搭配 Bindings。
class InjectSimplePage extends GetView {
@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 extends StatelessWidget {
const GetView({Key key}) : super(key: key);
final String tag = null;
T get controller => GetInstance().find(tag: tag);
@override
Widget build(BuildContext context);
}
不在需要 StatelessWidget 和 StatefulWidget。这也是开发最常用的模式,推荐大家使用。
当然,也许有时候觉得每次声明一个 Bingings 类也很麻烦,那么可以使用 BindingsBuilder ,这样就可以简单地使用一个函数来实例化任何想要注入的东西。
GetPage(
name: '/details',
page: () => DetailsView(),
binding: BindingsBuilder(() => {
Get.lazyPut(() => 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一样,当它不再被使用时,它将删除它的依赖关系,但它将保留它们的工厂,这意味着如果再次需要该实例,它将重新创建该依赖关系。