Flutter笔记getX库中的GetView中间件
1. GetPage简介
GetView 是 GetX 库中的一个用于构建视图的组件。它与一个注册的 Controller 关联,并通过 getter 方法提供对该 Controller 的访问。用人话说就是,GetView 简化了 GetX 中对 控制器的访问。
2. 控制器模式思想的简要回顾
2.1 状态提升模式的缺陷
当我们做状态管理的时候,一个常用的手段就是 状态提升(State Lifting)。状态提升是一种在 React 和 Flutter 等前端和跨端框架中都十分常见的设计模式,用于管理组件之间共享状态的方法。这种模式通常用于处理以下情况:
- 共享数据: 当多个组件需要访问和共享相同的数据时,将状态提升到这些组件的共同祖先组件中,以便它们可以共享数据。
- 状态同步: 当某个状态需要被多个组件 修改 时,将这个状态提升到共同的父组件,由父组件负责管理和更新状态,然后将状态传递给子组件。
首先,如果一个状态需要被很多组件访问,那么这个状态就需要被提升到很高的层级,这会使得组件树变得复杂。其次,状态提升模式使得状态和UI紧密耦合,这可能会导致代码难以维护和测试。另外一个方面,状态提升在Flutter中将很容易导致扩大刷新范围,浪费性能。因此需要一种有效的解决方案来弥补状态提升的不足。这个解决方案就是所谓的 控制器模式。
2.2 控制器模式
在原生Flutter中,控制器模式的基本思想是通过创建一个独立的控制器类,将与业务逻辑相关的状态和方法都封装在这个控制器中。控制器类负责管理状态、处理逻辑,而界面组件则负责展示UI,并通过控制器来获取或更新状态。
- 独立控制器类: 创建一个独立的控制器类,用于管理相关的状态和逻辑。
- 状态封装: 将与业务逻辑相关的状态封装在控制器中,使得控制器成为状态的唯一管理者。
- 界面组件简化: 界面组件专注于展示UI,通过控制器获取状态和调用方法,避免在组件内部处理过多的业务逻辑。
- 解耦和复用: 通过使用控制器,实现组件的状态和行为的解耦,提高代码的复用性和可维护性。
3. 使用单例模式控制GetX实例数量
3.1 不使用 工厂构造方法 单例模式
基于 Dart 对于面向对象语法中相关功能的支持,为控制器或者服务实现单例可以按照下面的三个步骤进行:
- 供一个私有静态属性用于存储唯一的控制器实例;
- 创建用于内部静态构造的构造器,尽量避免提供外部可访问的构造方法;
- 提供一个外部访问的访问器接口,在该接口中:
- 如果还没有创建过控制器,则内部构建数以该类的唯一构造器实例后返回;
- 如果存储的构造器已经非空,则返回该之前创建过的属于该类的唯一构造器实例。
基于以上步骤,一个计数器控制器增加单例控制的面向对象实现如下:
import 'package:get/get.dart'; /// 计数器控制器类 class CounterController extends GetxController { // 提供一个私有静态属性用于存储唯一的控制器实例 static CounterController? _instance; // 仅提供一个私有构造器防止外部创建实例 CounterController._(); // 提供一个外部访问的访问器接口 static CounterController? get to { // 表示仅仅当 _instance 为 null 时,内部构造该控制器实例 _instance ??= CounterController._(); return _instance; } // 下面表示一些状态变量个状态相关的内容... int counter = 0; void increment() { counter++; update(); } }
3.2 使用 工厂构造方法 实现单例模式
在Dart中,我们可以使用工厂构造函数来实现单例模式。工厂构造函数与普通构造函数不同,它不一定每次都会创建一个新的实例。相反,工厂构造函数可以从缓存中返回一个已存在的实例,或者返回一个子类型的实例。
以下是如何使用工厂构造函数来实现CounterController的单例:
import 'package:get/get.dart'; class CounterController extends GetxController { // 提供一个私有静态属性用于存储唯一的控制器实例 static CounterController? _instance; // 使用工厂构造函数来实现单例 factory CounterController() { if (_instance == null) { _instance = CounterController._internal(); } return _instance!; } // 私有构造函数 CounterController._internal(); // 下面表示一些状态变量个状态相关的内容... int counter = 0; void increment() { counter++; update(); } }
在这个例子中,我们首先定义了一个私有的静态属性_instance来存储单例对象。然后,我们定义了一个工厂构造函数CounterController(),它会检查_instance是否为null,如果为null,就创建一个新的CounterController实例并赋值给_instance,否则就直接返回_instance。这样,我们就可以确保CounterController的单例性。
然后,我们定义了一个私有的构造函数CounterController._internal(),这个构造函数只能在类的内部被调用,这样可以防止外部代码创建CounterController的新实例。
最后,我们定义了一些状态变量和状态相关的方法,这些与单例模式无关,只是CounterController的业务逻辑。
4. GetX 依赖注入
4.1 依赖注入的基本概念
依赖注入(Dependency Injection,简称DI)是一种设计模式,它的主要目标是实现松耦合。在依赖注入中,一个对象不需要直接创建或管理它的依赖项,而是通过外部的方式(例如构造函数、属性或方法)来接收这些依赖项。这样,对象就可以专注于自己的业务逻辑,而不需要关心如何创建和管理依赖项。
依赖注入的主要优点是提高了代码的可测试性和可重用性。由于对象不再直接创建和管理依赖项,所以我们可以在测试时轻松地替换依赖项,例如使用模拟对象(Mock Object)来代替真实的依赖项。此外,由于依赖项是通过外部的方式来提供的,所以我们可以在不同的场景下使用不同的依赖项,这增加了代码的可重用性。
4.2 GetX中的依赖注入原理
GetX 的依赖注入原理主要基于 Dart 的映射(Map)数据结构和 GetX 的依赖管理容器。在 GetX 中,依赖管理容器是一个全局的映射表,键是依赖项的类型,值是依赖项的实例。
当你调用 Get.put()、Get.lazyPut()、Get.putAsync()、Get.create() 等方法时,GetX 会首先检查依赖管理容器中是否已经存在相同类型的依赖项。如果不存在,GetX 会创建一个新的依赖项,并将它的类型和实例添加到依赖管理容器中。如果已经存在,GetX 会根据具体的方法来决定是否替换已存在的依赖项。
当你调用 Get.find() 方法时,GetX 会根据指定的类型从依赖管理容器中查找对应的依赖项。如果找到了,就返回这个依赖项;如果没有找到,就抛出一个异常。
当你调用 Get.delete() 方法时,GetX 会根据指定的类型从依赖管理容器中删除对应的依赖项。如果这个依赖项是一个GetxController 或 GetxService,GetX还会调用它的 onClose()
方法。
通过这种方式,GetX 可以实现依赖项的自动管理,包括创建、查找、删除和生命周期管理。这大大简化了依赖管理的复杂性,使得开发者可以更专注于业务逻辑的实现。
4.3 基于GetX依赖注入使用单例
注入依赖
基于GetX依赖注入使用单例时,需要调用 Get.put()、Get.lazyPut()、Get.putAsync()、Get.create() 创建一个新的依赖项,然后将它注册到依赖管理容器中。每个依赖项都有一个对应的类型,GetX会根据这个类型来管理和查找依赖项。
例如:
void main() { Get.put(CounterController()); runApp(MyApp()); }
由于往往需要创建使用多个依赖,这种情况下,我个人习惯于在项目中创建一个 app_injections.dart
文件用于存放这些依赖项。例如:
class DependencyInjection { static void init() { Get.lazyPut(() => AuthService(AuthProvider())); Get.lazyPut(() => ChatService()); // 继续添加其它依赖项 } }
另外,使用 GetX 时,则顶层组件使用的是其包装的 GetMaterialApp 以替代 MaterialApp。你可以选择在 onInit 中完成一些初始化工作, 比如:
GetMaterialApp( //...省略其它内容 onInit: () async { await initialization(context); // 等待初始化完成 }, )
其中,为了代码简洁,可以单独写一个 initialization 方法:
Future<void> initialization(BuildContext context) async { // 可以在这里完成依赖项初始化 DependencyInjection.init(); }
获取依赖
你调用Get.find()方法时,GetX会根据指定的类型从依赖管理容器中查找对应的依赖项。如果找到了,就返回这个依赖项;如果没有找到,就抛出一个异常,因此请确保你的依赖被正确注入。
final counterController = Get.find<CounterController>();
删除依赖项
当你调用Get.delete()方法时,GetX会根据指定的类型从依赖管理容器中删除对应的依赖项。如果这个依赖项是一个GetxController或GetxService,GetX还会调用它的onClose()方法。如:
Get.delete<CounterController>();
生命周期管理
如果一个依赖项是一个 GetxController,GetX 会自动管理它的生命周期。当这个依赖项被创建时,GetX会调用它的 onInit() 和 onReady()方法;当这个依赖项被删除时,GetX 会调用它的 onClose() 方法。
相比之下GetxService 的实例在整个应用生命周期中都是可用的,它们不会被自动回收。这是因为GetxService通常用于提供全局的、需要长期存在的服务,例如用户认证、数据库操作、网络请求等。
作用域管理
GetX支持作用域管理,你可以使用Get.createScope()和Get.deleteScope()方法来创建和删除作用域。每个作用域都有一个唯一的标识符,你可以在创建和获取依赖项时指定作用域。
Get.createScope('myScope'); Get.put(CounterController(), tag: 'myScope'); final counterController = Get.find<CounterController>(tag: 'myScope'); Get.deleteScope('myScope');
4.4 GetX 控制器内置的一种简化的单例获取方法
在GetX中,除了使用Get.find()方法来获取控制器的单例实例外,还可以使用.to的方式来获取。这是GetX为我们提供的一种简化的获取单例的方式。
当你创建一个继承自GetxController的类时,你可以在类的内部定义一个静态的to属性,这个属性会自动获取这个类的单例实例。这样,你就可以直接通过.to的方式来获取这个类的单例实例,而不需要每次都调用Get.find()方法。
例如:
class CounterController extends GetxController { static CounterController get to => Get.find(); var count = 0.obs; void increment() { count.value++; } }
在这个例子中,我们定义了一个静态的to属性,它会自动调用Get.find()方法来获取CounterController的单例实例。然后,我们就可以在其他地方通过CounterController.to的方式来获取这个单例实例:
CounterController.to.increment();
显而易见的是,这种方式的好处是,它可以让代码看起来更简洁,而且更符合面向对象的设计原则。你心动了吗?赶紧试一试修改你大量的 find 吧。
5. GetView中间件
在你想着要修改 find 使用 4.4 小节 的方式时,有时候可能使用更简单的 GetView 就可以完成你的需求。
由于有时候在 Flutter 中,我们经常需要在不同的组件中访问和操作同一个控制器实例。为了避免在每个组件中都写一遍 Get.find(),GetX 库提供了一个 GetView 中间件。
GetView 是一个抽象类,它继承自 StatelessWidget,并提供了一个 controller
属性,这个属性会自动获取对应类型的控制器实例,因此相比于在 StatelessWidget 中自己获取单例, 使用起GetView 来会更加地方便。
GetView 的主要功能是简化控制器的获取。当你创建一个继承自 GetView 的组件时,你只需要指定控制器的类型,然后你就可以在组件中直接使用 controller
属性来访问控制器实例。
例如,还是哪个计数器地例子,我们再一次进行改进。我们创建一个 CounterController 类,它继承自 GetxController 并有一个 count
状态和一个 increment
方法:
class CounterController extends GetxController { var count = 0.obs; void increment() { count.value++; } }
然后,我们创建一个继承自GetView的组件:
class CounterPage extends GetView<CounterController> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Counter')), body: Center( child: Obx(() => Text('Count: ${controller.count.value}')), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: controller.increment, ), ); } }
在这个例子中,GetView 会自动获取一个 CounterController 的实例,并将它赋值给 controller
属性。然后,我们就可以在组件中直接使用 controller
属性来访问和操作 CounterController 的实例。这样,我们就不需要在每个组件中都写一遍 Get.find() 了。