Flutter状态管理GetX使用详解
如题,GetX包含很多功能,各种弹出widget、路由管理、国际化、Utils、状态管理等。本文只针对核心功能——状态管理的基础使用部分(暂不解析原理)。从浅入深、全面地做出详细介绍,笔者也是因为之前使用GetX,看过一些文档和blog。这篇文章笔者是从小白的视角来完成的,认真看完肯定使用完全没有问题。虽然笔者已经尽量直白、尽量简单,但是因为文章基本上包含了GetX状态管理的大部分内容,再加上大量的示例代码(多数都是计数器的重复性代码),可能篇幅有点长。
一、声明响应式变量及简单使用
有3种声明方式:
1、使用 Rx{Type}
// 建议使用初始值,但不是强制性的 final name = RxString(''); final isLogged = RxBool(false); final count = RxInt(0); final balance = RxDouble(0.0); final items = RxList<String>([]); final myMap = RxMap<String, int>({});复制代码
2、使用 Rx,规定泛型 Rx
final name = Rx<String>(''); final isLogged = Rx<Bool>(false); final count = Rx<Int>(0); final balance = Rx<Double>(0.0); final number = Rx<Num>(0) final items = Rx<List<String>>([]); final myMap = Rx<Map<String, int>>({}); // 自定义类 - 可以是任何类 final user = Rx<User>();复制代码
3、这种更实用、更简单、更可取的方法,只需添加 .obs 作为value的属性。(推荐使用)
final name = ''.obs; final isLogged = false.obs; final count = 0.obs; final balance = 0.0.obs; final number = 0.obs; final items = <String>[].obs; final myMap = <String, int>{}.obs; // 自定义类 - 可以是任何类 final user = User().obs;复制代码
demo使用,以计数器为例:
入口设置:推荐使用 GetMaterialApp(后续示例皆如此)
class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { /// 这里使用 GetMaterialApp return GetMaterialApp( home: Demo1(), ); } }复制代码
计数器
class Demo1 extends StatelessWidget { Demo1({Key? key}) : super(key: key); /// 3种方式声明变量 // RxInt count = RxInt(0); // var count = Rx<int>(0); var count = 0.obs; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("GetX"),), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 用Obx包装需要使用变量的widget Obx(() => Text( "count的值为:$count", style: const TextStyle(color: Colors.redAccent,fontSize: 20), )), const SizedBox(height: 30,), ElevatedButton( // 按钮点击count值++ onPressed: () => count ++, child: const Text("点击count++"), ) ], ), ), ); } }复制代码
注意: 此时count的类型是RxInt,不是int。可以通过count.value来获取其本身的int值
当我们查看源码的时候,可以发现调用.obs
的时候,内部还是通过RxInt<e>(this)
进行了一层包装,所以.obs
就是为了方便开发者使用的,推荐使用这种方式声明变量。
自定义类的使用
新建一个People
类
class People{ // 第一种:直接声明变量 // var name = "xiaoMing".obs; // var age = 18.obs; // 第二种:构造函数 var name; var age; People({this.name, this.age}); }复制代码
第一种使用:
// 声明 var people = People(); // 使用 Obx(() => Text( "名字:${people.name.value},年龄:${people.age.value}", style: const TextStyle(color: Colors.redAccent,fontSize: 20), )), // 改变状态 onPressed: (){ people.name.value = "xiaoLi"; people.age.value = 15; },复制代码
第二种使用:
// 声明 final people = People(name: "xiaoMing",age: 18).obs; // 使用 Obx(() => Text( "名字:${people.value.name},年龄:${people.value.age}", style: const TextStyle(color: Colors.redAccent,fontSize: 20), )), // 改变状态 onPressed: (){ people.value.name = "xiaoLi"; people.value.age = 15; },复制代码
二、GetxController
在实际项目开发中,我们一般不会像上述那样把UI代码、业务逻辑都放在一起处理,这样对项目的架构、代码的可读性、后期的优化和维护将会是致命的。GetX
也为我们提供了解决方案:GetxController
。
GetxController
提供了三种使用方式:
Obx
:响应式状态管理,当数据源变化时,将自动执行刷新组件的方法GetX
:响应式状态管理,当数据源变化时,将自动执行刷新组件的方法GetBuilder
:简单状态管理,当数据源变化时,需要手动执行刷新组件的方法,此状态管理器内部实际上是对StatefulWidget的封装,占用资源极少!
使用场景:
一般来说,对于大多数场景都是可以使用响应式变量。但是每个响应式变量
(.obs)
,都需要生成对应的GetStream,如果对象足够多,将生成大量的GetStream,必将对内存造成较大的压力,该情况下,就要考虑使用简单状态管理了。
响应式状态管理
逻辑层
继承于GetxController
的类,处理页面逻辑的,
class CounterController extends GetxController{ /// 定义了该变量为响应式变量,当该变量数值变化时,页面的刷新方法将自动刷新 var count = 0.obs; /// 自增方法 void increase() => ++count; }复制代码
view层
class Demo2 extends StatelessWidget { const Demo2({Key? key}) : super(key: key); @override Widget build(BuildContext context) { /// 通过依赖注入方式实例化的控制器 final counter = Get.put(CounterController()); return Scaffold( appBar: AppBar(title: const Text("GetX"),), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Obx(() => Text( "count的值为:${counter.count}", style: const TextStyle(color: Colors.redAccent,fontSize: 20), )), /* GetX<CounterController>( init: counter, builder: (controller){ return Text( "count的值为:${controller.count}", style: const TextStyle(color: Colors.redAccent,fontSize: 20), ); }, ), */ const SizedBox(height: 30,), ElevatedButton( // 按钮点击count值++ onPressed: () => counter.increase(), child: const Text("点击count++"), ), ], ), ), ); } }复制代码
注意:
响应式状态管理器只有当响应式变量的值发生变化时,才会会执行刷新操作,如当变量从“a”再变为“a”,是不会执行刷新操作。
final counter = Get.put(CounterController());
通过依赖注入方式实例化的控制器,不是Controller controller = Controller()
,也不是在正在使用的类中实例化的类,所有它可以在整个App中使用,前提是没有销毁。换句话说使用Get.put()
实例化类,使用对当下所有子路由可用。后续也可以通过Get.find()
找到对应的GetxController
。
Get.lazyPut()
:懒加载一个依赖,只有在使用时才会被实例化。其他功能同Get.put()
。
Get.putAsync()
:注册一个异步的依赖,例如:`
Get.putAsync(() async { final prefs = await SharedPreferences.getInstance(); await prefs.setInt('counter', 12345); return prefs; });`
不管是
Get.put()
还是Get.lazyPut()
,方法内部还有几个参数,因为不常用,这里不做说明。有兴趣者可以自行查阅文档了解。大家可以看到,笔者的CounterController实例化是写在
build
中的,因为 stl是无状态组件,不会被二次或多次重载,所以实例操作只会被执行一次。而且在使用PageView时,如果注入操作写在类中,那所有PageView页面控制器会全被初始化,而不是切换到某个页面时,对应页面的控制器才被初始化!所以在使用PageView时,注入操作须写在build
方法中。
简单状态管理
逻辑层
class CounterController extends GetxController{ var count = 0; void increase(){ ++count; update(); // 手动刷新 } }复制代码
view层
class Demo3 extends StatelessWidget { const Demo3({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final counter = Get.put(CounterController()); return Scaffold( appBar: AppBar(title: const Text("GetX"),), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ GetBuilder<CounterController>( // init: counter, builder: (controller){ return Text( "count的值为:${counter.count}", style: const TextStyle(color: Colors.redAccent,fontSize: 20), ); }, initState: (controller){}, dispose: (controller){}, ), const SizedBox(height: 30,), ElevatedButton( // 按钮点击count值++ onPressed: () => counter.increase(), child: const Text("点击count++"), ), ], ), ), ); } }复制代码
注意:
参数init:因为在加载变量的时候就使用
Get.put()
生成了CounterController
对象,GetBuilder会自动查找该对象,所以,就可以不 使用init参数。
GetBuilder
拥有StatefulWidget
所有周期回调,如:initState
、dispose
可以在相应回调内做一些操作。但是比这更好的方法是直接从控制器中使用onInit()
和onClose()
方法。
指定id刷新
多个GetBuilder使用同一个CounterController
的变量,但是我们只想更新其中一个GetBuilder的变量,就可以在添加id
参数
逻辑层
class CounterController extends GetxController{ var count = 0; void increase(){ ++count; update(["id"]); // 只对id为“id”的GetBuilder刷新 } }复制代码
view层
GetBuilder<CounterController1>( // 不刷新 builder: (controller){ return Text( "count的值为:${counter.count}", style: const TextStyle(color: Colors.redAccent,fontSize: 20), ); }, GetBuilder<CounterController1>( id: "id", // 添加id绑定,update时只当前GetBuilder会刷新 builder: (controller){ return Text( "count的值为:${counter.count}", style: const TextStyle(color: Colors.redAccent,fontSize: 20), ); },复制代码
跨页面交互
场景 :
OnePage
push到TwoPage
,在TwoPage
操作计数器后,返回,在OnePage
显示TwoPage
点击的次数。
页面一:
逻辑层:
class OneController extends GetxController{ var count = 0; /// 自增方法 void increase(){ count++; update(); } /// 跳转到Two页面 void toTwoPage(){ Get.to(()=> const PageTwo(),arguments: {"msg":"今天天气很好"}); } }复制代码
view层:
class PageOne extends StatelessWidget { const PageOne({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final oneController = Get.put(OneController()); return Scaffold( appBar: AppBar(title: const Text("One"),), body: Center( child: GetBuilder<OneController>( builder: (oneController){ return Text("Two页面点击了${oneController.count}"); }, ), ), floatingActionButton: FloatingActionButton( onPressed: (){ oneController.toTwoPage(); }, child: const Icon(Icons.arrow_forward_outlined) ), ); } }复制代码
页面二:
逻辑层:
class TwoController extends GetxController{ var count = 0; var msg = ""; /// 接收上个页面数据,并刷新操作 @override void onReady() { var map = Get.arguments; msg = map["msg"]; update(); super.onReady(); } /// 自增方法 void increase(){ count++; update(); } }复制代码
注意:
GetxController
包含比较完整的生命周期回调,可以在onInit()
接受传递的数据;如果接收的数据需要刷新到界面上,请在onReady()
回调里面接收数据操作,onReady()
是在addPostFrameCallback
回调中调用,刷新数据的操作在onReady()
进行,能保证界面是初始加载完毕后才进行页面刷新操作。
view层:
class PageTwo extends StatelessWidget { const PageTwo({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final oneController = Get.find<OneController>(); final twoController = Get.put(TwoController()); return Scaffold( appBar: AppBar(title: const Text("Two"),), body: Center( child:Column( mainAxisAlignment: MainAxisAlignment.center, children: [ GetBuilder<TwoController>( builder: (twoController){ return Text("这个是One传递的数据:${twoController.msg}"); }, ), GetBuilder<TwoController>( builder: (twoController){ return Text("count点击次数:${twoController.count}"); }, ) ], ), ), floatingActionButton: FloatingActionButton( onPressed: (){ oneController.increase(); twoController.increase(); }, child: const Icon(Icons.add) ), ); } }复制代码
通过Get.find()
,获取到了之前实例化OneController
,再操作相应的事件。
架构优化
上述都是将所有的状态变量和操作放在同一个地方,但是在复杂的业务场景下,这就显得很冗余,不利于后期维护和优化。于是我们会划分三个结构:state(状态层),logic(逻辑层),view(界面层)。
计数器改造:
state层
统一管理所有的状态变量
class CounterState{ late int count; CounterState(){ count = 0; } }复制代码
logic层
实例化状态类,以便操作所有的变量
class CounterController extends GetxController{ /// 实例化状态类,以便操作所有的变量 final CounterState state = CounterState(); /// 自增方法 void increase(){ state.count++; update(); } }复制代码
view层
class Demo3 extends StatelessWidget { const Demo3({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final controller = Get.put(CounterController()); /// 上行代码已经将CounterController实例化,可以通过Get.find获取 final state = Get.find<CounterController>().state; return Scaffold( appBar: AppBar(title: const Text("架构重构"),), body: Center( child: GetBuilder<CounterController>( builder: (controller){ return Text("counter点击了${state.count}次"); }, ), ), floatingActionButton: FloatingActionButton( onPressed: (){ controller.increase(); }, child: const Icon(Icons.arrow_forward_outlined) ), ); } }复制代码
上述重构了之后,代码结构就很清晰明了。
state只专注数据,需要使用数据,直接通过state获取
logic只专注于触发事件交互,操作或更新数据
view只专注UI显示
GetxController 事件监听:Workers
Workers将协助你在事件发生时触发特定的回调。
class Controller extends GetxController{ var name = "".obs; var age = 18.obs; @override void onInit() { /// 每次`name`变化时调用。 ever(name, (callback)=> null); /// 每次监听多个值变化时调用。 everAll([name,age], (callback) => null); /// 只有在变量第一次被改变时才会被调用 once(name, (callback) => null); /// 场景:变量频繁改变,如果用户多次输入、多次点击等。 防DDos - 当变量停止变化1秒后调用, /// 例如:搜索功能。用户输入完整单词后再执行搜索操作,而不是用户每输入一个字符就要进行一次操作 debounce(name, (callback) => null,time: const Duration(seconds: 1)); /// 忽略指定时间内变量的所有变化 interval(name, (callback) => null,time: const Duration(seconds: 1)); super.onInit(); } }复制代码
注意:
Worker应该总是在启动Controller或Class时使用,所以应该总是在onInit(推荐)、Class构造函数或StatefulWidget的initState(大多数情况下不推荐这种做法,但应该不会有任何副作用)。
GetxController 生命周期
class CounterController extends GetxController{ /// 初始化 @override void onInit() { // TODO: implement onInit super.onInit(); } /// 加载完成 @override void onReady() { // TODO: implement onReady super.onReady(); } /// 控制器被释放 @override void onClose() { // TODO: implement onClose super.onClose(); } }复制代码
三、Binding的使用
在使用GetX的时候,往往每次都是用需要手动实例化一个控制器final controller = Get.put(CounterController());
,如果每个界面都要实例化一次,有些许麻烦。使用Binding 能解决上述问题,可以在项目初始化时把所有需要进行状态管理的控制器进行统一初始化,直接使用Get.find()
找到对应的GetxController
使用。
可以将路由、状态管理器和依赖管理器完全集成
这里介绍三种使用方式,推荐第一种使用getx的命名路由的方式
不使用binding,不会对功能有任何的影响。
第一种:使用命名路由进行Binding绑定
/// 入口类 class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { /// 这里使用 GetMaterialApp /// 初始化路由 return GetMaterialApp( initialRoute: RouteConfig.onePage, getPages: RouteConfig.getPages, ); } } /// 路由配置 class RouteConfig { static const String onePage = "/onePage"; static const String twoPage = "/twoPage"; static final List<GetPage> getPages = [ GetPage( name: onePage, page: () => const OnePage(), binding: OnePageBinding(), ), // GetPage( // name: twoPage, // page: () => TwoPage(), // binding: TwoPageBinding(), // ), ]; } /// binding层 class OnePageBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => CounterController()); } } /// 逻辑层 class CounterController extends GetxController{ var count = 0; /// 自增方法 void increase(){ count++; update(); } } /// view层 class OnePage extends StatelessWidget { const OnePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { /// 直接使用Get.find()找到对应的GetxController使用。 final controller = Get.find<CounterController>(); return Scaffold( appBar: AppBar(title: const Text("Bindings"),), body: Center( child: GetBuilder<CounterController>( builder: (controller){ return Text("点击了${controller.count}次"); }, ), ), floatingActionButton: FloatingActionButton( onPressed: (){ controller.increase(); }, child: const Icon(Icons.arrow_forward_outlined) ), ); } }复制代码
第二种:使用initialBinding初始化所有的Binding
/// 入口类 class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return GetMaterialApp( /// 初始化所有的Binding initialBinding: AllControllerBinding(), home: const OnePage(), ); } } /// 所有的Binding层 class AllControllerBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => CounterController()); ///Get.lazyPut(() => OneController()); ///Get.lazyPut(() => TwoController()); } } /// 逻辑层 代码同上 /// view层 代码同上复制代码
第三种:使用构建器进行Binding绑定
不需要新建Bindings类来进行单独的绑定(
class OnePageBinding extends Bindings
),直接使用构建器BindingsBuilder。
/// 入口类 class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return GetMaterialApp( initialRoute: RouteConfig.onePage, getPages: RouteConfig.getPages, ); } } /// 路由配置 class RouteConfig { static const String onePage = "/onePage"; static final List<GetPage> getPages = [ GetPage( name: onePage, page: () => const OnePage(), /// 使用构建器BindingsBuilder binding: BindingsBuilder(()=>{ Get.lazyPut(() => CounterController()) }), ), ]; } /// 逻辑层 代码同上 /// view层 代码同上复制代码
四、GetView、GetWidget
个人不是太建议使用GetView、GetWidget,使用StatelessWidget足矣,怎么注入、怎么释放,简洁明了。
GetView
GetView
内部注入了控制器,有一个名为controller
的getter
方法 :
T get controller => GetInstance().find<T>(tag: tag)!;复制代码
帮我们实现了Get.Find()
。当我们只是单个控制器作为依赖项时,可以使用GetView
。避免使用Get.Find()
,直接使用controller
就可以。
逻辑层
class CounterController extends GetxController{ var count = 0; /// 自增方法 void increase(){ count++; update(); } }复制代码
view层
class OnePage extends GetView<CounterController> { const OnePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { Get.put(CounterController()); return Scaffold( appBar: AppBar(title: const Text("GetView"),), body: Center( child: GetBuilder<CounterController>( builder: (controller){ return Text("点击了${controller.count}次"); }, ), ), floatingActionButton: FloatingActionButton( onPressed: (){ controller.increase(); }, child: const Icon(Icons.arrow_forward_outlined) ), ); } }复制代码
使用方法非常简单,只是要将View层继承自GetView并传入需要注册的控制器并Get.put()即可。
GetWidget
一般情况下,在View层释放时,Controller也会自动释放。但是GetWidge缓存了一个Controller,它不会随着View层释放而释放。使用:Get.create(()=>Controller())
。
///逻辑层 代码同上 /// view层 class OnePage extends GetView<CounterController> { const OnePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { Get.create(()=>Controller()); return Scaffold( appBar: AppBar(title: const Text("GetView"),), body: Center( child: GetBuilder<CounterController>( builder: (controller){ return Text("点击了${controller.count}次"); }, ), ), floatingActionButton: FloatingActionButton( onPressed: (){ controller.increase(); }, child: const Icon(Icons.arrow_forward_outlined) ), ); } }复制代码
注意:
因为我们大部分情况下都不需要缓存Controller,所以会很少会使用到GetWidget,因为会一直缓存,慎用!!!
五、完整使用示例:新闻案例
使用一个小案例结合前面学习的知识做一个总结,主要只用MVC模式,包含网络请求、模型处理、GetXController的使用。
进入新闻列表的空白页面,显示加载的loading,加载完成,将数据显示在页面上,同时loading消失。
网络请求
/// 网络请求 class HttpService{ static Future<Map<String,dynamic>> getNews() async{ var response = await Dio().get("http://apis.juhe.cn/fapig/douyin/billboard?type=hot_video&size=50&key=9eb8ac7020d9bea6048db1f4c6b6d028"); return jsonDecode(response.toString()); } }复制代码
创建新闻模型类
/// 新闻模型类 class NewsModel{ late String title; late String shareUrl; late String author; late String itemCover; late int hotValue; late String hotWords; late int playCount; late int diggCount; late int commentCount; NewsModel.fromMap(Map<String,dynamic> json){ title = json["title"] ?? ""; shareUrl = json["share_url"] ?? ""; author = json["author"] ?? ""; itemCover = json["item_cover"] ?? ""; hotValue = json["hot_value"] ?? 0; hotWords = json["hot_words"] ?? ""; playCount = json["play_count"] ?? 0; diggCount = json["digg_count"] ?? 0; commentCount = json["comment_count"] ?? 0; } }复制代码
逻辑层
/// 逻辑层 class NewsController extends GetxController{ /// 是否在加载中,显示加载的loading bool isLoading = true; /// 新闻模型列表 List<NewsModel> newsList = []; @override void onInit() { // TODO: implement onInit getNewsList(); super.onInit(); } /// 数据请求与处理 void getNewsList() async { try{ Map<String,dynamic> map = await HttpService.getNews(); List list = map["result"]; newsList = List<NewsModel>.from(list.map((jsonMap) => NewsModel.fromMap(jsonMap))); newsList.removeAt(0); update(); }finally{ isLoading = false; update(); } } }复制代码
view层
/// 视图层 class NewsPage extends StatelessWidget { const NewsPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final counter = Get.put(NewsController()); return Scaffold( appBar: AppBar(title: const Text("新闻列表"),), body: GetBuilder<NewsController>( builder: (counter){ if(counter.isLoading == true){ return const Center( child: CircularProgressIndicator(), ); }else{ return ListView.builder( itemCount: counter.newsList.length, itemBuilder: (_,index){ NewsModel newsModel = counter.newsList[index]; return Container( margin: const EdgeInsets.symmetric(horizontal: 15,vertical: 10), height: 100, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Image.network(newsModel.itemCover, width: 120, fit: BoxFit.cover,), const SizedBox(width: 10,), Expanded( child: Text(newsModel.title,style: const TextStyle(fontSize: 16),), ) ], ), ); }, ); } }, ), ); } }复制代码
效果
作者:小虎牙和慧伢
链接:https://juejin.cn/post/7020598013986865182