Flutter中间件处理插件redux_epics的使用
Flutter中间件处理插件redux_epics的使用
Redux Epics
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分解为更小、更易于管理和测试的单元。例如,我们可以拥有searchEpic
、chatEpic
和updateProfileEpic
。
然而,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,利用switchMap
和takeUntil
操作符。
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
更多关于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。注意,实际应用中你可能需要处理更多的边缘情况和错误处理。