実世界のAndroid、MVVM、およびリポジトリ

投稿日:

(Mike DeMaso)(2020年11月2日)

コードをクリーン、正確、読みやすく、保守しやすい状態に保ちながら、応答性の高いユーザーインターフェイスを実現するにはどうすればよいですか?これは、2008年以来Android開発者の頭の中で跳ね回っている質問です。

Android開発者に自分で理解させてきた長年の経験を経て、Googleは最近、このトピックに関するガイダンスを MVVM のバリアントを宣伝するアプリアーキテクチャのガイド。これは素晴らしいスタートですが、チームが自分で答えるには多くの質問が残ります。 Googleは、 LiveData Room DataBinding は、定型文を減らし、コードの懸念を分離するのに役立ちます。サードパーティでさえ、 RxJava レトロフィットを手伝ってくれました。非同期作業を処理し、ネットワークを介して物事をフェッチする方法。これらすべてが大きな問題の小さな部分を解決するために浮かんでいるので、実装と保守が簡単な流動的なUIエクスペリエンスを提供するために、どのようにそれらをすべてまとめることができますか?

この記事では、共有しますMVVMアーキテクチャを作成する以前の試みから学んだことと、パズルのこれらすべての部分をまとめる方法の簡単な例を示します。

MVVMの簡単な説明

リポジトリが何を解決するのかを理解するために、最初に基本的なMVVMパターンについて詳しく見ていきましょう。

基本MVVMパターン
基本的なMVVM図

ビュー

これは、のグラフィカルユーザーインターフェイスの表現です。あなたのコード。これらは、XML(レイアウトファイル)またはコード(Jetpack Compose)で表すことができます。通常、ViewをViewModelにリンクする何らかの形式のデータバインディングがあります。ビューとバインダーの間に線を引く場所に応じて、Activity andFragmentオブジェクトはどちらかまたは両方と見なすことができます。

ViewModel

これは、からのデータの変換を担当します。ビューが表示するのに意味のある形式にモデルを作成します。 ViewとViewModelの間の二重矢印で終わる接続にだまされてはいけません。 MVPのViewModelとPresenterの主な違いは、ViewModelにはビューへの参照が含まれていないことです。これは一方向の関係であり、他の何かがViewModelオブジェクトを管理する必要があることを意味します。

モデル

これは、ViewModelがビューに提供する情報のソースとして使用されているデータ(またはドメインモデル)を指します。このレイヤーは、一部の記事で言及されているように、物事が少しぼやける場所です。それをデータ自体と呼び、他の人はそれをデータアクセスレイヤーと呼びます。これは、Googleのリポジトリパターンが問題を解決するために介入する場所です。

リポジトリに入る

Googleはしばらくの間このパターンについて言及します。これらの例は、リポジトリでMVVMを使用する基本原則を理解するための優れたガイドですが、いくつかの小さなものが欠けていることがわかります(重要な)これらのスニペットをより大きく、より複雑なプロジェクトに変換するのに役立つガイダンス。

GoogleのMVVM図

リポジトリパターンは、「クリーンなAPIを提供するように設計されています。アプリの残りの部分でこのデータを簡単に取得できること。」残念ながら、アーキテクチャにリポジトリを追加するだけでは、コードをクリーンにする必要はありません。レイヤーを適切に分離して明確な契約を結ぶことなく、絡み合った混乱を作り出すことができます。

DraftKingsでは、開発者がクリーンなコードを一貫して作成できるように、いくつかの追加ガイドラインに焦点を当てています。

レイヤーをインターフェースで分離する

ここでは、エンジニアに公開されているものについて考えるよう促すためのインターフェースを追加しています
インターフェースを追加してGoogleの図を改善する

これらのレイヤー間でインターフェースを使用すると、すべてのレベルのエンジニアが優れたコーディング原則を守るのに役立つことを確認しました。これにより、単体テストが本当に一度に1つのレイヤーのみをテストしていることを確認し、大規模なテストスイートの作成と保守のオーバーヘッドを削減できます。また、外部向けAPIを明確に定義し、さまざまなレイヤーの実装の詳細をわかりにくくするのに役立ちます。これにより、このオブジェクトの機能について外の世界に伝えていることを評価するように促され、コードがクリーンであることを確認するための組み込みの機会が得られます。

また、コードベース内のさまざまなレイヤーをより効果的にリファクタリングする機能も提供します。インターフェイスが変更されない限り、それらを使用するコードベースの領域はそのままにしておくことができます。たとえば、ネットワークライブラリをVolleyからRetrofitに移行する場合は、Daggerクラスのメソッドを変更するだけで済みます。リモートデータソースの上にインターフェースを作成して提供し、そのエンドポイントを使用するすべてのリポジトリに変更を加える必要はありません。これにより、そのような変更の範囲が大幅に縮小され、最終製品にバグが発生する可能性が低くなります。

ここに、ViewModelをリポジトリの具象クラスに保持させると、意図しない動作が発生する可能性がある例を示します。この例は少し工夫が凝らされており、fooPublishSubjectFooRepositoryprivateですが、そのソリューションはより脆弱です。FooRepositoryはそのパラメータへのアクセスを必要とする別のスコープで使用する必要があり、インスタンスへのアクセスを開くと、混乱します。そのメンバー変数を直接使用するのが適切です。

これらの依存関係の配信

プロジェクトの複雑さが増すにつれて、依存関係はより複雑になります。つまり、一般的に、人々はある種の依存性注入ライブラリ( Dagger Koin など)を利用します。 。

DIライブラリは、必要な依存関係を取得するためのクリーンで簡単な方法を提供するだけでなく、アプリケーションで必要となるこれらのオブジェクトの数を考えることもできます。

この思考プロセスにより、短剣グラフに属するオブジェクトのベストプラクティスを確立することができました。単一のインスタンスのみが必要なものはすべて、ルート/グローバル/アプリケーションスコープのグラフに存在する必要があります。多くのインスタンスが存在する可能性があるものはすべて、オンデマンドで作成し、適切に保持する必要があります。

これは、複数のViewModelがモデルにアクセスできるようにするため、新しいリポジトリオブジェクトがDaggerグラフに属することを意味します。基になるRoomまたはRetrofitソースの単一インスタンスを介して。一方、ViewModelは、それらを必要とするビューごとに新しく作成する必要があります。トランプのスタックのようなアクティビティのスタックを考えてみてください。ViewModelがスーツと価値を推進します。 3つのクラブを上部に追加して、下のすべてのカードを3つのクラブに変更するという行為は望ましくないため、各ビューには、データを保持および分離するためのViewModelの独自のインスタンスが必要です。

これで、DIソリューションが保持するものを定義しました。
保持されると予想されるオブジェクトを表示します。ダガーグラフ

ビューモデルをダガーグラフから除外することにしました。これまで、この選択についてはあまり明確ではありませんでしたが、これがandroidx.lifecycleに含まれる ViewModelProvider パターンと、それがアクティビティ/フラグメント間の関係を強化するのにどのように役立つかを考えると、正しい方向/ XMLとViewModelを「1対1」として。一方、ViewModelとリポジトリの関係は「多対多」にすることができます。実際には、これは、すべてのアクティビティ/フラグメント/ XMLに対して、そのビューのlを処理する単一のViewModelがあることを意味します。 ogicですが、必要なデータを入手するために多くのリポジトリにアクセスできます。データは通常、アプリケーション全体で再利用および表示されるため、多くの異なるViewModelが、DaggerGraphのリポジトリの同じインスタンスを簡単かつ効率的に使用できます。

APIを含む

どの企業でも大規模な場合、ホワイトボードから顧客の手にプロジェクトを届けるには、多くのエンジニア、チーム、さらには部門が必要です。ここドラフトキングスでは、それも違いはありません。 Androidアプリケーションにデータを取り込むには、バックエンドのいくつかの異なるチームと協力して、データベースからAPI、Androidクライアントにデータを取り込む必要があります。このコードは別のチームが所有していることが多いため、バックエンドは通常、クライアントとは異なるペースで「移動」します。

これは、そうでないプロジェクトの開始時に特に当てはまります。開発に使用できる状態のAPIがあります。バックエンドで作業しているエンジニアが下す矛盾する決定についてあまり心配することなく、クライアントに内部的に渡されるデータオブジェクトに関する設計と実装の決定を行いたいと考えています。

それ以外にも、同じビジネスデータをクライアントに返すサービスはほとんどありませんが、それらは異なるチームによって所有され、異なる問題を解決しようとしているため、APIを介して返されるデータの構造とタイプが互いに異なります。クライアントの内部では、これらは実際には同じものを表しているため、これらの応答を変換してクライアントにとって普遍的なものに組み合わせることができることは非常に理にかなっています。

ここでは、「ユーザー」のバリエーションを返すAPIに2つのエンドポイントがあることがわかります。ログインエンドポイントとプロファイルエンドポイント。それらを1つのデータ型にマージするには、処理するバリエーションごとにコンストラクターを作成するだけで、アプリケーションはAPIが1か所に提供する2つの異なるタイプのユーザーの知識を制限できます。これにより、APIのデータ構造とタイプの変更を1つのエンドポイントに制限しながら、機能間で(モデルレイヤーを介して)データを共有することがはるかに簡単になります。

ネットワーク応答オブジェクトを区別しています。アーキテクチャ内のビジネスモデルオブジェクト。これは、ネットワーク応答を取得してアプリケーションの残りの部分のビジネスモデルに変換するためのリモートデータソースの役割を定義するのにも役立ちます。

これらのレイヤーが生成するデータタイプを定義すると、リモートデータソースの役割を定義するのにも役立ちます
レイヤー間で送信されるデータのタイプを明確にし、より多くの再利用を可能にします

コードの例

次に、GoogleのUserRepositoryの例に飛び込み、独自のバージョンを作成します。上記で行った変更に固執します。

まず、GoogleのUserRepositoryの最終バージョンを見てみましょう。

Dagger(または Hilt )を使用していることがわかります、 Kotlin Coroutines 、Room、そしておそらくRetrofit。 Retrofitサービス、コルーチンエグゼキュータ、およびDao(データアクセスオブジェクト)をリポジトリに提供します。

これの基本的なフローは、データのネットワークリクエストを行い、データ(または監視しているもの)を返すことです。データの場合)すぐに部屋から。ネットワーク要求が完了したら、データに対して必要なことをすべて実行し、新しいオブジェクトをテーブルに挿入します。挿入により、以前に返されたデータに変更があったことが自動的に通知され、新しい取得が求められ、最後にビューが更新されます。

いくつかの設定

UserRepository、最初に、実行するスレッドを挿入する便利な方法など、必要になるいくつかの事項に対処する必要があります。

これは、後でテストするのに役立ちます。これを短剣グラフで設定すると、コードベース全体に正しいスレッドを簡単に挿入できると同時に、ユニット内の TestScheduler と交換することができます。テスト(ユニットテストを書いています…そうですか?)

これがユーザークラスです。UserResponseは、APIからRetrofitおよび、社内で受け継ぐビジネスモデル。ネットワークオブジェクトをシンプルにし、コードジェネレーターを使用して作成することもできますが、ビジネスオブジェクトは必要なものにさらに一致させることができます。

ここでは、Retrofitサービスを定義しています。 Singleの代わりにObservableを返すようにすることもできます。これにより、ダウンストリームロジックが少し単純になりますが、ネットワークリクエストとSingleがどのように機能するか、非同期と成功または失敗の両方。そのロジックは、リモートデータソースレイヤーでも実行されます。

次は私たちの部屋ですダオ。 Roomはすでにインターフェースとアノテーションで機能しているため、永続データの処理方法をわかりにくくするために、別のインターフェース、クラス、オブジェクトを作成する必要性を感じませんでした。

Observableを使用して、Userオブジェクトの放出に反応します。挿入アクションでCompletableを返すと、発生する可能性のあるルームエラーを処理できます。

最後に、これがUserRepository自体の最後のインターフェースです。非常にシンプルでクリーンです。必要なもの以外の唯一の追加部分は、onCleared()メソッドです。これは、ViewModelもクリアされます。

実装

コンストラクターの署名が上記のGoogleの例と非常に似ていることに気付くでしょう。ネットワーク経由でデータを取得できるオブジェクト、Dao、および実行するスレッドを指示するオブジェクトを提供しています。

getUser(userId: Int)メソッドでも上記の例での動作は似ています。非同期ネットワーク要求を作成し、データベースからのデータを監視しているオブジェクトをすぐに返します。そのネットワークリクエストが完了したら、そのデータをテーブルに挿入します。これにより、UIの更新がトリガーされます。

ここでは、RxJavaであまり凝ったことをしていないことに気付くでしょう。新しいPublishSubjectを作成し、データを収集します。これを永続レイヤーのselectとマージすると、Observableが返され、ネットワークレイヤーからもエラーが送信されます。各レイヤーから上のレイヤーに必要なエラーのみを取得する方法を何度も行き来しました。これは、私たちが到達した最も単純で最もカスタマイズ可能なソリューションでした。はい、追加のオブザーバブルが作成されますが、エラー処理をより細かく制御できます。

最後パズルのピースはリモートデータソースです。ここでは、UserResponseUserに変換して、残りのコードベースを異なるペースで移動できるようにする方法を確認できます。 APIから入力されるデータ。

すべてをまとめる

この時点で、これらのオブジェクトを上から短剣グラフに追加する必要があります。提供されたメソッドは、実装ではなくインターフェースタイプを返す必要があることに注意してください。このようにして、Daggerグラフで上記で定義されたすべてのオブジェクトを構築でき、コードベースの任意の場所でUserRepositoryからデータを非常に簡単に取得できます。

最後に、RxJavaを使用しているため、必ずclear()を呼び出す必要があります。 DisposableオブジェクトはViewModelにあり、onCleared()UserRepositoryインスタンスで内部のDisposableオブジェクトをクリーンアップします。

これで、リポジトリを介して提供されるリポジトリを使用した、かなり単純なMVVM実装ができました。ダガーグラフ。私たちが作成したレイヤーはそれぞれ明確な目的を果たし、外の世界に公開するものをフィルタリングするのに役立つインターフェースによって保持されます。これにより、RxJava TestSchedulerを使用して、これらのレイヤーを互いに独立してテストすることもできます。これにより、テストの実行を完全に制御できます。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です