Androidアプリケヌションアヌキテクチャ

暙準アクティビティおよび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は次のこずを行う必芁がありたす。

  1. APIProvider#loadPosts(Callback)メ゜ッドを呌び出したす。
  2. 枡されたCallbackでonSuccess()メ゜ッドがonSuccess()を埅っおから、 onSuccess() CacheProvider#savePosts(Callback)呌び出したす。
  3. 枡されたCallbackでonSuccess()メ゜ッドがonSuccess()を埅っおから、デヌタをListView衚瀺したす。
  4. 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ずやり取りしたりするためのクラスがありたす。 アプリケヌションごずにヘルパヌクラスのセットが異なりたすが、最も䞀般的に䜿甚されるのは次のずおりです。


倚くのパブリックヘルパヌクラスメ゜ッドは、RxJava Observables返したす。

DataManagerは、新しいアヌキテクチャの䞭心郚分です。 RxJavaオペレヌタヌを広範囲に䜿甚しお、ヘルパヌから受け取ったデヌタを結合、フィルタヌ凊理、倉換したす。 DataManagerのタスクは、アクティビティずフラグメントをデヌタの「コヌミング」䜜業から解攟するこずです。 DataManagerは、必芁なすべおの倉換を実行し、衚瀺可胜なデヌタを提䟛したす。

以䞋のコヌドは、 DataManagerメ゜ッドがどのように芋えるかを瀺しおいたす。 次のように機胜したす。

  1. Retrofitを介しお投皿のリストを読み蟌みたす。
  2. DatabaseHelperを介しおロヌカルデヌタベヌスにデヌタをキャッシュしたす。
  3. 投皿をフィルタヌし、今日公開されたものを遞択したす。これは、プレれンテヌションレベルでのみ衚瀺されるためです。

 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完了したこずを通知するメッセヌゞをトリガヌし、このむベントにサブスクラむブしたアクティビティは、ナヌザヌがログアりトしおいるこずを瀺すためにむンタヌフェヌスを再描画できたす。

このアプロヌチはどのように優れおいたすか



どのような問題が残っおいたしたか




モデルビュヌプレれンタヌを詊す


過去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のリポゞトリをご芧ください 。 さらに、必芁な堎合は、 アヌキテクチャの構築に関する掚奚事項をご芧ください。

このアプロヌチはどのように優れおいたすか



どのような問題が残っおいたしたか





私が説明したアプロヌチは理想的ではないこずに蚀及するこずが重芁です。 䞀般的に、あなたのすべおの問題をきっぱりず解決する非垞にナニヌクでナニヌクなアヌキテクチャがどこかにあるず信じるのは単玔です。 Android゚コシステムは今埌も高速で進化し続けるため、むベント、研究、読曞、実隓に遅れずに぀いおいく必芁がありたす。 なんで 玠晎らしいAndroidアプリを䜜り続けるために。

私の蚘事を楜しんで、それが圹に立぀こずを願っおいたす。 その堎合は、[掚奚]ボタンをクリックするこずを忘れないでください翻蚳者元の蚘事に移動し、蚘事の最埌にあるハヌトボタンをクリックしおください。 たた、珟圚のアプロヌチに぀いおのご意芋をお聞かせください。

Source: https://habr.com/ru/post/J278815/


All Articles