状态管理是数据驱动视图设计理念中非常重要的一个概念,是每一个Flutter开发者都需要掌握的。
状态管理的本质是管理数据,如何获取,如何接收数据,并通过数据的变化驱动视图UI的改变。
2019 Google I/O 大会,Google就推出Provider,成为官方推荐的状态管理方式之一。Provider包基于InheritedWidget思想实现的一套跨组件状态共享解决方案,绑定InheritedWidget与依赖它的子孙组件的依赖关系,并且当InheritedWidget数据发生变化时,可以自动更新依赖的子孙组件!利用这个特性,我们可以将需要跨组件共享的状态保存在InheritedWidget中,然后在子组件中引用InheritedWidget即可。
笔者在使用Provider的过程中,发现了许多痛点,最大的痛点在于Provider只能在父子组件保存状态,同层级模块状态管理需要全局处理,也就是说非父子组件的状态管理问题,需要借助其他手段(全局,单例,eventbus)实现。也许后续的版本已经解决,笔者没有继续追踪。
也许正是存在这样的痛点,GetX应运而生,GetX可以随时添加、删除控制器Controller,使用控制器Controller便能轻松的进行父子、同级组件间的状态管理!
本小册便使用了GetX,GetX是Flutter状态管理的一个轻量且强大的解决方案,github地址:github.com/jonataslaw/… ,GetX的核心思想与实现:
- 便捷的路由管理
- 高性能的状态管理
- 智能的依赖注入
1,便捷的路由管理
1.1,普通路由
首先看看原生Flutter路由中,是如何跳转页面的:
Navigator.push(context, MaterialPageRoute<void>(
builder: (BuildContext context) {
return NextPage();
},
));
GetX中:
Get.to(NextPage());
就是这么便捷简单。
1.2,命名路由
对应于原生Flutter中的
Navigator.pushNamed(context, "nextPage")
GetX中:
Get.toNamed(Routes.NextPage);
可以看到GetX并不需要上下文参数context便能实现路由跳转。
注册路由表,命名路由和页面一一对应
abstract class AppPages {
static final pages = [
GetPage(
name: Routes.Initial,
page: () => HomePage(),
),
GetPage(
name: Routes.NextPage,
page: () => NextPage(),
),
];
}
abstract class Routes {
static const Initial = '/';
static const NextPage = '/NextPage';
}
在main()函数中,将MaterialApp替换为GetMaterialApp
void main() {
runApp(GetMaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: Routes.Initial, //App的初始页面(第一个页面)
theme: appThemeData,
defaultTransition: Transition.fade,
getPages: AppPages.pages, //注册所有命名路由
));
}
参数传递
Get.toNamed(Routes.NextPage, arguments: '你好');
在NextPage()中接收参数
String text = Get.arguments;
从上可以看出,GetX大大简化了路由管理,其他未述用法细节如Snackbar, Dialog, BottomSheet的跳转可以参照GetX官方文档。
2,高性能的状态管理
首先我们来看一个典型的使用GetX框架的页面结构:
controller是控制器,继承GetxController,可以重写onInit(),onReady()等生命周期函数,通常在controller中定义页面page需要的数据变量。controller的作用便是获取数据,处理数据,数据的变化从而驱动视图page的改变!
page便是页面的具体的布局文件。
binding则是将controller绑定到page上,也就是说由这个controller来控制这个page,形成一对一的对应关系,这个具体在下一小节讲解。
我们来具体看下面这个例子:
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetBuilder<HomeController>(
init: HomeController(),
builder: (controller) {
return Scaffold(
appBar: AppBar(title: Text('HomePage')),
body: Center(
child: Text(controller.counter.toString()), //显示controller中变量counter的值
),
floatingActionButton: FloatingActionButton(
onPressed: () {
controller.increment(); //点击按钮调用controller的方法,更改变量counter的值
},
child: Icon(Icons.add),
),
);
});
}
}
整个build()方法使用GetBuilder<HomeController>包裹其中,并在init参数中初始化了这个page的控制器HomeController,下面是controller代码块:
class HomeController extends GetxController {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
update();
}
}
点击page上按钮,调用controller中的increment()方法,改变了_counter的值,同时调用GetX方法update(),这样page中使用GetBuilder()包裹的child都会刷新。这便实现了数据驱动视图的改变。
上面的例子可以看出,点击按钮后,整个GetBuilder()包裹的child,text和button都会刷新,但是显然button本身是不需要刷新的。那么我们仅仅需要把GetBuilder()包裹在text的外层即可。这便实现了最小局部的刷新,性能提升了。
上面例子刷新页面时,手动调用了update()方法,有没有可能在变量值发生变化时,无需调用update()方法,就能自动实现page的刷新呢?答案是肯定的,GetX推出了响应性刷新,之前定义变量如下:
var textString = 'hello';
我们只需要加一个简单的后缀,就能将textString变量变成可观察变量,改变变量值时,使用它的widget便能被刷新。
var textString = 'hello'.obs;
使用该变量的widget也就不使用上面的GetBuilder包裹了,而是改成如下两种方式:
GetX<HomeController>(
builder: (_) {
return Text(
'${_.count1}',
style: TextStyle(fontWeight: FontWeight.bold),
);
},
),
Obx(() => Text(
'${Get.find<HomeController>().count1}',
style: TextStyle(fontWeight: FontWeight.bold),
)),
3,智能的依赖注入
在第1章节中,提到binding的作用是将controller注入到内存中,将controller和page相关联,那么这个注入动作和关联动作是如何实现的呢?
关联动作:在路由定义的时候,加入binding属性
abstract class AppPages {
static final pages = [
//闪屏页
GetPage(
name: Routes.SPLASH,
page: () => SplashPage(),
binding: SplashBinding()),
//主页
GetPage(name: Routes.HOME, page: () => HomePage(), binding: HomeBinding()),
];
}
注入动作:在binding.dart中,注入controller
class SplashBinding extends Bindings{
@override
void dependencies() {
Get.put<SplashController>(SplashController()); //注入代码
}
}
Get.put<SplashController>(SplashController())注入的controller的生命周期,和其绑定的page(widget)的生命周期一致,page从内存中删除,则controller也会被销毁。那么如果在App入口函数main()中put,则这个controller便像一个单例一样,一直存在。
GetX还有下面几种注入方式:
- 延迟注入
Get.lazyPut<SplashController>(() => SplashController());
这个时候SplashController并不会被创建,而是在使用它的时候才会被创建,
Get.find<SplashController>(); //使用时创建
- 异步注入
当注入的
controller需要一定的时候初始化时,我们可以选择异步注入
Get.putAsync<SharedPreferences>(() async {
final sp = await SharedPreferences.getInstance();
return sp;
});
最后需要的注意的一点是:我们通常在App中有这样的跳转需求,商品详情页A -> 其他页面B -> 商品详情页C,路由中出现了两个相同的页面page,此时在为商品详情页注入controller的时候需要加上tag属性进行区分。
Get.put<ItemDetailController>(ItemDetailController(), tag:"你设置的tag值");