我为什么选择Getx做状态管理?
使用Getx状态管理的正确姿势一
Getx的使用不与其它状态管理工具冲突,无需覆盖MaterialApp也没问题。
创建Service/Controller类
当然,不一定需要继承这两个类,任何类型都可以交给Getx来管理实例。
// GetxService和GetxController的区别 // GetxService在不使用的时候,实例不会销毁。 而GetxController在不使用的时候会自动释放 class LoginService extends GetxService { } class LoginController extends GetxController { } // 在需要使用的地方 LoginController controller = Get.find();初始化Getx实例管理器,并初始化
void setupGetx() { // database // network api // Service Get.lazyPut(() => LoginService()); // Get.put(LoginService()); // Controller Get.lazyPut(() => LoginController(), fenix: true); } // 在main中调用初始化函数 void mian() { setupGetx(); }Get.lazyPut和Get.put的区别:
Get.put是在初始化的时候直接创建实例,而Get.lazyPut是在第一次使用的时候创建实例。
fenix: true的作用?
xxxService在创建后不会销毁,而xxxController在不用的时候会销毁。如果不加上fenix: true,那么在销毁后,不会重新创建。
在需要的地方直接访问Controller或Service
// 在需要的地方通过Get.find()引入 class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override State<LoginPage> createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { LoginController controller = Get.find(); @override Widget build(BuildContext context) { // 或者 LoginController controller = Get.find(); return const Placeholder(); } } // 如果没有必须使用StatefulWidget的必要性,可以使用 GetView<XXX>. GetView只是简单封装了XXX controller = Get.find(),可以少写一行代码 class LoginPage extends GetView<LoginController> { LoginPage({super.key}); @override Widget build(BuildContext context) { controller.xxx(); return const Placeholder(); } }
使用Getx状态管理的正确姿势二
如何使用状态管理
使用Obx变量
// 1. 变量改为xxx.obs, 改为可观察的变量 class LoginController extends GetxController { var isLogin = false.obs; var nums = <int>[].obs var user = Rx<User?>(null); } // 2. 在需要的地方直接使用变量。使用Obx包住,在变量变化的时候会自动更新 class LoginPage extends GetView<LoginController> { const LoginPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Obx(() { if (controller.isLogin.isFalse) { return const Text("not login"); } return const Text("login done"); }), ), ); } }注意:
- 注意:确保Obx包裹的代码里至少使用一次obs变量,否则会告警;
错误一:Obx包住的Widget创建必须在Obx回调里,不能直接在外部传入Child Widget,会导致无法正常观察变量
class LoginPage extends GetView<LoginController> { const LoginPage({super.key}); @override Widget build(BuildContext context) { Widget child = controller.isLogin.isTrue ? const Text("login done") : const Text("not login"); return Scaffold( body: Center( child: Obx(() { return child; }), ), ); } }
如果不喜欢在代码中出现奇怪的Obx代码,也可以让整个Widget被观察,只要把StatelessWidget替换成ObxWidget,别的什么都不用做
class LoginPage extends ObxWidget { @override Widget build() { final LoginController controller = Get.find(); return Scaffold( appBar: AppBar( actions: [ IconButton( onPressed: () { controller.setLogin(!controller.isLogin.value); }, icon: const Icon(Icons.add), ), ], ), body: controller.isLogin.isTrue ? const Text("login done") : const Text("not login"), ); } }同上,至少保证build()里面至少使用一次Obs变量,否则会告警。
把整一个Controller当做一个可观察的整体
适合多个变量需要同时更新
class LoginController extends GetxController { bool isLogin = false; void setLogin(bool login) { isLogin = login; update(); // 记得调用update } } class LoginPage extends GetView<LoginController> { const LoginPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( actions: [ IconButton( onPressed: () { controller.setLogin(!controller.isLogin); }, icon: const Icon(Icons.add)), ], ), body: GetBuilder<LoginController>(builder: (loginController) { if (!loginController.isLogin) { return const Text("not login"); } return const Text("login done"); }), ); } }
我建议的编码规范
- setupGetx的初始化,建议按照实例类型把代码分类,有网络相关、数据库相关、服务相关、页面相关(Controller);
- 建议按照页面,每个页面对应最多一个Controller,把页面里业务逻辑部分尽可能写到Controller中。Controller的生命周期与页面一致;如果需要记录UI状态,建议也分成一类,例如XXXUI等;
- 个人喜欢,不太喜欢使用@singleton自动管理实例,因为会导致无法直观了解全局有多少单实例对象。
为什么不选择Provider?
不选择Provider的原因很明确,Provider有几个缺点
- 它与页面的关联太紧密,在非页面的地方,不方便获得状态实例;
- 它是通过Element Tree向上查找,在Provider子Element的地方无法获取Provider
- Provider的更新是整体更新,有性能上的问题。(如果喜欢整体更新,那Getx的GetBuilder也可以做到)
为什么放弃了River?
River官方文档里声明的解决了下面这些问题,但是这些问题使用getx可以有代替实现方法,且比riverpod更灵活。
异步请求需要在本地缓存,因为每当 UI 更新时就重新执行异步请求是不合理的
Getx中的无论使用XXXService或者XXXController都可以做到只加载一次。XXXService实现了RiverPod(keepAlive: true)的效果,XXXController实现了默认效果。
我们还需要处理错误和加载状态
// river: AsyncData. ->. getx: StateMixin。 class LoginController extends GetxController with StateMixin { bool isLogin = false; void setLogin(bool login) { isLogin = login; update(); } }实际开发过程中,会发现状态不仅仅是默认提供的这些,而如果遇到了超出限制的情况(例如需要多个变量组合),River的处理就显得无力。
不依赖于BuildContext
实际上,ConsumerWidget本身就是一个Widget,必须拿到WidgetRef才能够,并且,还不能在Widget的initState和dispose去愉快使用ref
// 限制1: 无法在initState里初始化. Getx无限制 @override void initState() { super.initState(); // ref.read(loginProvider.notifier).setLogin(true); // 出错 loginController.setLogin(true); // 没有问题 } // 限制2:无法在displose再次使用ref: Avoid using 'Ref' inside State.dispose. Getx无限制 @override void dispose() { ref.read(loginProvider.notifier).setLogin(false); super.dispose(); }
我不喜欢River的原因
- River一个状态只能监听一个变量或实例。 要么使用基础字段,要么得自己封装,自己封装增加了使用成本;
- River依赖于代码生成,代码生成的时间影响开发速度。
River能做的事情,Getx都可以做。而Getx更灵活,更强大。网上只看到说Getx不被推荐,但确实没有看到一条具有足够说服力的。