Flutter Redux:带例子的完整教程

2,688 阅读10分钟

在任何典型的应用程序中,包括Flutter应用程序,总是在一个类内或多个类之间进行数据传输。一个类或函数产生的数据要被消耗,无论是在其内部还是由另一个类或函数。数据很可能是通过构造函数从一个部件传递到另一个部件。

有时,你可能不得不在数据最终到达其目标部件之前跨越几层部件来传递数据。在这种情况下,中间的几层小组件并不需要这些数据,而是作为工具将数据传递给需要它的小组件。

这是一种非常低效的应用程序内的数据管理技术,尤其是在大规模的情况下。它导致了大量的模板代码,并可能导致你的应用程序的性能下降。

本文将探讨Flutter应用状态管理及其中的一些技术。我们还将深入探讨如何使用Redux在我们的Flutter应用程序中有效地管理数据。

什么是 Redux?

Redux是一个状态管理架构库,它成功地将数据以重复的方式分布在小部件上。它通过数据的单向流动来管理应用程序的状态。让我们来探讨一下下面的图。

Diagram for explaining unidirectional data flow

在这个例子中,主小部件中产生的数据在子小部件8中需要。通常情况下,这些数据通过子部件2到子部件6,然后,最后到达子部件8。对于需要在层次结构中较高的任何部件的状态中生成或保存数据的部件来说,情况也是这样的。

通过Redux,你可以构造你的应用程序,使状态被提取在一个集中定位的存储中。这个集中存储的数据可以被任何需要该数据的widget访问,而不需要通过树中的其他widget链。

The data flow when Redux is used to structure state management

任何需要添加、修改或检索由Redux存储管理的状态中的数据的widget,都必须用适当的参数来请求它。

同样地,对于状态的每一个变化,依赖的widget都会通过用户界面或任何其他配置的方式对变化做出响应。

为什么状态管理很重要?

在一个有许多widget的中型或大型应用程序中,当一个子widget需要数据时,通常要管理来自main.dart 文件的数据。

这些数据可以通过小部件的构造器作为参数分发,直到数据到达接收的小部件,但正如我们在介绍中所讨论的,这可能会导致通过不需要这些数据的小部件进行数据传输的长链。

通过构造函数传递数据不仅会很麻烦和困难,而且还会影响到应用程序的性能。这是因为当你从主部件--或任何根部件--管理数据时,每当其任何子部件发生变化时,整个部件树都会重建。你只想在需要更改数据的小组件中运行build 方法。

Redux是一个单一的真相来源

在你的应用程序中,应该只有一个信息存储。这不仅有助于调试,而且每次数据在你的应用程序中发生变化时,你都能更容易地检测到它在哪里以及为什么发生变化。

不变性

你的应用程序的状态应该是不可改变的,并且只能通过读取它来访问。这部分意味着,如果你想改变一个状态内的值,你必须用一个包含你的新值的新状态来完全替换该状态。

这有助于确保存储的安全性,并且只允许通过操作来改变状态。它也使应用程序内部透明化,因为你总是可以检测到状态变化的原因和负责这些变化的对象。

函数应该是状态的改变者

对状态的改变应该只由函数发生。这些被称为reducers的函数,是唯一被允许对你的应用程序的状态进行改变的实体。

@immutable
class AppState{
  final value;
  AppState(this.value);
}

enum Actions {Add, Subtract};

AppState reducer(AppState previousState, action){
  if(action == Actions.Add){
    return new AppState(previousState.value +  1);
  }
  if(action == Actions.Subtract){
    return new AppState(previousState.value -  1);
  }
  return previousState;
}

在上面的代码中,AppState 是一个不可变的类,它持有value 变量的状态。

状态中允许的操作是AddSubtract

对状态的修改是通过reducer 函数完成的,该函数接收状态,以及对状态进行的操作。

Flutter Redux的架构

商店

这是应用状态存在的中心位置。存储器持有整个应用状态或任何其他单一状态在每个给定时间的信息。

final Store<AppState> store = Store<AppState>(
reducer, initialState: AppState.initialState()
);

reducer 是一个用新状态更新存储的函数。它接收状态和动作作为参数,并根据动作更新状态。

回顾一下,状态是不可改变的,并且是通过创建一个新的状态来更新。还原器是对状态进行更新的唯一途径。

Flutter使用继承的widget来操作存储。这些继承的部件中的一些是。

  • StoreProvider: 这个部件将商店注入到应用程序的部件树中。
class MyApp extends StatelessWidget {

 @override
 Widget build(BuildContext context) {

     ...

   return StoreProvider<AppState>(
     store: store,
     child: MaterialApp(
       title: 'Flutter Demo',
       theme: ThemeData.dark(),
       home: StoreBuilder<AppState>(
        ...
       ),

     ),
   );
 }
}
  • StoreBuilder: 这个听从整个存储,并在每次更新时重建整个widget树;它从StoreProvider 和 接收存储。StoreConnector
@override
 Widget build(BuildContext context) {

  ...

   return StoreProvider<AppState>(
     store: store,
     child: MaterialApp(
       title: 'Flutter Demo',
       theme: ThemeData.dark(),
       home: StoreBuilder<AppState>(
           onInit: (store) => store.dispatch(Action()),
         builder: (BuildContext context, Store<AppState> store) => MyHomePage(store),
       ),

     ),
   );
 }
  • StoreConnector: 这是一个用来代替StoreBuilder 的widget。它接收来自StoreProvider 的存储,从我们的存储中读取数据,然后将其发送到其builder 函数。然后,每当数据发生变化时,builder 函数就会重建这个小部件。
class MyHomePage extends StatelessWidget{
 final Store<AppState> store;

 MyHomePage(this.store);

 @override
 Widget build(BuildContext context){

   return Scaffold(
     appBar: AppBar(
       title: Text('Redux Items'),
     ),
     body: StoreConnector<AppState, Model>(
       builder: (BuildContext context, Model model) {
   ...
  }
 }
}

什么是Redux动作?

通常情况下,当一个状态被存储时,应用程序周围有一些小部件和子小部件来监控这个状态和它的当前值。动作是决定对该状态执行什么事件的对象。

在这个状态上执行的事件之后,这些跟踪状态中的数据的小部件会重建,它们呈现的数据也会更新到状态中的当前值。行动包括任何传递到商店的事件,以更新应用程序的状态。

当任何widget想要使用动作对状态进行改变时,它使用商店的dispatch 方法来与状态沟通这个动作--商店调用这个动作。

final Store<AppState> store = Store<AppState>(
reducer, initialState: AppState.initialState()
);


store.dispatch(Action());

状态的变化是如何影响用户界面的?

对状态的更新反映在用户界面上--每次状态更新,它都会触发在StoreConnector widget内重建用户界面的逻辑,该逻辑随着状态的每次变化而重建widget树。

Diagram describing the effect of state changes on UI

假设屏幕上有一个弹出式窗口,需要用户以点击或敲击按钮的形式做出反应。我们把这个弹出窗口看作是上图中的 "视图"。

点击按钮的效果就是动作。这个动作被包装好并发送给Reducer,后者处理这个动作并更新Store中的数据。然后,存储空间持有应用程序的状态,状态检测数据值的这种变化。

由于在屏幕上呈现的数据是由状态管理的,所以数据的这种变化会反映在视图中,循环往复。

Redux中间件

中间件是一个Redux组件,它在reducer 函数接收之前处理一个动作。它接收应用程序的状态和调度的动作,然后用动作执行定制的行为。

比方说,你想执行一个异步操作,比如从外部API加载数据。中间件拦截动作,然后进行异步任务,并记录可能发生的任何副作用或显示的任何其他自定义行为。

这就是带有中间件的Redux流程的样子。

Redux state management with middleware

把它放在一起

让我们把到目前为止所学到的一切,建立一个基本的应用程序,在Flutter中实现Redux。

我们的演示应用程序将包含一个带有按钮的界面,每次点击都会获取一个地点的当前时间。该应用程序向世界时间API发送一个请求,以获取启用该功能所需的时间和位置数据。

Flutter Redux的依赖性

在你的终端上运行该命令。

flutter create time_app

在您的pubspec.yaml 文件中添加以下依赖项,然后运行flutter pub get

  • Redux:包含并提供在Flutter应用程序中使用Redux所需的基本工具,包括。
    • 将用于定义商店的初始状态的商店
    • reducer 函数
    • 中间件
  • flutter_redux:是对Redux包的补充,它安装了一组额外的实用小工具,包括。
    • StoreProvider
    • StoreBuilder
    • StoreConnector
  • flutter_redux_dev_tools。这个包的功能类似于Flutter Redux包,但包含了更多的工具,你可以用它来跟踪相关的状态和动作的变化
  • redux_thunk:用于中间件注入
  • http:启用通过中间件的外部API调用
  • 国际:启用从API收到的时间数据的格式化。

接下来,为AppState 添加以下代码。

class AppState {
  final String _location;
  final String _time;

  String get location => _location;
  String get time => _time;

  AppState(this._location, this._time);

  AppState.initialState() : _location = "", _time = "00:00";

}

这里的应用程序状态有字段来显示位置和时间。这些字段最初被设置为空值。我们还为每个字段提供了一个getter方法来检索它们各自的值。

接下来,我们将编写我们的动作类,FetchTimeAction

class FetchTimeAction {
  final String _location;
  final String _time;

  String get location => _location;
  String get time => _time;

  FetchTimeAction(this._location, this._time);
}

action 类也有与AppState 相同的字段。当这个动作被调用时,我们将使用字段中的值来更新状态。

现在我们将编写AppState reducer 函数。

AppState reducer(AppState prev, dynamic action) {
  if (action is FetchTimeAction) {
    return AppState(action.location, action.time);
  } else {
    return prev;
  }
}

reducer 函数接收状态和动作。如果动作是一个FetchTimeAction ,它将使用动作字段中的值返回一个新的状态。否则,它将返回到之前的状态。

中间件的代码如下。

ThunkAction<AppState> fetchTime = (Store<AppState> store) async {

  List<dynamic> locations;

  try {
    Response response = await get(
        Uri.parse('http://worldtimeapi.org/api/timezone/'));
    locations = jsonDecode(response.body);
  }catch(e){
    print('caught error: $e');
    return;
  }

  String time;
  String location = locations[Random().nextInt(locations.length)] as String;
  try {
    Response response = await get(
        Uri.parse('http://worldtimeapi.org/api/timezone/$location'));
    Map data = jsonDecode(response.body);

    String dateTime = data['datetime'];
    String offset = data['utc_offset'].substring(1, 3);

    DateTime date = DateTime.parse(dateTime);
    date = date.add(Duration(hours: int.parse(offset)));
    time = DateFormat.jm().format(date);
  }catch(e){
    print('caught error: $e');
    time = "could not fetch time data";
    return;
  }

  List<String> val = location.split("/");
  location = "${val[1]}, ${val[0]}";

  store.dispatch(FetchTimeAction(location, time));

};

fetchTime 是一个异步函数,接收store 作为其唯一参数。在这个函数中,我们向API发出一个异步请求,以获取可用的位置列表。

然后,我们使用DartRandom() 函数在列表中选择一个随机位置,并进行另一个异步请求,以获取所选位置的时间。我们还格式化了从API收到的日期和位置值,以适应我们的应用。

最后,我们向商店发送了一个FetchTimeAction ,以便我们可以更新新的状态。

现在,让我们来构建我们应用程序的其余部分。

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {

  final store = Store<AppState>(reducer,
      initialState: AppState.initialState(),
      middleware: [thunkMiddleware]);

// root widget
  @override
  Widget build(BuildContext context) {
    return StoreProvider<AppState>(
      store: store,
      child: MaterialApp(
        title: 'Flutter Redux Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {

 
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title:   const Text("Flutter Redux demo"),
      ),
      body:   Center(
        child:   Container(
          height: 400.0,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[

              // display time and location
              StoreConnector<AppState, AppState>(
                converter: (store) => store.state,
                builder: (_, state) {
                  return  Text(
                    'The time in ${state.location} is ${state.time}',
                    textAlign: TextAlign.center,
                    style: const TextStyle(fontSize: 40.0, fontWeight: FontWeight.bold),
                  );
                },
              ),

              // fetch time button
              StoreConnector<AppState, FetchTime>(
                converter: (store) => () => store.dispatch(fetchTime),
                builder: (_, fetchTimeCallback) {
                  return   SizedBox(
                    width: 250,
                    height: 50,
                    child: RaisedButton(
                        color: Colors.amber,
                        textColor: Colors.brown,
                        onPressed: fetchTimeCallback,
                        child:   const Text(
                            "Click to fetch time",
                          style: TextStyle(
                            color: Colors.brown,
                            fontWeight: FontWeight.w600,
                            fontSize: 25
                          ),
                        )),
                  );
                },
              )
            ],
          ),
        ),
      ),
    );
  }
}

typedef FetchTime = void Function();

我们首先分配了一个Store<AppState> 的实例。然后,我们将MaterialApp 包裹在StoreProvider<AppState> 。这是因为它是一个基础部件,将把给定的Redux商店传递给所有请求它的后代。

渲染位置和时间的Text widget是依赖于商店的后代widget之一,所以我们把它包裹在StoreConnector ,以实现商店和这个widget之间的通信。

RaisedButton 小组件是第二个依赖商店的小组件。我们也把它包装在StoreConnector 。每次点击都会触发中间件运行其功能,从而更新应用程序的状态。

这就是我们最终的应用程序的样子。

The visual for our final app

结论

在Flutter应用程序,或一般的前端应用程序中,管理你的数据和反映它的用户界面是关键。

数据是一个相当广泛的术语。它可以指你的应用程序上显示的任何数值,其意义范围包括确定用户是否登录,或由你的应用程序用户产生的任何形式的互动结果。

当构建你的下一个应用程序时,我希望这篇文章能提供一个全面的指南,告诉你如何使用Flutter Redux架构有效地构建它。

The postFlutter Redux: Complete tutorial with examplesappeared first onLogRocket Blog.