本文比较渣,有待重构....
今天的任务是将昨的代码用redux整理一下。
在此之前先说统一几个名词在本文中的叫法。本文源码见github
store : 仓库
dispatch : 分发
action : 动作
reducer : 分解器
connector : 连接器
provider : 供应器
converter : 转换器
builder : 构造器
依赖: flutter_redux: ^0.5.3
复制代码
1.初始项目的Redux化
大家应该都还记得初始项目吧,下面是它的梳理图,磨刀不误砍柴工。
我打算从它开始入手,向你简单介绍redux是什么?
1.1:分析行为及变化
很简单,行为是点击,变化是数字的自增长。
关于reducer,不想是什么纯不纯,在我看来它就是一个独立的逻辑单元,
不依靠外界存活,在逻辑上便可存在:给定一个输入就会返回一个预期的输出
enum Actions {
increment//定义增加行为
}
//使用counterReducer将行为从类中抽离分解,成为独立逻辑单元
int counterReducer(int input, dynamic action) {
var output;
switch(action){
case Actions.increment:
output=input+1;
break;
}
return output;
}
复制代码
1.2:新建ReduxPage组件
redux核心之一便是Store,是一个仓库用来储存,供应,分发。
返回一个仓库提供器,它是一个Widget,需要store和child属性。
class ReduxPage extends StatelessWidget {
final Store<int> store;
ReduxPage({Key key, this.store,}) : super(key: key);
return StoreProvider<int>(
store: store,
child: child,
);
}
复制代码
1.3:现在的焦点在于孩子是如何构建的
这里为了看得清楚些,将
countText
与fab
两个与状态有关的组件抽离一下
var child = MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text("Flutter Redux Demo"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'You have pushed the button this many time'
),
countText//显示数字的Text
],
),
),
floatingActionButton: fab,//点击的按钮
),
);
复制代码
显示数字的Text:countText
如果你想要到仓库拿东西,你需要什么?钥匙呗。
StoreConnector
仓库连接器就是这把钥匙
converter转换器中回调出store对象,你就可以通过store去取值了,通过构造器生成组件返回出去
var countText= StoreConnector<int, String>(
converter: (store) => store.state.toString(),//转换器,获取仓库,从仓库拿值
builder: (context, count) {//构造器,构建Widget
return Text(
count,
style: Theme.of(context).textTheme.display1,
);
},
);
复制代码
处理动作的按钮
处理动作也是需要仓库,使用进行分发(dispatch)相应动作(action)
在构造器中,你就可以使用该动作逻辑了。
var fab= StoreConnector<int, VoidCallback>(
converter: (store) {
return () => store.dispatch(Actions.increment);//分发动作
},
builder: (context, callback) {//构造器,使用动作逻辑
return FloatingActionButton(
onPressed: callback,
tooltip: 'Increment',
child: Icon(Icons.add),
);
},
);
复制代码
这里将动作方成为攻方,响应方成为受方,下面的图阐释了两方Widget如何构建
1.4:仓库对象的构建
可以说这核心便是仓库store了,看一下对象如何生成
void main() {
final store = Store<int>(counterReducer, initialState: 0);
runApp(ReduxPage(
store: store,
));
}
复制代码
2.redux优势
也许你会说:"感觉也不咋地啊,感觉好麻烦。"
2.1:增加一个功能时
比如我想要点一下加10该怎么办?使用redux你需要定义一个行为,及响应。
在行为分发时修改行为即可。也许你说我不用redux,改行就行了。如果逻辑非常多怎么办
之后又要改回来怎么办?抽象出一个行为来管理逻辑切换起来是非常方便的
而且想要修改直接在reducer中进行即可,就避免了污染封装的组件源码。
enum Actions{
increment,
increment10
}
int counterReducer(int input, dynamic action) {
var output;
switch(action){
case Actions.increment:
output=input+1;
break;
case Actions.increment10:
output=input+10;
break;
}
return output;
}
var fab= StoreConnector<int, VoidCallback>(
converter: (store) {
return () => store.dispatch(Actions.increment10);
},
复制代码
2.2:全局的状态共享
另一个界面如何轻松享有上个界面的数据,这是个很大的问题。
当然可以通过构造传参,但这显然十分麻烦,不仅乱,而且还要接收个参数。
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
class SecondPage extends StatelessWidget {
SecondPage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
var text = StoreConnector<int, String>(//直接从状态取值
converter: (store) => store.state.toString(),
builder: (context, count) {
return Text(
count.toString(),
style: Theme.of(context).textTheme.display1,
);
},
);
return Scaffold(
appBar: AppBar(
title: Text("SecondPage"),
),
body: Align(
alignment: Alignment.topCenter,
child: text,
),
);
}
}
复制代码
ReduxPage中为文字添加点击跳转到SecondPage
Builder _skipToSecondPage(StoreConnector<int, String> countText) {
return Builder(
builder: (context) =>
InkWell(child: countText, onTap: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (BuildContext context) {
return SecondPage();
}));
},),
);
}
复制代码
3.对昨天TodoList的改造
还是一样的界面效果。
3.1:定义Todo描述类
class Todo {
String sth; //待做事项
bool done;//是否已完成
Todo({this.sth, this.done}); //是否已做完
}
复制代码
3.2:定义状态类和动作及变化
昨天分析了有个有三个状态和四个动作
class TodoState {
List<Todo> todos; //列表数据
String text; //当前输入文字
ShowType showType;//显示类型
TodoState({this.todos, this.text, this.showType}); //显示类型
}
enum Acts {
add, //添加到todo
selectAll, //筛选所有
selectTodo, //筛选待完成
selectDone, //筛选已完成
}
TodoState todoReducer(TodoState input, dynamic action) {
switch (action) {
case Acts.add:
if (input.text != null && input.text != "") {
input.todos.add(Todo(sth: input.text, done: false));
input.text = "";
}
break;
case Acts.selectAll:
input.showType=ShowType.all;
break;
case Acts.selectTodo:
input.showType=ShowType.todo;
break;
case Acts.selectDone:
input.showType=ShowType.done;
break;
}
return input;
}
final todoListStore = Store<TodoState>(todoReducer,
initialState://初始状态
TodoState(todos: <Todo>[], text: "", showType: ShowType.all));
复制代码
3.3:组件的封装
和上面一样,使用StoreConnector来从仓库拿资源,只不过这里资源是TodoState对象
动作的话,作为攻方,依旧是那回调来执行相应动作。
class TodoList extends StatefulWidget {
final Store<TodoState> store;
TodoList({
Key key,
this.store,
}) : super(key: key);
@override
_TodoListState createState() => _TodoListState();
}
class _TodoListState extends State<TodoList> {
@override
Widget build(BuildContext context) {
var textField= StoreConnector<TodoState, TodoState>(
converter: (store) =>store.state,//转换器,获取仓库,从仓库拿值
builder: (context, state) {//构造器,构建Widget
return TextField(
controller: TextEditingController(text: state.text),
keyboardType: TextInputType.text,
textAlign: TextAlign.start,
maxLines: 1,
cursorColor: Colors.black,
cursorWidth: 3,
style: TextStyle(
fontSize: 16, color: Colors.lightBlue, backgroundColor: Colors.white),
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
hintText: '添加一个待办项',
hintStyle: TextStyle(color: Colors.black26, fontSize: 14),
contentPadding: EdgeInsets.only(left: 14.0, bottom: 8.0, top: 8.0),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.white),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10), bottomLeft: Radius.circular(10)),
),
),
onChanged: (str){
state.text=str;
},
);
},
);
var btn = StoreConnector<TodoState, VoidCallback>(
converter:(store) {
return () => store.dispatch(Acts.add);//分发动作
},
builder: (context, callback) {
return RaisedButton(
child: Icon(Icons.add),
padding: EdgeInsets.zero,
onPressed: (){
callback();
FocusScope.of(context).requestFocus(FocusNode());
});
},
);
var inputBtn = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
child: textField,
width: 200,
),
ClipRRect(
borderRadius: BorderRadius.only(
topRight: Radius.circular(10), bottomRight: Radius.circular(10)),
child: Container(
child: btn,
width: 36,
height: 36,
),
),
],
);
var listInfo = [
["全部", Acts.selectAll],
["已完成", Acts.selectDone],
["未完成", Acts.selectTodo],
];
var op = Row(//操作按钮
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: listInfo.map((e) {
return StoreConnector<TodoState, VoidCallback>(
converter: (store) {
return () => store.dispatch(e[1]);
},
builder: (context, callback) {
return RaisedButton(
onPressed: callback,
child: Text(e[0]),
color: Colors.blue,
);
},
);
}).toList(),
);
var listView = StoreConnector<TodoState, TodoState>(
converter: (store) => store.state, //转换器,获取仓库,从仓库拿值
builder: (context, state) {
var result;
//构造器,构建Widget
switch(state.showType){
case ShowType.all:
result= formList(state.todos);
break;
case ShowType.todo:
result= formList(List.of( state.todos.where((e)=>!e.done)));
break;
case ShowType.done:
result= formList(List.of( state.todos.where((e)=>e.done)));
break;
}
return result;
});
return StoreProvider<TodoState>(
store: widget.store,
child: Column(
children: <Widget>[inputBtn, op, Expanded(child: listView)],
),
);
}
Widget formList(List<Todo> todos) {
return ListView.builder(
itemCount: todos.length,
padding: EdgeInsets.all(8.0),
itemExtent: 50.0,
itemBuilder: (BuildContext context, int index) {
var key = todos[index].sth;
var value = todos[index].done;
var text = Align(
child: Text(
key,
style: TextStyle(
decorationThickness: 3,
decoration:
value ? TextDecoration.lineThrough : TextDecoration.none,
decorationColor: Colors.blue),
),
alignment: Alignment.centerLeft,
);
return Card(
child: Row(
children: <Widget>[
Checkbox(
onChanged: (b) {
todos[index].done = b;
setState(() {});
},
value: todos[index].done,
),
text
],
),
);
},
);
}
}
复制代码
3.3:ViewModel的使用
可以看到StoreConnector中有两个泛型,其中第二个命名为ViewModel
现在实现一下,在已完成和未完成按钮点击后将CheckBox隐藏,这时就非常方便了。
--->[1.加入一个状态]----
class TodoState {
List<Todo> todos; //列表数据
String text; //当前输入文字
ShowType showType;//显示类型
bool showBox=true;//是否显示checkBox
TodoState({this.todos, this.text, this.showType,this.showBox}); //显示类型
}
--->[2.在todoReducer中直接修改showBox状态]----
case Acts.selectAll:
input.showBox=true;
input.showType=ShowType.all;
break;
case Acts.selectTodo:
input.showType=ShowType.todo;
input.showBox=false;
break;
case Acts.selectDone:
input.showType=ShowType.done;
input.showBox=false;
break;
---->[提取出checkBox组件代码]----
var checkBox=StoreConnector<TodoState, CheckBoxViewModel>(
converter: (store) {
return CheckBoxViewModel(store,index);
},
builder: (context, model) {
return Offstage(child:Checkbox(
value: model.done,
onChanged: model.onClick) ,
offstage: !model.store.state.showBox,) ;
},
);
---->[定义CheckBox模型]----
class CheckBoxViewModel {
final Store store;
final int index;
void Function(bool check) onClick;
CheckBoxViewModel(this.store,this.index) {
onClick = (check) {
store.dispatch(Acts.check);
store.state.todos[index].done = check;
};
}
get done{
return store.state.todos[index].done;
}
}
复制代码
这样就可以很容易对状态进行更改,否则,要在TodoList中直接改会很费劲。
结语
本文到此接近尾声了,如果想快速尝鲜Flutter,《Flutter七日》会是你的必备佳品;如果想细细探究它,那就跟随我的脚步,完成一次Flutter之旅。
另外本人有一个Flutter微信交流群,欢迎小伙伴加入,共同探讨Flutter的问题,本人微信号:zdl1994328
,期待与你的交流与切磋。