最新のアプリケーションのほとんどのコードは、おそらくAndroid 4.0の時代に書き戻されました。 アプリケーションは、ContentProvider、 RoboSpice 、 さまざまなライブラリ 、およびアーキテクチャのアプローチの時代を生き延びました。 したがって、機能の変更だけでなく、新しいトレンド、テクノロジー、ツールにも柔軟に対応できるアーキテクチャを持つことが非常に重要です。
この記事では、IFunnyアプリケーションのアーキテクチャー、私たちが順守している原則、および開発プロセス中に発生する主な問題の解決方法についてお話したいと思います。
開発の基本と考えるポイントから始めましょう。
- チーム内で同じ言語を話す。 新しい開発者はそれぞれ独自のアーキテクチャのビジョンを持ち、既存のコードにエントロピーを導入できます。 個別の独立したアプリケーションコンポーネントを構築するための基本的なパターンが必要です。
- グローバルな抽象化の欠如。 同時に、アプリケーションアーキテクチャの指示ではなく、フレームワークに自分自身を押し込み、各コンポーネントをより便利な方法で実装したくありません。 アーキテクチャは開発者のために機能する必要があり、その逆はできません。
- コンポーネントの再利用:既存のコードをできるだけ簡単に使用する機能。
- 画面回転処理。 アプリケーションの主な問題の1つは、アクティビティ/フラグメントの回転または再作成後の画面の回復です。 これまで、バンドル内のすべてのデータをonSaveInstansState / onRestoreInstanceStateに追加しました 。
- アプリケーションライフサイクルの正しい処理 。
- 単方向データフロー:アプリケーション内でのデータ処理の順序の証拠。
それでは、私たちが何に到達し、各問題をどのように解決したかについて順番に話しましょう。
最初は、アプリケーションの開発時に、アクティビティ/フラグメントがコントローラーとして機能するMVCがありました。 小規模なアプリケーションでは、これは強力な抽象化を必要としない非常に便利なパターンであり、このパターンは元々プラットフォームによって決定されていました。
しかし、時間の経過とともに、アクティビティ/フラグメントは判読不能なサイズに成長します(私たちの記録は、フラグメントの1つにある3000行のコードです)。 新しい機能はそれぞれ、現在のコードの状態に何らかの形で基づいており、これらのクラスにコードを追加し続けることは困難です。
画面全体を独立したコンポーネントに分割する必要があるという結論に達し、このために別のエンティティを特定しました。
public abstract class ViewController<T extends ViewModel, D> { public abstract void attach(ViewModelContainer<T> container, @Nullable D data); public abstract void detach(); }
public interface ViewModelContainer<T extends ViewModel> extends LifecycleOwner { View getView(); T getViewModel(); }
フラグメントは次のようになります。
public class ChatFragment extends TrackedFragmentSubscriber implements ViewModelContainer<ChatViewModel>, IMessengerFragment { @Inject ChatMessagesViewController mChatViewController; @Inject TimeInfoViewController mTimeInfoViewController; @Inject ChatToolbarViewController mChatToolbarViewController; @Inject SendMessageViewController mSendMessageViewController; @Inject MessagesPaginationController mMessagesPaginationController; @Inject ViewModelProvider.Factory mViewModelFactory; @Inject UnreadMessagesViewController mUnreadMessagesViewController; @Inject UploadFileProgressViewController mUploadFileProgressViewController; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.face_to_face_chat, container, false); } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mChatViewController.attach(this); mSendMessageViewController.attach(this); mChatToolbarViewController.attach(this); mMessagesPaginationController.attach(this); mUnreadMessagesViewController.attach(this); mTimeInfoViewController.attach(this); mUploadFileProgressViewController.attach(this); } @Override public void onDestroyView() { mUploadFileProgressViewController.detach(); mTimeInfoViewController.detach(); mUnreadMessagesViewController.detach(); mMessagesPaginationController.detach(); mChatToolbarViewController.detach(); mSendMessageViewController.detach(); mChatViewController.detach(); super.onDestroyView(); } @Override public ChatViewModel getViewModel() { return ViewModelProviders .of(this, mViewModelFactory) .get(ChatViewModel.class); } }
このアプローチには、多くの利点が同時にあります。
- コンポーネントの再利用。
たとえば、検索バーを使用するいくつかの画面があります。
この動作を追加するには、コードに登録するだけです。
@Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mSearchFieldViewController.attach(this); }
または、たとえば、複数選択の可能性がある検索結果ですが、データ型自体、このデータのソース、ナビゲーションと戦略、キャッシュは完全に異なります。 ディスプレイのみが一致します:
- テスト容易性。 単一の画面の動作をテストするためにフラグメント/アクティビティを作成する必要はありません。
- モジュール性。 アプリケーションの個々の部分(UIまたはデータ処理)は、相互に参照することなく開発できます。
- しかし同時に、開発者に制限は追加されず、個々のコンポーネントごとに独自のアーキテクチャアプローチ(MVC、MVI、MVVM、またはその他のMVX)を使用できます。 この抽象化は、Androidのコンポーネントから私たちを分離し、コードを記述するための一般的なスタイルを設定するだけです。
次に、データ構造を整理する必要があります。 画面の状態をどこかに保存し、アクティビティ/フラグメントの再現を体験する必要があります。
バンドル内のデータストレージが私たちに合わない理由:
- 定型コードが多すぎる。
- フラグメントとメソッド呼び出しメソッドのライフサイクルはかなり複雑です。 ビューステートとデータを保存することは明らかではありません。
したがって、アクティビティはビューの状態を復元します。
protected void onRestoreInstanceState(Bundle savedInstanceState) { if (mWindow != null) { Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG); if (windowState != null) { mWindow.restoreHierarchyState(windowState); } } }
また、 RecycleViewアダプターがオーバーライドされたonRestoreInstanceState内で更新された場合、復元されたデフォルトのスクロールはリセットされます。
- すべての重いデータについては、データベース内のストレージを整理する必要があります。そうしないと、TooLargeTransactionExceptionをキャッチできます。
retain fragmentを使用することにしました。つまり、ViewModelの形式でGoogleからの便利なラッパーです。 これらのオブジェクトは、再利用できないフラグメントとしてFragmentManagerに存在します。
仕組み
FragmentManagerは、そのようなオブジェクトをFragmentManagerNonConfigの別のフィールドに保存します。 このオブジェクトは、FragmentManagerの外側のメモリ領域(ActivityClientRecordと呼ばれるオブジェクト)でActivityとFragmentManagerを再作成しても存続します。 このオブジェクトはActivity.onDestroyで形成され、状態をActivity.attachに復元します。 しかし、彼は画面が回転したときにのみ回復することができます。 つまり システムがアクティビティを「釘付け」した場合、何も保存されません 。
各ViewControllerには、状態が配置される独自のViewModelが必要です。 また、データを表示するビューも必要です。 このデータは、アクティビティまたはフラグメントによって実装されるViewModelContainerを介して渡されます。
次に、コンポーネント間のデータと状態のフローを整理する必要があります。 実際、このタスクにはいくつかのオプションを使用できます。 たとえば、ViewControllerとViewModelの間のやり取りにRxを使用するのが良い解決策です。
これらの目的でLiveDataを使用することにしました。
LiveDataはRxの一種のストリームであり、多くのオペレーターはいません(実際にはオペレーターが十分ではないため、LiveDataとRxを並べて使用する必要があります)が、データをキャッシュし、アプリケーションのライフサイクルを処理できます。
一般に、すべてのデータはViewModel内にあります。 この場合、データ処理はその外部で行われます。 ViewControllerは単にイベントをトリガーし、ViewModelのオブザーバーを介してデータを待機します。
ViewModel内には、これらすべての状態をキャッシュする必要なLiveDataオブジェクトがあります。 画面が回転すると、ViewControllerが再作成され、データをサブスクライブし、最後の状態がそれになります。
public class ChatViewModel extends ViewModel { private final MessageRepositoryFacade mMessageRepositoryFacade; private final CurrentChannelProvider mCurrentChannelProvider; private final SendbirdConnectionManager mSendbirdConnectionManager; private final MediatorLiveData<List<MessageModel>> mMessages = new MediatorLiveData<>(); private final MutableLiveData<String> mMessage = new MutableLiveData<>(); @Inject public ChatViewModel(MessageRepositoryFacade messageRepositoryFacade, SendbirdConnectionManager sendbirdConnectionManager, CurrentChannelProvider currentChannelProvider) { mMessageRepositoryFacade = messageRepositoryFacade; mCurrentChannelProvider = currentChannelProvider; mSendbirdConnectionManager = sendbirdConnectionManager; initLiveData(); } public LiveData<List<MessageModel>> getMessages() { return mMessages; } public void writeMessage(String message) { mMessage.postValue(message); } public void sendMessage() { // ... } private void initLiveData() { LiveData<List<MessageModel>> messages = Transformations.switchMap(mCurrentChannelProvider.getCurrentChannel(), input -> { if (!Resource.isDataNotNull(input)) { return AbsentLiveData.create(); } return mMessageRepositoryFacade.getMessagesList(input.data.mUrl); }); mMessages.addSource(messages, mMessages::setValue); mMessages.addSource(mSendbirdConnectionManager.getConnectionStateLiveData(), connectionState -> { if (connectionState == null) { return; } switch (connectionState) { case OPEN: // ... break; case CLOSED: // ... break; } }); } }
ビューを初期化するには、ButterKnifeとViewHolderアプローチを使用して、初期化されたビューの弱点を取り除きます。
各ViewControllerには独自のViewHolderがあり、ViewHolderのデタッチが無効化されると、 attachの呼び出しに初期化されます。 表示内のすべてのフィールドは、後続のものに登録されます。
public class ViewHolder { private final Unbinder mUnbinder; private final View mView; public ViewHolder(View view) { mView = view; mUnbinder = ButterKnife.bind(this, view); } public void unbind() { mUnbinder.unbind(); } public View getView() { return mView; } }
次に、画面のコントローラーについて説明します。
@ActivityScope public class SendMessageViewController extends SimpleViewController<ChatViewModel> { @Nullable private ViewHolder mViewHolder; @Nullable private ChatViewModel mChatViewModel; @Inject public SendMessageViewController() {} @Override public void attach(ViewModelContainer<ChatViewModel> container) { mViewHolder = new ViewHolder(container.getView()); mChatViewModel = container.getViewModel(); mViewHolder.mSendMessageButton.setOnClickListener(v -> mChatViewModel.sendMessage()); mViewHolder.mChatTextEdit.addTextChangedListener(new SimpleTextWatcher() { @Override public void afterTextChanged(Editable s) { mChatViewModel.setMessage(s.toString()); } }); } @Override public void detach() { ViewHolderUtil.unbind(mViewHolder); mChatViewModel = null; mViewHolder = null; } public class ChatViewHolder extends ViewHolder { @BindView(R.id.message_edit_text) EmojiconEditText mChatTextEdit; @BindView(R.id.send_message_button) ImageView mSendMessageButton; @BindView(R.id.message_list) RecyclerView mRecyclerView; @BindView(R.id.send_panel) View mSendPanel; public ViewHolder(View view) { super(view); } } }
@ActivityScope public class ChatMessagesViewController extends SimpleViewController<ChatViewModel> { private final ChatAdapter mChatAdapter; @Nullable private ChatViewModel mChatViewModel; @Nullable private ViewHolder mViewHolder; @Inject public ChatMessagesViewController(ChatAdapter chatAdapter) { mChatAdapter = chatAdapter; } @Override public void attach(ViewModelContainer<ChatViewModel> container) { mChatViewModel = container.getViewModel(); mViewHolder = new ViewHolder(container.getView()); mViewHolder.mRecyclerView.setAdapter(mChatAdapter); mChatViewModel.getMessages().observe(container, data -> mChatAdapter.updateMessages(data)); } @Override public void detach() { ViewHolderUtil.unbind(mViewHolder); mViewHolder = null; mChatViewModel = null; } public class SendMessageViewHolder extends ViewHolder { @BindView(R.id.message_list) RecyclerView mRecyclerView; public ViewHolder(View view) { super(view); LinearLayoutManager linearLayoutManager = new LinearLayoutManager(view.getContext()); linearLayoutManager.setReverseLayout(true); linearLayoutManager.setStackFromEnd(true); mRecyclerView.setLayoutManager(linearLayoutManager); } } }
LiveDataのロジックにより、リストはonStopとonStartの間で更新されません。これは、現時点ではLiveDataが非アクティブであるが、新しいメッセージがプッシュされる可能性があるためです。
これにより、データストレージの実装をカプセル化でき、クラス間の呼び出しの順序も明確になります。 呼び出し順序とはどういう意味ですか?
たとえば、MVPを取り上げます。
PresenterとViewには相互にリンクがあることが理解されます。 Viewは、カスタムイベントをPresenterに転送します。 彼は何らかの形でそれらを処理し、結果を返します。 この相互作用では、データストリームが明確になりません。 両方のオブジェクトは相互に明示的なリンクを持っているため(そして、インターフェースはこの接続を切断せず、少し抽象化するだけです)、呼び出しは両方の方向に進み、Viewが受動的である方法について議論します。 何を転送し、何を処理するかなど。 など また、この点で、多くの場合、プレゼンターのレースを開始します。
この場合、ユーザーデータもデータベースにキャッシュされることは明らかです。 ただし、キャッシュは非同期で行われ、ユーザーの応答はそれを受信した直後にLiveDataに送信されるため、まったく依存しません。
これはすべて、マルチスレッド、ネットワーク呼び出しとどのように友達になりますか?
すべてのネットワーク要求は、アクティビティまたはフラグメントへの参照を持たないクラスのコンテキストから取得され、要求からのデータはグローバルクラスで処理され、これもアプリケーションスコープにあります。 マッピングは、オブザーバーまたは他のリスナーを介してこのデータを受け取ります。 これがLiveDataを介して行われる場合、onPauseとonStart間のマッピングは更新されません。
表示のみに関連する重い操作(データベースからのデータの取得、画像のデコード、ファイルへの書き込み)は、ViewModelコンテキストから行われ、RxまたはLiveDataを介して高速化されます。 ディスプレイを再作成すると、これらの操作の結果はメモリに残り、これによりリークが発生することはありません。
LiveDataとViewModelの短所について話す場合、次の点を強調できます。
- LiveDataは、onStartとonStopの間でのみアクティブです。つまり、onSaveInstanceStateの後に機能します。その後、FragmenManagerとの対話に注意する必要があります。
- LiveDataを操作するためのオペレータが不足しており、Rxがない場合はかなり制限されます。
- ViewModelは、システムがアクティビティを強制終了しなかった場合(アクティビティを保持しない) 、アクティビティの再作成に耐えられません。つまり、重要なデータの一部はLiveDataにのみキャッシュできません。
- ViewModelは、レクリエーションに関連するすべてのネストされたフラグメントの問題を継承します。
おわりに
実際、記事に書かれていることはすべて原始的で明白に見えますが、最もシンプルなアーキテクチャの原則に従うことで、開発者がアプリケーションを作成するときに遭遇する技術的な問題のほとんどを解決できるため、Keep It Simple、Stupidの原則を開発の主要なものの1つと見なします。 そして、MVP、MVC、またはMVVMと呼ばれるものに関係なく、主なことは、なぜそれが必要なのか、どのような問題が解決に役立つのかを理解することです。
https://developer.android.com/topic/libraries/architecture/guide.html
https://en.wikipedia.org/wiki/KISS_principle
https://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html
https://android.jlelse.eu/android-architecture-components-viewmodel-e74faddf5b94
http://hannesdorfmann.com/android/arch-components-purist