标签 状态管理 下的文章

使用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");
            }),
          ),
        );
      }
    }

    注意:

    1. 注意:确保Obx包裹的代码里至少使用一次obs变量,否则会告警;
    2. 错误一: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有几个缺点

  1. 它与页面的关联太紧密,在非页面的地方,不方便获得状态实例;
  2. 它是通过Element Tree向上查找,在Provider子Element的地方无法获取Provider
  3. Provider的更新是整体更新,有性能上的问题。(如果喜欢整体更新,那Getx的GetBuilder也可以做到)

为什么放弃了River?

River官方文档里声明的解决了下面这些问题,但是这些问题使用getx可以有代替实现方法,且比riverpod更灵活。

  1. 异步请求需要在本地缓存,因为每当 UI 更新时就重新执行异步请求是不合理的

    Getx中的无论使用XXXService或者XXXController都可以做到只加载一次。XXXService实现了RiverPod(keepAlive: true)的效果,XXXController实现了默认效果。

  2. 我们还需要处理错误和加载状态

    // river: AsyncData. ->. getx: StateMixin。
    
    class LoginController extends GetxController with StateMixin {
      bool isLogin = false;
    
      void setLogin(bool login) {
        isLogin = login;
        update();
      }
    }

    实际开发过程中,会发现状态不仅仅是默认提供的这些,而如果遇到了超出限制的情况(例如需要多个变量组合),River的处理就显得无力。

  3. 不依赖于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的原因

  1. River一个状态只能监听一个变量或实例。 要么使用基础字段,要么得自己封装,自己封装增加了使用成本;
  2. River依赖于代码生成,代码生成的时间影响开发速度。

River能做的事情,Getx都可以做。而Getx更灵活,更强大。网上只看到说Getx不被推荐,但确实没有看到一条具有足够说服力的。