搜尋套件、儲存庫與 APP 分類

套件詳情v0.15.2

redux_epics

一個使用 Stream 執行副作用的 redux.dart 中間件

AndroidiOSWebmacOSWindowsLinux
近 30 天下載
1.4萬
喜歡數
38
Pub 分數
160

安裝套件

flutter pub add redux_epics

套件介紹

根據套件公開資料產生的選型評估,僅供參考。

redux_epics 是一個用於 redux.dart 狀態管理庫的中間件,透過反應式串流來處理副作用。它允許開發者使用 Dart 的 Stream API 以宣告式且可測試的方式管理非同步操作(例如 API 呼叫或導航)。透過與 Redux 的 dispatch 機制整合,確保副作用能在狀態管理流程中乾淨地協調。此套件特別適用於需要透過外部互動實現可預測狀態轉換的複雜應用。

適用情境

  • 處理 HTTP 請求
  • 管理導航流程
  • 異步處理使用者事件
  • 實現即時資料更新
  • 與外部服務同步狀態

主要特色

  • 基於流的副作用處理
  • 與 redux.dart 的整合
  • 宣告式效果組合
  • 可測試且可重用的史詩
  • 支援錯誤處理和重試

適合對象

  • 複雜的狀態管理需求
  • 具有大量異步邏輯的應用程式
  • 偏好函數式響應模式的團隊
  • 使用 redux.dart 的項目已存在
  • 尋求清晰關注點分離的開發者

使用建議

  • 需要理解 Streams 和 Rx
  • 為簡單應用增加複雜性
  • 需要仔細設計錯誤處理
  • 與其他替代方案相比,文件較少
  • 初學者的學習曲線更陡峭

原始 README

以下為關聯原始碼儲存庫的 README 原文快照,最新內容請造訪原始碼儲存庫。

Redux Epics

CI Checks

Redux is great for synchronous updates to a store in response to actions. However, working with complex asynchronous operations, such as autocomplete search experiences, can be a bit tricky with traditional middleware. This is where Epics come in!

The best part: Epics are based on Dart Streams. This makes routine tasks easy, and complex tasks such as asynchronous error handling, cancellation, and debouncing a breeze.

Note: For users unfamiliar with Streams, simple async cases are easier to handle with a normal Middleware Function. If normal Middleware Functions or Thunks work for you, you're doing it right! When you find yourself dealing with more complex scenarios, such as writing an Autocomplete UI, check out the Recipes below to see how Streams / Epics can make your life easier.

Example

Let's say your app has a search box. When a user submits a search term, you dispatch a PerformSearchAction which contains the term. In order to actually listen for the PerformSearchAction and make a network request for the results, we can create an Epic!

In this instance, our Epic will need to filter all incoming actions it receives to only the Action it is interested in: the PerformSearchAction. This will be done using the where method on Streams. Then, we need to make a network request with the search term using asyncMap method. Finally, we need to transform those results into an action that contains the search results. If an error has occurred, we'll want to return an error action so our app can respond accordingly.

Here's what the above description looks like in code.

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) => 
      // Pseudo api that returns a Future of SearchResults
      api.search((action as PerformSearch).searchTerm)
        .then((results) => SearchResultsAction(results))
        .catchError((error) => SearchErrorAction(error)));
}
Connecting the Epic to the Redux Store

Now that we've got an epic to work with, we need to wire it up to our Redux store so it can receive a stream of actions. In order to do this, we'll employ the 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]);
Combining epics and normal middleware

To combine the epic Middleware and normal middleware, simply use both in the list! Note: You may need to provide

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

If you're combining two Lists, please make sure to use the + or the ... spread operator.

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

Combining Epics

Rather than having one massive Epic that handles every possible type of action, it's best to break Epics down into smaller, more manageable and testable units. This way we could have a searchEpic, a chatEpic, and an updateProfileEpic, for example.

However, the EpicMiddleware accepts only one Epic. So what are we to do? Fear not: redux_epics includes class for combining Epics together!

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

Advanced Recipes

In order to perform more advanced operations, it's often helpful to use a library such as RxDart.

Casting

In order to use this library effectively, you generally need filter down to actions of a certain type, such as PerformSearchAction. In the previous examples, you'll noticed that we need to filter using the where method on the Stream, and then manually cast (action as SomeType) later on.

To more conveniently narrow down actions to those of a certain type, you have two options:

TypedEpic

The first option is to use the built-in TypedEpic class. This will allow you to write Epic functions that handle actions of a specific type, rather than all actions!

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

Stream<dynamic> searchEpic(
  // Note: This epic only handles PerformSearchActions
  Stream<PerformSearchAction> actions, 
  EpicStore<State> store,
) {
  return actions
    .asyncMap((action) =>
      // No need to cast the action to extract the search term!
      api.search(action.searchTerm)
        .then((results) => SearchResultsAction(results))
        .catchError((error) => SearchErrorAction(error)));
}
RxDart

You can use the whereType method provided by RxDart. It will both perform a where check and then cast the action for you.

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

Stream<dynamic> ofTypeEpic(Stream<dynamic> actions, EpicStore<State> store) {
  // Wrap our actions Stream as an Observable. This will enhance the stream with
  // a bit of extra functionality.
  return actions
    // Use `whereType` to narrow down to PerformSearchAction 
    .whereType<PerformSearchAction>()
    .asyncMap((action) =>
      // No need to cast the action to extract the search term!
      api.search(action.searchTerm)
        .then((results) => SearchResultsAction(results))
        .catchError((error) => SearchErrorAction(error)));
}
Cancellation

In certain cases, you may need to cancel an asynchronous task. For example, your app begins loading data in response to a user clicking on a the search button by dispatching a PerformSearchAction, and then the user hit's the back button in order to correct the search term. In that case, your app dispatches a CancelSearchAction. We want our Epic to cancel the previous search in response to the action. So how can we accomplish this?

This is where Observables really shine. In the following example, we'll employ Observables from the RxDart library to beef up the power of streams a bit, using the switchMap and takeUntil operator.

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>()
      // Use SwitchMap. This will ensure if a new PerformSearchAction
      // is dispatched, the previous searchResults will be automatically 
      // discarded.
      //
      // This prevents your app from showing stale results.
      .switchMap((action) {
        return Stream.fromFuture(api.search(action.searchTerm)
            .then((results) => SearchResultsAction(results))
            .catchError((error) => SearchErrorAction(error)))
            // Use takeUntil. This will cancel the search in response to our
            // app dispatching a `CancelSearchAction`.
            .takeUntil(actions.whereType<CancelSearchAction>());
  });
}
Autocomplete using debounce

Let's take this one step further! Say we want to turn our previous example into an Autocomplete Epic. In this case, every time the user types a letter into the Text Input, we want to fetch and show the search results. Each time the user types a letter, we'll dispatch a PerformSearchAction.

In order to prevent making too many API calls, which can cause unnecessary load on your backend servers, we don't want to make an API call on every single PerformSearchAction. Instead, we'll wait until the user pauses typing for a short time before calling the backend API.

We'll achieve this using the debounce operator from RxDart.

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>()
      // Using debounce will ensure we wait for the user to pause for 
      // 150 milliseconds before making the API call
      .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>());
  });
}

Dependency Injection

Dependencies can be injected manually with either a Functional or an Object-Oriented style. If you choose, you may use a Dependency Injection or Service locator library as well.

Functional
// epic_file.dart
Epic<AppState> createEpic(WebService service) {
  return (Stream<dynamic> actions, EpicStore<AppState> store) async* {
    service.doSomething()...
  }
}
OO
// epic_file.dart
class MyEpic implements EpicClass<State> {
  final WebService service;

  MyEpic(this.service);

  @override
  Stream<dynamic> call(Stream<dynamic> actions, EpicStore<State> store) {
    service.doSomething()...
  } 
}
Usage - Production

In production code the epics can be created at the point where combineEpics is called. If you're using separate main_<environment>.dart files to configure your application for different environments you may want to pass the config to the RealWebService at this point.

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

final apiBaseUrl = config.apiBaseUrl

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

static final epics = combineEpics<AppState>([
    functionalEpic,
    ooEpic,    
    ...
    ]);
static final epicMiddleware = new EpicMiddleware(epics);
Usage - Testing
...
final testFunctionalEpic = createEpic(new MockWebService());
// or
final testOOEpic = new MyEpic(new MockWebService());
...

所屬分類

依賴

依賴清單 2 項

原始碼儲存庫

使用此套件的專案