もしあなたがモバイル開発者なら、こちらの状況はおそらく常に起こるかもしれません。データをロードしてユーザーに表示することです。それに関して、だいたい二つの注意点があります:

1.データの伝送は時間がかかります。それとも

2. ネットワークに接続できないなどの予期せぬ事態により、データの伝送はエラーになった場合もあります。

下記はRxJavaを使用してデータをロードする際の一般的な状況です。

public void onAttach(View view) {
  view.showLoading(true)
  repository.getData().subscribe({ data ->
    view.showLoading(false)
    view.showData(data)
  }, { error ->
    view.showLoading(false)
    // should we have a retry action?
    view.showError("boo!")
  })
}

データのロードは実にシンプルな作業です。RxJava を使うのもかなり効率的な方法です。しかし、もし下記の状況が起きた場合、私たちはそれを慣用的な方法でモデル化できます。

(例:データが変わり、リロードが必要な場合、データを更新して、伝播の必要な場合)

public void onAttach(View view) {
  repository.getLatestDataStream().subscribe({ data ->
    view.showData(data)
  })
}

いくつかの問題が起こる可能性があります:

1.データがrefreshing/loadingする際、その内容を失う可能性があります。

2.もしエラーが伝播すると、ストリームを止める可能性があります。

この問題を解決するには、このコンテキストを提供するimmutable value type(不変の値型)を作成しましょう。

// Lce -> Loading / Content / Error
class Lce<T> {
  public static <T> Lce<T> data(T data) {
    // implementation
  }
  public static <T> Lce<T> error(Throwable error) {
    // implementation
  }
  public static <T> Lce<T> loading() {
    // implementation
  }
  boolean isLoading();
  boolean hasError();
  Throwable getError();
  T getData();
}

repository signatureを更新しましょう

interface DataRepository {
  Observable<Lce<Data>> getDataEventStream();
}

今から、データが使えます

public void onAttach(View view) {   // Remember to clean the subscription!   repository.getDataEventStream().subscribe({ event ->     if (event.isLoading) {       view.showLoading(true)     } else if (event.hasError()) {       view.showError(event.getError())     } else {       view.showData(event.getData())     }    }) }

Lce Observablesを構成する

これまでは、Observable <Lce <Data >>のスタブ付きバージョンのみを示しました。その理由は、実装の詳細です。それをどのように構築するかは、データ要件に依存します。

Retrofit でAPIを使用した場合の単純な単一リクエストの実装方法

Observable<Lce<Data>> getDataEventStream() {
  return api.getData()
    .map(data -> Lce.data(data))
    .startWith(Lce.loading())
    .onErrorReturn(e -> Lce.error(e))
}

再生可能にしたいなら(リフレッシュできるようにしたい場合)

final PublishRelay<String> refreshRelay = PublishRelay.create()
  
void refreshData() {
  refreshRelay.call("refresh event")
}

Observable<Lce<Data>> getDataEventStream() {
  return refreshRelay
     .startWith("initial")
     .switchMap { event ->
       api.getData()
         .map(data -> Lce.data(data))
         .startWith(Lce.loading())
         .onErrorReturn(e -> Lce.error(e))
     }
}

ビューの実装は、この変更を処理するために多少変更する必要はありません。我々はビュー層から実装の詳細を隠していると同時に、正しい振る舞いを保証するために論理ユニットをテスト可能にします。

Combining(結合)

ビューでは、2つのソースからデータを結合する必要があるとします。これらの観測値を単一のストリームに簡単に合成することができます

class UserContent {
 // requires both the user and list of published content
}

Observable<Lce<User>> getUserEventStream(String userId);
Observable<Lce<List<Content>> getContentEventStream(String userId);

Observable<Lce<UserContent>> getProfileEventStream(String id) {
  return Observable.combineLatest(
    getUserEventStream(id),
    getContentEventStream(id),
    { user, content ->
      if (user.isLoading || content.isLoading) {
        return Lce.loading()
      } else if (user.hasError()) {
        return Lce.error(user.getError()
      } else if (content.hasError()) {
        return Lce.error(content)
      } else {
        return Lce.data(createUserContent(
            user.getData(), content.getData())
      }
    })
}

何らかの要因でユーザーまたはコンテンツの更新を引き起こした場合、このストリームは正しいイベントをビューに伝えます。したがって、ユーザーまたはコンテンツのいずれかが読み込まれている場合、ビューにその内容が表示されます。

Real world use case(実世界でのユースケース)

Instacartの消費者アプリで、後続のAPI呼び出しを行う前に、ユーザーデータを利用可能にする必要があります。

class UserManager {
  Observable<Lce<User>> getUserEventStream() {
   return updateEventStream().startWith(cachedBundle().map(toLce())) 
  }
}

store chooserを展示するために店のリストが必要で。そして店の分類方法はその店の郵便番号です。もし郵便番号が変わると、私たちも店関連の情報も更新しなければなりません。

class StoreRepository {
  Observable<Lce<List<Store>> getStoreEventStream() {
    return userBundleManager.switchMap { bundleEvent ->
      if (bundleEvent.isLoading()) {
        return Observable.just(Lce.loading())
      } else if (bundleEvent.hasError()) {
        return Observable.just(Lce.error(bundleEvent.getError()))
      } else {
       // Stores can come from cached source or network
       return storeListEventStream(bundleEvent.data.getZipCode())
      }
   }
}

結論

プログレスバーやエラーの状態を表示するときの条件は、しばしば面倒で複雑になります。showProgress(true/false)これは私の解決方法でした。これにより、小さな変更があるエッジケースでローディング条件を簡単に破る可能性がある場合、コードの追加と変更が非常に複雑になりました。このパターンは、ロード/コンテンツ/エラーロジックをテストできるようにしながら、このような状況を回避するのに役立ちます。

タイトル:LCE: Modeling Data Loading in RxJava

作者:Laimonas Turauskas

原文URL:https://tech.instacart.com/lce-modeling-data-loading-in-rxjava-b798ac98d80

今すぐシェアしよう!
今すぐシェアしよう!