目前Flutter已经有许多状态管理的方案,但就我个人而言,并不能完全满足我的要求。我希望状态管理更加简单,而不是成为负担,我希望状态管理更加可靠,而不是使用过于复杂的实现。譬如目前最为简洁的get
库,为了实现一些黑科技语法糖,其实现就较为复杂。我们知道一台机器越复杂,可靠性就会降低。道理就如同在一些动乱地区,非常流行一些傻大黑粗的皮卡车,结构简单,皮实耐用。
为了兼具简洁和可靠性,同时摆脱对InheritedWidget
的限制,我使用注解和依赖注入库来实现,原理上类似Bloc,但更加简洁,他们都是使用Stream来做状态管理,这是就是EbloX库。
首先来看一个大家喜闻乐见的计数器示例:
1. 添加依赖dependencies:
eblox:
eblox_annotation:
dev_dependencies:
build_runner:
eblox_generator:
2. 编写我们的ViewModel类来处理业务逻辑
import 'package:eblox/blox.dart';
import 'package:eblox_annotation/blox.dart';
import 'package:flutter/cupertino.dart';
part 'counter_view_model.g.dart';
@bloX
class _CounterVModel extends Blox{
@StateX(name:'CounterState')
int _counter = 0;
@ActionX(bind: 'CounterState')
void _add() async{
_counter ++;
}
@ActionX(bind: 'CounterState')
void _sub(){
_counter--;
}
@override
void dispose() {
super.dispose();
debugPrint('CounterVModel dispose...');
}
}
首先从Blox继承一个以下划线开头的ViewModel类,该类需要使用@bloX
注解修饰 。接下来定义一个UI需要的状态数据_counter
,也必须以下划线_
开头,然后使用@StateX
注解修饰该数据,这样就能自动生成一个State类来包装该数据。@StateX
注解可以传name
参数生成指定名字的State类,也可以缺省,默认生成规则会去除变量名的下划线,然后首字母大写+State
,譬如变量_color
,则会生成ColorState
类。
接下来,我们需要定义动作,也就是对这个状态的 *** 作。使用@ActionX
注解修饰下划线_
开头的方法,就是一个动作,它会生成对应的Action类,生成规则与State类似,可以传name
指定名称,也可以缺省,如上例,将会生成AddAction
类和SubAction
类。bind
参数用于指定该动作关联的State类名称。
我们定义的两个方法分别用来自增、自减计数变量,但这两个方法不需要我们去调用,而且它们也是私有的。当UI上发出AddAction
或SubAction
动作时,对应的Action方法会自动调用。
在项目根路径下执行flutter pub run build_runner watch --delete-conflicting-outputs
命令,会生成counter_view_model.g.dart
文件
class CounterPage extends StatelessWidget {
const CounterPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child:BloxBuilder(
create:()=>CounterVModel(),
builder: (count) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("$count"),
ElevatedButton(
onPressed: () {
AddAction().to();
},
child: const Text("+"),
),
ElevatedButton(
onPressed: () {
SubAction().to();
},
child: const Text("-"),
),
],
),
);
}),
),
);
}
}
在UI上,可以使用BloxBuilder
组件来获取状态,它需要指定两个泛型,第一个是我们的ViewModel类CounterVModel
,第二个是我们的状态类CounterState
,接下来实现create
回调,可在此处实例化CounterVModel
。如果你不喜欢用create
实例化,也可以调用I.put(CounterVModel())
方法在其他任意地方实例化。builder
回调用于返回Widget,它的参数就是State。
当我们需要发起动作时,直接实例化相应的动作,并调用to
方法传入泛型,发起动作,触发计数器的状态改变。当状态改变时,UI能自动感知,也发生对应变化。
可以看到使用Eblox库的整体流程非常简单,开发者只需要定义状态和动作,然后在界面上发起动作即可。完整示例工程,请查看 这里。
原理说明状态管理不是必须品,使用状态管理框架的原因是希望开发更加简单,同时将业务逻辑与UI创建分离,使得项目可以长期维护下去,而不会把代码变成一座屎山。
代码之所以会变成屎山,大部分原因就是职责不明,代码相互耦合。我们以上述的计数器为例,如果在按钮的onPressed
中可以直接调用_add()
方法修改计数器,这就耦合了。一旦后续_add
方法内部发生修改,就会影响外部所有调用该方法的地方。onPressed
调用_add
也是一种不明确的行为,这种调用意味着什么呢?对于后续接手代码维护的人而言,代码中大量的这种不明确行为是让人崩溃的。
基于这些原因,我们需要状态管理框架,提升代码的可维护性。
现在,我们将业务逻辑写到Blox层,UI与业务逻辑之间的联系,由State和Action两个概念维系。这样就实现了业务逻辑与UI的分离。UI主要是接受用户的 *** 作的,这些 *** 作就是一个个动作,而UI的创建则需要状态,不同的状态决定了不同的UI,UI界面的变化,其实就是状态的变化。这样,我们只需要修改状态即可,UI就能自动感知,从而发生变化。
大多时候,数据可能来自于服务器,所以我们需要在Blox之下增加一层Service,由Service来封装与服务器的交互逻辑,Service隔离了具体的数据源,对于ViewModel而言,Service就是数据源。
常见案例计数器例子过于简单,我们来看一个更加常见的案例:
这是一个模拟歌曲搜索的界面,基于上述的State和Action概念来分析,我们首先需要明确State和Action。很明显,点击搜索按钮搜索就是一个动作,而搜索结果就是一个状态。
不同的状态,对应不同的UI界面,结果为空时,UI显示Empty
标签,有结果时,就显示结果项。
我们来看Eblox如何实现:
1. 定义状态和动作
part 'search_view_model.g.dart';
@bloX
class _SearchVModel extends Blox{
@AsyncX(name: 'SongListState')
SongListModel _songModel = SongListModel();
@bindAsync
@ActionX(bind: 'SongListState')
BloxAsyncTask _search(String name){
return (){
return SearchService.search(name);
};
}
}
注意,可以使用@AsyncX
注解来修饰异步状态。这里因为我们模拟数据是耗时加载的,因此需要异步加载。还有一点要注意,这里的_search
方法声明了参数。被@ActionX
注解的方法是可以声明参数的,不仅可以声明参数,还可以声明位置参数或命名参数(花括号中声明参数),此处是声明的位置参数。这些参数会自动包含到动作中,从而在发起动作时传参。
定义动作时,这里增加了一个注解@bindAsync
,用于修饰与异步状态关联的动作。需注意,@bindAsync
修饰的方法,返回值必须是BloxAsyncTask
类型,这里泛型T
是我们需要的异步状态的类型。它的原型其实是typedef BloxAsyncTask
接下来,就可以在_search
方法中编写加载数据的逻辑。前文已经说过,增加一个Service层封装数据源,这里直接调用Service提供的搜索接口搜索歌曲,但要注意返回值必须是一个Future
的方法类型,这里闭包包装一下即可。
2. 解析注解,生成代码
这里,可以使用flutter pub run build_runner build
命令手动生成,但每次新增或修改Blox里的代码时都要手敲一遍,因此推荐使用flutter pub run build_runner watch --delete-conflicting-outputs
命令,它可以开启监控,每次修改Blox代码时都会自动重新生成。
3. 编写UI
class SearchPage extends StatelessWidget {
SearchPage({Key? key}) : super(key: key);
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Song search'),),
body: SafeArea(
child: Column(
children: [
TextField(
controller: _controller,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
suffix: IconButton(
icon: const Icon(Icons.search_rounded),
onPressed: (){
if(_controller.text.isNotEmpty) {
SearchAction(_controller.text).to();
}
},
)),
),
Flexible(
child: BloxView>(
create: () => SearchVModel(),
onLoading: () => const Center(child: CircularProgressIndicator()),
onEmpty: ()=> const Center(child: Text("Empty")),
builder: (state) {
return ListView.builder(
itemCount: state.data.songs.length,
itemBuilder: (ctx, i) {
return Container(
alignment: Alignment.center,
height: 40,
child: Text(state.data.songs[i],style: const TextStyle(color: Colors.blueGrey,fontSize: 20),),
);
});
},
)),
],
),
),
);
}
}
这里界面比较简单,上下结构,上面一个输入框,下面是列表部分。当用户点击搜索按钮时,发起SearchAction
动作。注意,这里实例化SearchAction
类的参数就对应_search
方法中的参数。
对于异步状态,可以使用BloxView
来获取。它提供了onLoading
, onEmpty
, onError
,builder
等回调来处理数据加载过程中的状态。开始加载时,回调onLoading
,我们可以在此创建相应的加载动画,加载完成后,成功获取数据,回调builder
来构建界面,如果没有数据回调onEmpty
创建相应页面,加载报错,回调onError
。
这里需要小心,如果状态中包装的是自定义的数据类型,如此处的SongListModel
,你仍然希望onEmpty
起作用,那么该数据类需要混入BloxData
并实现isEmpty
方法:
class SongListModel with BloxData{
SongListModel({UnmodifiableListView? songs}){
if(songs !=null) this.songs = songs;
}
UnmodifiableListView songs = UnmodifiableListView([]);
@override
bool get isEmpty => songs.isEmpty;
}
用EbloX实现此案例,代码仍然十分简洁清晰。完整代码,请查看这里
其他使用Eblox实现业务逻辑与UI的分离后,代码测试变得更加简单方便,以一个简单的单元测试为例:
void main(){
const len = 10;
group('Counter test', () {
setUp(() {
I.put(CounterVModel());
});
tearDown(() {
I.delete();
});
test('test add counter', () {
for(var i = 0;i();
}
expect(Future(() => $().counter.data), completion(len));
});
test('test sub counter', () {
for(var i = 0;i();
}
expect(Future(() => $().counter.data), completion(-10));
});
});
}
另外,如果UI上需要多个状态,那么可以使用MultiBuilder
来处理。BloxBuilder
和BloxView
仅能处理单个状态。
目前Eblox处于0.0.2版本,仅测试了一些有限范围的使用,后续有时间会完善更多功能,欢迎大家测试BUG,不胜感激!
关注公众号:编程之路从0到1
了解更多技术干货
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)