[- Flutter 数据&状态篇 -] redux

2022-09-20 10:11:55 浏览数 (1)

本文比较渣,有待重构....

今天的任务是将昨的代码用redux整理一下。 在此之前先说统一几个名词在本文中的叫法。本文源码见github

代码语言:javascript复制
store       : 仓库
dispatch    : 分发
action      : 动作
reducer     : 分解器
connector   : 连接器
provider    : 供应器
converter   : 转换器
builder     : 构造器

依赖: flutter_redux: ^0.5.3

1.初始项目的Redux化

大家应该都还记得初始项目吧,下面是它的梳理图,磨刀不误砍柴工。 我打算从它开始入手,向你简单介绍redux是什么?


1.1:分析行为及变化

很简单,行为是点击,变化是数字的自增长。 关于reducer,不想是什么纯不纯,在我看来它就是一个独立的逻辑单元, 不依靠外界存活,在逻辑上便可存在:给定一个输入就会返回一个预期的输出

代码语言:javascript复制
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属性。

代码语言:javascript复制
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:现在的焦点在于孩子是如何构建的

这里为了看得清楚些,将countTextfab两个与状态有关的组件抽离一下

代码语言:javascript复制
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去取值了,通过构造器生成组件返回出去
代码语言:javascript复制
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) 在构造器中,你就可以使用该动作逻辑了。
代码语言:javascript复制
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了,看一下对象如何生成

代码语言:javascript复制
void main() {
  final store =  Store<int>(counterReducer, initialState: 0);
  runApp(ReduxPage(
    store: store,
  ));
}

2.redux优势

也许你会说:"感觉也不咋地啊,感觉好麻烦。"

2.1:增加一个功能时

比如我想要点一下加10该怎么办?使用redux你需要定义一个行为,及响应。 在行为分发时修改行为即可。也许你说我不用redux,改行就行了。如果逻辑非常多怎么办 之后又要改回来怎么办?抽象出一个行为来管理逻辑切换起来是非常方便的 而且想要修改直接在reducer中进行即可,就避免了污染封装的组件源码。

代码语言:javascript复制
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:全局的状态共享

另一个界面如何轻松享有上个界面的数据,这是个很大的问题。 当然可以通过构造传参,但这显然十分麻烦,不仅乱,而且还要接收个参数。

代码语言:javascript复制
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
代码语言:javascript复制
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描述类
代码语言:javascript复制
class Todo {
  String sth; //待做事项
  bool done;//是否已完成
  Todo({this.sth, this.done}); //是否已做完
}

3.2:定义状态类和动作及变化

昨天分析了有个有三个状态和四个动作

代码语言:javascript复制
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 &amp;&amp; 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对象 动作的话,作为攻方,依旧是那回调来执行相应动作。

代码语言:javascript复制
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隐藏,这时就非常方便了。

代码语言:javascript复制
--->[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,期待与你的交流与切磋。

0 人点赞