Android MVVMViewModelでコンテキストを取得する方法


96

AndroidアプリにMVVMパターンを実装しようとしています。ViewModelsには(テストを簡単にするために)Android固有のコードを含めるべきではないことを読みましたが、さまざまなこと(xmlからのリソースの取得、設定の初期化など)にコンテキストを使用する必要があります。これを行うための最良の方法は何ですか?AndroidViewModelアプリケーションコンテキストへの参照があるのを見ましたが、Android固有のコードが含まれているため、ViewModelに含める必要があるかどうかはわかりません。また、これらはアクティビティライフサイクルイベントに関連付けられていますが、コンポーネントのスコープを管理するために短剣を使用しているため、それがどのように影響するかはわかりません。私はMVVMパターンとDaggerを初めて使用するので、助けていただければ幸いです。


念のために誰かが使用しようとしているAndroidViewModelが、取得Cannot create instance exception後、あなたは私のこの回答を参照することができstackoverflow.com/a/62626408/1055241
gprathour

あなたはその方法からコンテキストを取得するために、代わりにユースケースを作成し、ViewModelにコンテキストを使用しないでください
ルーベンキャスター

回答:


78

Applicationによって提供されるコンテキストを使用できます。これは、参照を含む単純なaをAndroidViewModel拡張する必要がAndroidViewModelあります。ViewModelApplication


チャームのように働いた!
SPM

63

Androidアーキテクチャコンポーネントのビューモデルの場合、

メモリリークとして、アクティビティコンテキストをアクティビティのViewModelに渡すことはお勧めできません。

したがって、ViewModelでコンテキストを取得するには、ViewModelクラスでAndroid ViewModelクラスを拡張する必要があります。そうすれば、以下のサンプルコードに示すようなコンテキストを取得できます。

class ActivityViewModel(application: Application) : AndroidViewModel(application) {

    private val context = getApplication<Application>().applicationContext

    //... ViewModel methods 

}

3
アプリケーションパラメータと通常のViewModelを直接使用してみませんか?「getApplication <Application>()」には意味がありません。ボイラープレートを追加するだけです。
信じられないほどの

なぜメモリリークになるのでしょうか?
ベンバターワース

アクティビティはビューモデルよりも頻繁に破棄されるためです(たとえば、画面が回転しているとき)。残念ながら、ビューモデルにはまだメモリへの参照があるため、メモリはガベージコレクションによって解放されません。
ベンバターワース

52

テストを容易にするのは抽象化であるため、ViewModelsにテストを容易にするためのAndroid固有のコードを含めるべきではないというわけではありません。

ViewModelsにContextのインスタンスや、ViewsやContextを保持する他のオブジェクトのようなものを含めるべきではない理由は、それがアクティビティやフラグメントとは別のライフサイクルを持っているためです。

これが意味するのは、アプリで回転の変更を行ったとしましょう。これにより、アクティビティとフラグメントがそれ自体を破壊し、それ自体を再作成します。ViewModelはこの状態の間も存続することを目的としているため、破棄されたアクティビティのビューまたはコンテキストを保持していると、クラッシュやその他の例外が発生する可能性があります。

やりたいことをどのように行うべきかについては、MVVMとViewModelはJetPackのデータバインディングコンポーネントと非常にうまく機能します。通常、String、intなどを格納するほとんどの場合、データバインディングを使用してビューに直接表示させることができるため、ViewModel内に値を格納する必要はありません。

ただし、データバインディングが必要ない場合でも、コンストラクターまたはメソッド内でコンテキストを渡してリソースにアクセスできます。ViewModel内にそのコンテキストのインスタンスを保持しないでください。


1
android固有のコードを含めるには、単純なJUnitテストよりもはるかに遅いインストルメンテーションテストを実行する必要があることを理解していました。現在、クリックメソッドにデータバインディングを使用していますが、xmlからリソースを取得したり設定したりするのにどのように役立つかわかりません。好みのために、モデル内のコンテキストも必要になることに気づきました。私が現在行っているのは、Daggerにアプリケーションコンテキストを挿入させることです(コンテキストモジュールは、アプリケーションクラス内の静的メソッドから取得します)
Vincent Williams

@VincentWilliamsはい。ViewModelを使用すると、UIコンポーネントからコードを抽象化できるため、テストを簡単に実行できます。しかし、私が言っているのは、コンテキストやビューなどを含めない主な理由は、テストの理由ではなく、クラッシュやその他のエラーを回避するのに役立つViewModelのライフサイクルのためです。データバインディングに関しては、コード内のリソースにアクセスする必要があるほとんどの場合、データバインディングが直接実行できる文字列、色、寸法をレイアウトに適用する必要があるため、これはリソースの使用に役立ちます。
ジャッキー2018

3
ビューモデルの値に基づいてテキストビューのテキストを切り替えたい場合は、文字列をローカライズする必要があるため、コンテキストなしでビューモデルのリソースを取得する必要があります。リソースにアクセスするにはどうすればよいですか?
SrishtiRoy19年

3
@SrishtiRoyデータバインディングを使用する場合、ビューモデルの値に基づいてTextViewのテキストを簡単に切り替えることができます。これらはすべてレイアウトファイル内で行われるため、ViewModel内のコンテキストにアクセスする必要はありません。ただし、ViewModel内でContextを使用する必要がある場合は、ViewModelの代わりにAndroidViewModelの使用を検討する必要があります。AndroidViewModelには、getApplication()で呼び出すことができるアプリケーションコンテキストが含まれているため、ViewModelにコンテキストが必要な場合は、コンテキストのニーズを満たす必要があります。
ジャッキー

1
@PacerierViewModelの主な目的を誤解しました。これは関心の分離の問題です。ViewModelは、ビューレイヤーによって表示されているデータを維持する責任があるため、ビューへの参照を保持しないでください。UIコンポーネント(別名ビュー)はビューレイヤーによって維持され、Androidシステムは必要に応じてビューを再作成します。古いビューへの参照を保持すると、この動作と競合し、メモリリークが発生します。
ジャッキー

16

簡単な答え-これをしないでください

どうして ?

ビューモデルの目的全体を無効にします

ビューモデルで実行できるほとんどすべてのことは、LiveDataインスタンスやその他のさまざまな推奨アプローチを使用して、アクティビティ/フラグメントで実行できます。


26
では、なぜAndroidViewModelクラスが存在するのでしょうか。
AlexBerdnikov19年

1
@AlexBerdnikov MVVMの目的は、MVPよりもさらにViewModelからビュー(アクティビティ/フラグメント)を分離することです。テストが簡単になるように。
hushed_voice

3
@free_style説明してくれてありがとう、しかし疑問はまだ残っています:ViewModelでコンテキストを保持してはいけないのなら、なぜAndroidViewModelクラスが存在するのでしょうか?その全体的な目的は、アプリケーションのコンテキストを提供することですよね。
AlexBerdnikov19年

7
@AlexBerdnikovビューモデル内でアクティビティコンテキストを使用すると、メモリリークが発生する可能性があります。したがって、AndroidViewModelクラスを使用すると、(うまくいけば)メモリリークを引き起こさないアプリケーションコンテキストが提供されます。したがって、AndroidViewModelを使用する方が、アクティビティコンテキストを渡すよりも優れている可能性があります。しかし、それでもそうすると、テストが困難になります。これは私の見解です。
hushed_voice

1
リポジトリのres / rawフォルダのファイルにアクセスできませんか?
Fugogugo

15

ViewModelに直接Contextを含める代わりに、最終的には、必要なリソースを提供するResourceProviderなどのプロバイダークラスを作成し、それらのプロバイダークラスをViewModelに挿入しました。


1
AppModuleのDaggerでResourcesProviderを使用しています。ResourcesProviderまたはAndroidViewModelからコンテキストを取得するためのその良いアプローチは、リソースのコンテキストを取得するために優れていますか?
UsmanRana18年

@Vincent:resourceProviderを使用してViewModel内でDrawableを取得する方法は?
ブルマ2018

@Vegeta getDrawableRes(@DrawableRes int id)ResourceProviderクラス内のようなメソッドを追加します
Vincent Williams

1
これは、フレームワークの依存関係がドメインロジック(ViewModels)に境界を越えてはならないというクリーンアーキテクチャアプローチに反します。
IgorGanapolsky

1
@IgorGanapolsky VMは、正確にはドメインロジックではありません。ドメインロジックは、いくつか例を挙げると、インタラクターやリポジトリなどの他のクラスです。VMはドメインと相互作用するため、「接着剤」カテゴリに分類されますが、直接は相互作用しません。VMがドメインの一部である場合は、VMに過度の責任を負わせるため、パターンの使用方法を再検討する必要があります。
mradzinski

9

TL; DR:ViewModelのDaggerを介してアプリケーションのコンテキストを挿入し、それを使用してリソースをロードします。画像をロードする必要がある場合は、データバインディングメソッドからの引数を介してViewインスタンスを渡し、そのViewコンテキストを使用します。

MVVMは優れたアーキテクチャであり、Android開発の未来であることは間違いありませんが、まだ環境に配慮していることがいくつかあります。MVVMアーキテクチャのレイヤー通信を例にとると、さまざまな開発者(非常によく知られている開発者)がLiveDataを使用してさまざまな方法でさまざまなレイヤーを通信しているのを見てきました。それらのいくつかは、LiveDataを使用してViewModelをUIと通信しますが、コールバックインターフェイスを使用してリポジトリと通信するか、Interactors / UseCasesを使用して、LiveDataを使用してそれらと通信します。ここでのポイントは、すべてがまだ100%定義されいるわけではないということです。

そうは言っても、特定の問題に対する私のアプローチは、DIを介してアプリケーションのコンテキストを利用可能にしてViewModelsで使用し、strings.xmlからStringなどを取得することです。

画像の読み込みを扱っている場合は、データバインディングアダプターメソッドからViewオブジェクトをパススルーし、Viewのコンテキストを使用して画像を読み込みます。どうして?アプリケーションのコンテキストを使用して画像を読み込むと、一部のテクノロジー(Glideなど)で問題が発生する可能性があるためです。

それが役に立てば幸い!


5
TL; DRがトップになるはずです
JacquesKoorts19年

1
ご回答ありがとうございます。ただし、ビューモデルをandroidviewmodelから拡張し、クラス自体が提供する組み込みコンテキストを使用できるのに、なぜ短剣を使用してコンテキストを挿入するのでしょうか。特に、短剣とMVVMを連携させるためのとんでもない量の定型コードを考慮すると、他のソリューションははるかに明確に見えます。これについてどう思いますか?
JosipDomazet19年

8

他の人がAndroidViewModel言ったように、アプリを入手するために導き出すことができるものがありますContextが、私がコメントで集めたものから、あなたは目的のMVVMを打ち負かす@drawableあなたの内部からsを操作しようとしてViewModelいます。

一般的には、持っている必要がContextあなたにViewModelほとんど一般には、あなたがあなたの間の論理分割の仕方を再考検討すべき示唆View秒とViewModels

ViewModelドローアブルを解決してアクティビティ/フラグメントにフィードする代わりに、フラグメント/アクティビティに、が所有するデータに基づいてドローアブルをジャグリングさせることを検討してくださいViewModel。たとえば、オン/オフ状態のビューに表示するさまざまなドローアブルが必要です。これViewModelは(おそらくブール値の)状態を保持する必要がありViewますが、それに応じてドローアブルを選択するのはビジネスです。

DataBindingを使用すると非常に簡単に実行できます。

<ImageView
...
app:src="@{viewModel.isOn ? @drawable/switch_on : @drawable/switch_off}"
/>

より多くの状態とドローアブルがある場合、レイアウトファイルの扱いにくいロジックを回避するために、たとえば値を(カードスーツなど)に変換するカスタムBindingAdapterを作成できます。EnumR.drawable.*

または、Context内部で使用するコンポーネントが必要な場合もあります。ViewModel次に、外部でコンポーネントを作成してViewModel渡します。DIまたはシングルトンを使用するかContextViewModelin Fragment/を初期化する直前に依存コンポーネントを作成できますActivity

なぜわざわざ:ContextAndroid固有のものであり、ViewModelsのそれらに依存することは悪い習慣です:それらはユニットテストの邪魔になります。一方、独自のコンポーネント/サービスインターフェイスは完全に制御できるため、テスト用に簡単にモックすることができます。


5

アプリケーションコンテキストへの参照がありますが、Android固有のコードが含まれています

朗報Mockito.mock(Context.class)です。テストで必要なものを使用してコンテキストを返すことができます。

したがってViewModel、通常どおりにを使用し、通常どおりにViewModelProviders.Factoryを介してApplicationContextを指定します。


3

getApplication().getApplicationContext()ViewModel内からアプリケーションコンテキストにアクセスできます。これは、リソースや設定などにアクセスするために必要なものです。


質問を絞り込むと思います。ビューモデル内にコンテキスト参照があるのは悪いことですか(これはテストに影響しませんか?)、AndroidViewModelクラスを使用するとDaggerに何らかの影響がありますか?それは活動のライフサイクルに結びついていませんか?私はDaggerを使用してコンポーネントのライフサイクルを制御しています
Vincent Williams

14
ViewModelクラスはありませんgetApplication方法を。
beroal

4
いいえ、でもAndroidViewModelない
4Oh4

1
ただし、コンストラクターでApplicationインスタンスを渡す必要があります。これは、そこからApplicationインスタンスにアクセスするのと同じです
JohnSardinha19年

2
アプリケーションのコンテキストがあっても大きな問題にはなりません。フラグメント/アクティビティが破棄され、ビューモデルに現在存在しないコンテキストへの参照がまだある場合は中断されるため、アクティビティ/フラグメントコンテキストは必要ありません。ただし、APPLICATIONコンテキストが破棄されることはありませんが、VMにはそれへの参照があります。正しい?アプリは終了するが、Viewmodelが終了しないシナリオを想像できますか?:)
user17134 5019年

3

ViewModelを使用する動機はJavaコードとAndroidコードを分離することであるため、ViewModelでAndroid関連のオブジェクトを使用しないでください。これにより、ビジネスロジックを個別にテストでき、Androidコンポーネントとビジネスロジックの個別のレイヤーが作成されます。およびデータ、クラッシュにつながる可能性があるため、ViewModelにコンテキストを含めないでください


2
これは公正な観察ですが、一部のバックエンドライブラリには、MediaStoreなどのアプリケーションコンテキストが必要です。以下の4gus71nによる回答は、妥協する方法を説明しています。
ブライアンW.ワーグナー

1
はい、アプリケーションコンテキストは使用できますが、アクティビティのコンテキストは使用できません。アプリケーションコンテキストはアプリケーションのライフサイクル全体にわたって存在しますが、アクティビティコンテキストを非同期プロセスに渡すとメモリリークが発生する可能性があるため、アクティビティコンテキストは使用できません。私の投稿で言及されているコンテキストはアクティビティです。コンテキスト。ただし、アプリケーションコンテキストであっても、非同期プロセスにコンテキストを渡さないように注意する必要があります。
RohitSharma19年

2

クラスSharedPreferencesを利用する際に問題が発生したViewModelため、上記の回答からアドバイスを受け、を使用して次のことを行いましたAndroidViewModel。今はすべてが素晴らしく見えます

のために AndroidViewModel

import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;

import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.preference.PreferenceManager;

public class HomeViewModel extends AndroidViewModel {

    private MutableLiveData<String> some_string;

    public HomeViewModel(Application application) {
        super(application);
        some_string = new MutableLiveData<>();
        Context context = getApplication().getApplicationContext();
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        some_string.setValue("<your value here>"));
    }

}

そして、 Fragment

import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;


public class HomeFragment extends Fragment {


    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {
        final View root = inflater.inflate(R.layout.fragment_home, container, false);
        HomeViewModel homeViewModel = ViewModelProviders.of(this).get(HomeViewModel.class);
        homeViewModel.getAddress().observe(getViewLifecycleOwner(), new Observer<String>() {
            @Override
            public void onChanged(@Nullable String address) {


            }
        });
        return root;
    }
}

0

私はそれをこのように作成しました:

@Module
public class ContextModule {

    @Singleton
    @Provides
    @Named("AppContext")
    public Context provideContext(Application application) {
        return application.getApplicationContext();
    }
}

そして、AppComponentにContextModule.classを追加しました。

@Component(
       modules = {
                ...
               ContextModule.class
       }
)
public interface AppComponent extends AndroidInjector<BaseApplication> {
.....
}

次に、ViewModelにコンテキストを挿入しました。

@Inject
@Named("AppContext")
Context context;

0

次のパターンを使用します。

class NameViewModel(
val variable:Class,application: Application):AndroidViewModel(application){
   body...
}

0

ViewModelにコンテキストを挿入する際の問題は、画面の回転、ナイトモード、またはシステム言語に応じて、コンテキストがいつでも変更される可能性があり、それに応じて返されるリソースが変更される可能性があることです。単純なリソースIDを返すと、getString置換などの追加パラメーターで問題が発生します。高レベルの結果を返し、レンダリングロジックをアクティビティに移動すると、テストが難しくなります。

私の解決策は、ViewModelに関数を生成して返すようにすることです。この関数は、後でアクティビティのコンテキストを介して実行されます。Kotlinの構文糖衣構文により、これは非常に簡単になります。

ViewModel.kt:

// connectedStatus holds a function that calls Context methods
// `this` can be elided
val connectedStatus = MutableLiveData<Context.() -> String> {
  // initial value
  this.getString(R.string.connectionStatusWaiting)
}
connectedStatus.postValue {
  this.getString(R.string.connectionStatusConnected, brand)
}
Activity.kt  // is a Context

override fun onCreate(_: Bundle?) {
  connectionViewModel.connectedStatus.observe(this) { it ->
   // runs the posted value with the given Context receiver
   txtConnectionStatus.text = this.run(it)
  }
}

これにより、ViewModelは、ユニットテストによって検証された、表示された情報を計算するためのすべてのロジックを保持できます。アクティビティは、バグを隠すための内部ロジックのない非常に単純な表現です。


:データ・バインディングサポートを有効にするには、ちょうどそうのようなシンプルなのBindingAdapterを追加@BindingAdapter("android:text") fun setText(view: TextView, value: Context.() -> String) { view.text = view.context.run(value) }
hufman
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.