標準アクティビティおよびAsyncTaskからRxJavaを使用した最新のMVPアーキテクチャへの旅。
プロジェクトコードは、相互に機能するメカニズムとして機能する独立したモジュールに分割する必要があります( Chester Alvarezの写真)。
Android用開発ツールのエコシステムは非常に高速に開発されています。 毎週、誰かが新しいツールを作成し、既存のライブラリを更新し、新しい記事を書き、プレゼンテーションを行います。 1か月間休暇を取り、帰国するまでに、サポートライブラリおよび/またはGoogle Play開発者サービスの最新バージョンが既に公開されています。
過去3年間、 リボット Androidアプリケーションを開発してきましたが、アプリケーションのアーキテクチャと使用するテクノロジーの両方が常に進化し、改善されてきました。 この記事では、私たちが学んだ道、私たちが学んだ教訓、私たちが犯した間違い、およびこれらすべてのアーキテクチャの変更につながった推論を示します。
古き良き時代
2012年、プロジェクトの構造は非常にシンプルに見えました。 ネットワークライブラリはありませんでしたが、
AsyncTask
は引き続き友人でした。 以下の図は、これらのソリューションのアーキテクチャの例を示しています。
コードは2つのレベルに分割されました。RESTAPIとさまざまなローカルリポジトリの両方を介して受信したデータの受信/保存を担当するデータレイヤーと、データの処理と表示を担当するビューレイヤーです。
APIProvider
は、アクティビティとフラグメントがREST APIと対話できるようにするメソッドを提供します。 これらのメソッドは、
URLConnection
と
AsyncTask
を使用してバックグラウンドスレッドで要求を実行し、コールバック関数を介して結果をアクティビティに配信します。
CacheProvider
も同じ
CacheProvider
機能します
CacheProvider
またはSQLiteからデータを取得するメソッドがあり、結果を返すコールバック関数があります。
問題
このアプローチの主な問題は、プレゼンテーションのレベルの責任が大きすぎることです。 アプリケーションがブログ投稿のリストをダウンロードし、それらをSQLiteにキャッシュしてから
ListView
表示する必要がある簡単なシナリオを想像してみましょう。
Activity
は次のことを行う必要があります。
-
APIProvider#loadPosts(Callback)
メソッドを呼び出します。 - 渡された
Callback
でonSuccess()
メソッドがonSuccess()
を待ってから、onSuccess()
CacheProvider#savePosts(Callback)
呼び出します。 - 渡された
Callback
でonSuccess()
メソッドがonSuccess()
を待ってから、データをListView
表示します。 -
APIProvider
とCacheProvider
両方で発生する可能性のある2つのエラーを個別に処理します。
そして、これは別の簡単な例です。 実際には、APIが、プレゼンテーションレベルが期待する形式ではないデータを返す場合があります。つまり、
Activity
は、データを操作する前に何らかの方法でデータを変換および/またはフィルター処理する必要があります。 または、たとえば、
loadPosts()
はどこかから受け取る必要がある引数(たとえば、Play Services SDKを介して要求するメールアドレス
loadPosts()
を取ります。 確かに、SDKはコールバック関数を介して非同期的にアドレスを返します。これは、コールバック関数の3つのレベルのネストがあることを意味します。 ますます複雑になっていくと、コールバック地獄と呼ばれるものになってしまいます。
要約:
- アクティビティとフラグメントは、健全すぎて維持が困難になります。
- ネストレベルが多すぎると、コードがくなり、理解しにくくなり、新しい機能の追加や変更が困難になります。
- ユニットテストはより困難です(まったく不可能にならない場合)。これは、ユニットテストにあまり役立たない多くのロジックがアクティビティまたはフラグメントにあるためです。
RxJavaを使用した新しいアーキテクチャ
上記のアプローチを2年間使用しました。 この間に、私たちはいくつかの変更を行い、説明した問題による痛みと苦痛を軽減しました。 たとえば、いくつかの補助クラスを追加し、それらのロジックの一部を取り出して
APIProvider
とフラグメントをアンロードし、
APIProvider
Volleyの使用を開始しました。 これらの変更にもかかわらず、コードのテストは依然として困難であり、コールバックヘルがときどき発生しました。
RxJavaに関するいくつかの記事を読んだ2014年に状況は変わり始めました。 いくつかの試用プロジェクトで試してみたところ、ネストされたコールバック関数の問題の解決策が見つかったようです。 リアクティブプログラミングに慣れていない場合は、 この概要を読むことをお勧めします。 要するに、RxJavaでは、非同期ストリーム(データの管理者:この場合、ストリームのようなストリームを意味し、スレッド-実行スレッドと混同しないでください)を使用して、変換するストリームに適用できる多くの演算子を提供します。必要に応じてデータをフィルタリングまたは結合します。
過去2年間に蓄積したすべてのバンプを考慮して、新しいアプリケーションのアーキテクチャを検討し始め、次のことに気付きました。
コードはまだ2つのレベルに分かれています。データレイヤーには
DataManager
と一連のヘルパークラスが含まれ、プレゼンテーションレイヤーには
Activity
、
Fragment
、
ViewGroup
などのAndroid SDKクラスが含まれます。
ヘルパークラス(図の3列目)の責任範囲は非常に限定されており、一貫した方法で実装されています。 たとえば、ほとんどのプロジェクトには、REST APIにアクセスしたり、データベースからデータを読み込んだり、サードパーティのSDKとやり取りしたりするためのクラスがあります。 アプリケーションごとにヘルパークラスのセットが異なりますが、最も一般的に使用されるのは次のとおりです。
-
PreferencesHelper
:SharedPreferences
データを処理します。 -
DatabaseHelper
:SQLiteで動作します。 - REST APIを呼び出すレトロフィットサービス。 Volleyの代わりにRetrofitを使用し始めました。これは、RxJavaでの作業をサポートするためです。 はい、彼のAPIはより快適です。
多くのパブリックヘルパークラスメソッドは、RxJava
Observables
返します。
DataManager
は、新しいアーキテクチャの中心部分です。 RxJavaオペレーターを広範囲に使用して、ヘルパーから受け取ったデータを結合、フィルター処理、変換します。
DataManager
のタスクは、アクティビティとフラグメントをデータの「コーミング」作業から解放することです。
DataManager
は、必要なすべての変換を実行し、表示可能なデータを提供します。
以下のコードは、
DataManager
メソッドがどのように見えるかを示しています。 次のように機能します。
- Retrofitを介して投稿のリストを読み込みます。
-
DatabaseHelper
を介してローカルデータベースにデータをキャッシュします。 - 投稿をフィルターし、今日公開されたものを選択します。これは、プレゼンテーションレベルでのみ表示されるためです。
public Observable<Post> loadTodayPosts() { return mRetrofitService.loadPosts() .concatMap(new Func1<List<Post>, Observable<Post>>() { @Override public Observable<Post> call(List<Post> apiPosts) { return mDatabaseHelper.savePosts(apiPosts); } }) .filter(new Func1<Post, Boolean>() { @Override public Boolean call(Post post) { return isToday(post.date); } }); }
プレゼンテーションレベルのコンポーネントは、このメソッドを呼び出し、それによって返される
Observable
をサブスクライブするだけです。 サブスクリプションが完了すると、受信した
Observable
によって返された投稿を
Adapter
に追加して、
RecyclerView
または同様の表示を行うことができます。
このアーキテクチャの最後の要素はイベントバスです。 イベントバスを使用すると、データレベルで発生する特定のイベントに関するメッセージを実行でき、プレゼンテーションレベルのコンポーネントはこれらのメッセージをサブスクライブできます。 たとえば、
DataManager
の
signOut()
メソッドは、対応する
Observable
作業
Observable
完了したことを通知するメッセージをトリガーし、このイベントにサブスクライブしたアクティビティは、ユーザーがログアウトしていることを示すためにインターフェースを再描画できます。
このアプローチはどのように優れていますか?
- RxJavaの
Observables
と演算子は、ネストされたコールバック関数から私たちを救います。
-
DataManager
は、プレゼンテーションレベルで以前に実行された作業を引き受けるため、アクティビティとフラグメントをアンロードします。 -
DataManager
およびヘルパークラスのコードの一部を移動すると、アクティビティとフラグメントのユニットテストが簡単になります。 - 責任の明確な分離と、データ層との対話の唯一のポイントとしての
DataManager
の分離により、アーキテクチャ全体がよりテストフレンドリーになります。 ヘルパークラス、またはDataManager
、特別なスタブに簡単に置き換えることができます。
どのような問題が残っていましたか?
- 大規模で複雑なプロジェクトでは、
DataManager
が肥大化する可能性があり、そのサポートははるかに困難になります。 - プレゼンテーションレベルのコンポーネント(アクティベーションやフラグメントなど)をより軽量にしましたが、RxJavaサブスクリプション管理、エラー分析などを中心に展開するかなりの量のロジックが含まれています。
モデルビュープレゼンターを試す
過去1年間で、 MVPまたはMVVMとして、個々のアーキテクチャパターンがAndroidコミュニティで人気を集め始めています。 別の記事と同様に、 テストプロジェクトでこれらのパターンを調べた結果、MVPがプロジェクトのアーキテクチャに大幅な変更を加えることができることがわかりました。 すでにコードを2つのレベル(データとプレゼンテーション)に分割しているため、MVPの導入は自然に見えました。 新しいレベルのプレゼンターを追加し、コードの一部をビューからそこに転送する必要がありました。
データレベルは変更されませんが、MVPの対応するレベルの名前と一致するモデルと呼ばれるようになりました。
プレゼンターは、モデルからデータをロードし、データがロードされたときにプレゼンテーションレベルで適切なメソッドを呼び出す責任があります。 プレゼンターは、
DataManager
によって返される
Observables
にサブスクライブします。 したがって、 サブスクリプションやスケジューラなどのエンティティを使用する必要があります。 さらに、発生したエラーを分析したり、必要に応じて追加の演算子をデータストリームに適用したりできます。 たとえば、一部のデータを除外する必要があり、このフィルターが他のどこでも使用されない可能性が高い場合、このフィルターを
DataManager
ではなくプレゼンターレベルに移動することは理にかなっています。
以下は、プレゼンターレベルのメソッドの1つです。 前のセクションで定義した
dataManager.loadTodayPosts()
メソッドによって返される
Observable
へのサブスクリプションがあります。
public void loadTodayPosts() { mMvpView.showProgressIndicator(true); mSubscription = mDataManager.loadTodayPosts().toList() .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(new Subscriber<List<Post>>() { @Override public void onCompleted() { mMvpView.showProgressIndicator(false); } @Override public void onError(Throwable e) { mMvpView.showProgressIndicator(false); mMvpView.showError(); } @Override public void onNext(List<Post> postsList) { mMvpView.showPosts(postsList); } }); }
mMvpView
は、
mMvpView
が使用する
mMvpView
レベルのコンポーネントです。 通常、
Activity
、
Fragment
または
ViewGroup
ます。
前のアーキテクチャと同様に、プレゼンテーションレイヤーにはAndroid SDKの標準コンポーネントが含まれています。 違いは、これらのコンポーネントが
Observables
直接サブスクライブしないことです。 代わりに、
MvpView
インターフェースを実装し、
showError()
や
showProgressIndicator()
などの明確で理解可能なメソッドのリストを提供します。 プレゼンテーションレベルのコンポーネントは、ユーザーの操作(クリックイベントなど)を処理し、プレゼンターで適切なメソッドを呼び出す役割も担います。 たとえば、投稿のリストを読み込むボタンがある場合、
Activity
は
OnClickListener
presenter.loadTodayPosts()
メソッドを
OnClickListener
ます。
実際の例をご覧になりたい場合は、Githubのリポジトリをご覧ください 。 さらに、必要な場合は、 アーキテクチャの構築に関する推奨事項をご覧ください。
このアプローチはどのように優れていますか?
- アクティビティとフラグメントは、ユーザーインターフェイスのレンダリング/更新とユーザーインタラクションイベントの処理に集約されるため、さらに軽量になります。 したがって、保守がさらに容易になります。
- プレゼンター向けのユニットテストの記述は非常に簡単です。プレゼンテーションレベルをロックするだけです。 以前は、このコードはプレゼンテーション層の一部であり、ユニットテストはできませんでした。 アーキテクチャはさらにテストしやすくなっています。
-
DataManager
が肥大化しすぎた場合は、いつでもコードの一部をプレゼンターに移動できます。
どのような問題が残っていましたか?
- 大量のコードを使用しても、
DataManager
は肥大化する可能性があります。 これまでのところ、これは発生していませんが、イベントのそのような発展を放棄しません。
私が説明したアプローチは理想的ではないことに言及することが重要です。 一般的に、あなたのすべての問題をきっぱりと解決する非常にユニークでユニークなアーキテクチャがどこかにあると信じるのは単純です。 Androidエコシステムは今後も高速で進化し続けるため、イベント、研究、読書、実験に遅れずについていく必要があります。 なんで? 素晴らしいAndroidアプリを作り続けるために。
私の記事を楽しんで、それが役に立つことを願っています。 その場合は、[推奨]ボタンをクリックすることを忘れないでください(翻訳者:元の記事に移動し、記事の最後にあるハートボタンをクリックしてください)。 また、現在のアプローチについてのご意見をお聞かせください。