Flutter中间件处理插件redux_epics的使用

发布于 1周前 作者 wuwangju 来自 Flutter

Flutter中间件处理插件redux_epics的使用

Redux Epics

Travis Build Status

Redux非常适合用于同步更新store以响应actions。然而,对于复杂的异步操作(如自动完成搜索体验),传统的middleware可能有些棘手。这就是Epics发挥作用的地方!

最好的部分是:Epics基于Dart Streams构建。这使得常规任务变得简单,而复杂任务(如异步错误处理、取消和节流)也变得轻而易举。

注意:对于不熟悉Streams的用户,简单的异步情况更容易通过普通的Middleware函数处理。如果普通的Middleware函数或Thunks对你来说已经足够,那么你做得很好!当你发现自己需要处理更复杂的情况时,例如编写一个自动完成UI,请参考下面的示例代码,看看如何利用Streams/Epics使你的生活更轻松。

示例

假设你的应用程序有一个搜索框。当用户提交搜索词时,会触发PerformSearchAction,其中包含搜索词。为了监听PerformSearchAction并进行网络请求获取结果,我们可以创建一个Epic!

在这个例子中,我们的Epic将过滤接收到的所有动作,只对PerformSearchAction感兴趣。这将通过在Streams上使用where方法来实现。然后,我们需要使用asyncMap方法根据搜索词发起网络请求。最后,我们需要将这些结果转换为包含搜索结果的动作。如果发生错误,我们将返回一个错误动作,以便应用程序能够相应地作出反应。

以下是上述描述的代码实现:

import 'dart:async';
import 'package:redux_epics/redux_epics.dart';

Stream<dynamic> exampleEpic(Stream<dynamic> actions, EpicStore<State> store) {
  return actions
    .where((action) => action is PerformSearchAction)
    .asyncMap((action) => 
      // 假设的API,返回一个Future<SearchResults>
      api.search((action as PerformSearch).searchTerm)
        .then((results) => SearchResultsAction(results))
        .catchError((error) => SearchErrorAction(error)));
}

将Epic连接到Redux Store

现在我们有了一个可以工作的Epic,我们需要将其连接到Redux Store以便它能接收动作流。为此,我们将使用EpicMiddleware

import 'package:redux_epics/redux_epics.dart';
import 'package:redux/redux.dart';

var epicMiddleware = new EpicMiddleware(exampleEpic);
var store = new Store<State>(fakeReducer, middleware: [epicMiddleware]);

组合Epic和普通middleware

要组合Epic Middleware和普通middleware,只需在列表中同时使用两者即可!注意:如果你需要合并两个列表,请确保使用+...扩展运算符。

var store = new Store<AppState>(
  fakeReducer,
  middleware: [myMiddleware] + [EpicMiddleware<AppState>(exampleEpic)],
);

组合Epics

与其拥有一个处理所有可能类型动作的巨大Epic,不如将Epics分解为更小、更易于管理和测试的单元。例如,我们可以拥有searchEpicchatEpicupdateProfileEpic

然而,EpicMiddleware只接受一个Epic。不用担心:redux_epics包括一个用于组合Epics的类!

import 'package:redux_epics/redux_epics.dart';
final epic = combineEpics<State>([
  searchEpic, 
  chatEpic, 
  updateProfileEpic,
]);

高级配方

为了执行更高级的操作,通常有助于使用诸如RxDart这样的库。

类型转换

为了有效使用此库,您通常需要过滤特定类型的动作,如PerformSearchAction。在之前的示例中,您会注意到我们需要使用Stream上的where方法进行过滤,然后稍后手动转换(action as SomeType)。

TypedEpic

第一个选项是使用内置的TypedEpic类。这将允许您编写处理特定类型动作的Epic函数,而不是所有动作!

final epic = new TypedEpic<State, PerformSearchAction>(searchEpic);

Stream<dynamic> searchEpic(
  // 注意:这个Epic只处理PerformSearchActions
  Stream<PerformSearchAction> actions, 
  EpicStore<State> store,
) {
  return actions
    .asyncMap((action) =>
      // 不需要转换action以提取搜索词!
      api.search(action.searchTerm)
        .then((results) => SearchResultsAction(results))
        .catchError((error) => SearchErrorAction(error)));
}

RxDart

您可以使用RxDart提供的whereType方法。它将执行where检查并为您转换动作。

import 'package:redux_epics/redux_epics.dart';
import 'package:rxdart/rxdart.dart';

Stream<dynamic> ofTypeEpic(Stream<dynamic> actions, EpicStore<State> store) {
  // 将我们的actions Stream包装为Observable。这将增强stream的功能。
  return actions
    // 使用`whereType`缩小到PerformSearchAction 
    .whereType<PerformSearchAction>()
    .asyncMap((action) =>
      // 不需要转换action以提取搜索词!
      api.search(action.searchTerm)
        .then((results) => SearchResultsAction(results))
        .catchError((error) => SearchErrorAction(error)));
}

取消

在某些情况下,您可能需要取消异步任务。例如,您的应用程序在用户点击搜索按钮时开始加载数据,通过分发PerformSearchAction启动搜索,然后用户点击返回按钮以纠正搜索词。在这种情况下,应用程序分发了一个CancelSearchAction。我们希望Epic能够在响应于该动作时取消之前的搜索。我们如何实现这一点?

这是Observables真正发光的地方。在以下示例中,我们将使用RxDart库中的Observables,利用switchMaptakeUntil操作符。

import 'package:redux_epics/redux_epics.dart';
import 'package:rxdart/rxdart.dart';

Stream<dynamic> cancelableSearchEpic(
  Stream<dynamic> actions,
  EpicStore<State> store,
) {
  return actions
      .whereType<PerformSearchAction>()
      // 使用SwitchMap。这将确保如果新的PerformSearchAction被分发,
      // 之前的搜索结果将自动被丢弃。
      //
      // 这防止了应用程序显示过期的结果。
      .switchMap((action) {
        return Stream.fromFuture(api.search(action.searchTerm)
            .then((results) => SearchResultsAction(results))
            .catchError((error) => SearchErrorAction(error)))
            // 使用takeUntil。这将在应用程序分发`CancelSearchAction`时取消搜索。
            .takeUntil(actions.whereType<CancelSearchAction>());
  });
}

自动完成使用节流

让我们更进一步!假设我们想将之前的示例转换为自动完成Epic。在这种情况下,每当用户在文本输入框中键入字母时,我们都希望获取并显示搜索结果。每次用户键入字母时,我们将分发一个PerformSearchAction

为了防止发出过多的API调用,这可能会给后端服务器带来不必要的负载,我们不想在每个PerformSearchAction时都发出API调用。相反,我们将在用户暂停打字一小段时间后再调用后端API。

我们将使用RxDart中的debounce操作符来实现这一点。

import 'package:redux_epics/redux_epics.dart';
import 'package:rxdart/rxdart.dart';

Stream<dynamic> autocompleteEpic(
  Stream<dynamic> actions,
  EpicStore<State> store,
) {
  return actions
      .whereType<PerformSearchAction>()
      // 使用debounce将确保我们在用户暂停150毫秒后再发出API调用
      .debounce(new Duration(milliseconds: 150))
      .switchMap((action) {
        return Stream.fromFuture(api.search(action.searchTerm)
                .then((results) => SearchResultsAction(results))
                .catchError((error) => SearchErrorAction(error)))
            .takeUntil(actions.whereType<CancelSearchAction>());
  });
}

依赖注入

依赖项可以通过函数式或面向对象的方式手动注入。如果您愿意,也可以使用依赖注入或服务定位器库。

函数式

// epic_file.dart
Epic<AppState> createEpic(WebService service) {
  return (Stream<dynamic> actions, EpicStore<AppState> store) async* {
    await service.doSomething();
  }
}

面向对象

// epic_file.dart
class MyEpic implements EpicClass<State> {
  final WebService service;

  MyEpic(this.service);

  @override
  Stream<dynamic> call(Stream<dynamic> actions, EpicStore<State> store) {
    return service.doSomething();
  } 
}

生产环境使用

在生产代码中,可以在调用combineEpics的地方创建Epics。如果您使用单独的main_<environment>.dart文件来配置不同环境的应用程序,您可能希望在此时将配置传递给RealWebService

// app_store.dart
import 'package:epic_file.dart';
...

final apiBaseUrl = config.apiBaseUrl

final functionalEpic = createEpic(new RealWebService(apiBaseUrl));
// 或者
final ooEpic = new MyEpic(new RealWebService(apiBaseUrl));

static final epics = combineEpics<AppState>([
    functionalEpic,
    ooEpic,    
    ...
    ]);
static final epicMiddleware = new EpicMiddleware(epics);

测试环境使用

...
final testFunctionalEpic = createEpic(new MockWebService());
// 或者
final testOOEpic = new MyEpic(new MockWebService());
...

完整示例

import 'package:redux/redux.dart';
import 'package:redux_epics/redux_epics.dart';

enum Actions { increment, decrement }

int reducer(int prev, dynamic action) {
  if (action == Actions.increment) {
    return prev + 1;
  } else if (action == Actions.decrement) {
    return prev - 1;
  }

  return prev;
}

// 一个middleware,它将监听increment动作并用decrement动作撤销它!真是个捣蛋鬼!
//
// 我们在这里使用async*函数来简化操作!您也可以返回正常的Streams,或者使用RxDart增强Streams的功能!
Stream<dynamic> jokerEpic(
  Stream<dynamic> actions,
  EpicStore<int> store,
) async* {
  // 使用`await for`关键字在async*函数中监听流!
  await for (var action in actions) {
    // 然后检查是否收到了increment动作
    if (action == Actions.increment) {
      // 如果是,则使用`yield`关键字发出一个decrement动作!
      // 此decrement动作将被自动分发。
      yield Actions.decrement;
    }
  }
}

void main() {
  final store = Store<int>(
    reducer,
    initialState: 0,
    middleware: [EpicMiddleware(jokerEpic)],
  );

  store.onChange.listen(print);

  store.dispatch(Actions.increment);
}

这个完整的示例展示了如何创建一个简单的Redux应用,并使用redux_epics中间件来处理异步逻辑。通过这种方式,您可以更清晰地分离关注点,并使应用程序更具可维护性。


更多关于Flutter中间件处理插件redux_epics的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter中间件处理插件redux_epics的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在处理Flutter应用中的中间件逻辑时,redux_epics 是一个强大的工具,它允许你以响应式的方式处理 Redux 动作流。redux_epics 类似于 Redux 中间件,但它是专为处理副作用(如 API 调用、路由跳转等)而设计的。以下是如何在 Flutter 应用中使用 redux_epics 的一个简单示例。

首先,确保你已经在 pubspec.yaml 文件中添加了必要的依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_redux: ^0.6.0
  redux_epics: ^0.14.0  # 请注意版本号可能会有更新,请查阅pub.dev获取最新版本

然后,你需要设置 Redux store 和 Epic 中间件。以下是一个完整的示例,包括定义动作、reducer、Epic 和将它们组合在一起。

1. 定义动作类型

enum ActionType { increment, decrement, fetchDataSuccess, fetchDataFailure }

2. 定义动作类

class IncrementAction {}
class DecrementAction {}
class FetchDataAction {}
class FetchDataSuccessAction {
  final String data;
  FetchDataSuccessAction(this.data);
}
class FetchDataFailureAction {
  final String error;
  FetchDataFailureAction(this.error);
}

3. 定义动作创建函数

IncrementAction increment() => IncrementAction();
DecrementAction decrement() => DecrementAction();
FetchDataAction fetchData() => FetchDataAction();

4. 定义状态

class AppState {
  final int counter;
  final String? data;
  final String? error;

  AppState({required this.counter, this.data, this.error});

  AppState copyWith({int? counter, String? data, String? error}) {
    return AppState(
      counter: counter ?? this.counter,
      data: data ?? this.data,
      error: error ?? this.error,
    );
  }
}

5. 定义 Reducer

AppState rootReducer(AppState state, dynamic action) {
  if (action is IncrementAction) {
    return state.copyWith(counter: state.counter + 1);
  } else if (action is DecrementAction) {
    return state.copyWith(counter: state.counter - 1);
  } else if (action is FetchDataSuccessAction) {
    return state.copyWith(data: action.data, error: null);
  } else if (action is FetchDataFailureAction) {
    return state.copyWith(data: null, error: action.error);
  }
  return state;
}

6. 定义 Epic

import 'package:redux_epics/redux_epics.dart';
import 'dart:async';

Epic<AppState> fetchDataEpic(Store<AppState> store) {
  return (Stream<dynamic> actions, EpicDispatcher next) async* {
    await for (var action in actions) {
      if (action is FetchDataAction) {
        try {
          // 模拟一个异步数据获取
          await Future.delayed(Duration(seconds: 2));
          var data = "Fetched Data";
          next(FetchDataSuccessAction(data));
        } catch (e) {
          next(FetchDataFailureAction(e.toString()));
        }
      } else {
        yield action;
      }
    }
  };
}

7. 创建 Redux Store 并配置 Epic 中间件

import 'package:redux/redux.dart';
import 'package:redux_epics/redux_epics.dart';

void main() {
  final initialState = AppState(counter: 0);
  final epicMiddleware = createEpicMiddleware<AppState>();

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

  epicMiddleware.run(fetchDataEpic(store));

  // 在 Flutter 应用中使用这个 store
  runApp(MyApp(store: store));
}

8. 在 Flutter 应用中使用 Store

import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';

class MyApp extends StatelessWidget {
  final Store<AppState> store;

  MyApp({required this.store});

  @override
  Widget build(BuildContext context) {
    return StoreProvider<AppState>(
      store: store,
      child: MaterialApp(
        home: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = StoreProvider.of<AppState>(context).state.counter;
    final data = StoreProvider.of<AppState>(context).state.data;
    final error = StoreProvider.of<AppState>(context).state.error;

    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Redux Epics Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$counter',
              style: Theme.of(context).textTheme.headline4,
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => StoreProvider.of<AppState>(context).dispatch(increment()),
              child: Text('Increment'),
            ),
            ElevatedButton(
              onPressed: () => StoreProvider.of<AppState>(context).dispatch(decrement()),
              child: Text('Decrement'),
            ),
            ElevatedButton(
              onPressed: () => StoreProvider.of<AppState>(context).dispatch(fetchData()),
              child: Text('Fetch Data'),
            ),
            if (data != null) Text('Data: $data'),
            if (error != null) Text('Error: $error'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: null,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

这个示例展示了如何使用 redux_epics 来处理异步操作(如数据获取)并将结果分发给 Redux store。注意,实际应用中你可能需要处理更多的边缘情况和错误处理。

回到顶部