[- Flutter 數據&狀態篇 -] redux

本文比較渣,有待重構....git

今天的任務是將昨的代碼用redux整理一下。
在此以前先說統一幾個名詞在本文中的叫法。本文源碼見githubgithub

store       : 倉庫
dispatch    : 分發
action      : 動做
reducer     : 分解器
connector   : 鏈接器
provider    : 供應器
converter   : 轉換器
builder     : 構造器

依賴: flutter_redux: ^0.5.3
複製代碼

1.初始項目的Redux化

你們應該都還記得初始項目吧,下面是它的梳理圖,磨刀不誤砍柴工。
我打算從它開始入手,向你簡單介紹redux是什麼?redux


1.1:分析行爲及變化

很簡單,行爲是點擊,變化是數字的自增加。
關於reducer,不想是什麼純不純,在我看來它就是一個獨立的邏輯單元,
不依靠外界存活,在邏輯上即可存在:給定一個輸入就會返回一個預期的輸出bash

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:如今的焦點在於孩子是如何構建的

這裏爲了看得清楚些,將countTextfab兩個與狀態有關的組件抽離一下app

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去取值了,經過構造器生成組件返回出去less

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)
在構造器中,你就可使用該動做邏輯了。ide

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如何構建post


1.4:倉庫對象的構建

能夠說這核心即是倉庫store了,看一下對象如何生成ui

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,期待與你的交流與切磋。

相關文章
相關標籤/搜索