Android ViewModelの追加引数


106

AndroidViewModelアプリケーションコンテキストを除いて、カスタムコンストラクターに追加の引数を渡す方法はありますか?例:

public class MyViewModel extends AndroidViewModel {
    private final LiveData<List<MyObject>> myObjectList;
    private AppDatabase appDatabase;

    public MyViewModel(Application application, String param) {
        super(application);
        appDatabase = AppDatabase.getDatabase(this.getApplication());

        myObjectList = appDatabase.myOjectModel().getMyObjectByParam(param);
    }
}

そして、カスタムViewModelクラスを使用したい場合は、このコードをフラグメントで使用します。

MyViewModel myViewModel = ViewModelProvider.of(this).get(MyViewModel.class)

そのString paramため、カスタムに追加の引数を渡す方法がわかりませんViewModel。アプリケーションコンテキストのみを渡すことができますが、追加の引数は渡せません。私は本当にどんな助けにも感謝します。ありがとうございました。

編集:いくつかのコードを追加しました。私はそれが今より良いことを望みます。


詳細とコードを追加
hugo

エラーメッセージは何ですか?
モーゼスアプリコ2017

エラーメッセージはありません。ViewModelProviderはAndroidViewModelオブジェクトの作成に使用されるため、コンストラクターの引数を設定する場所がわからないだけです。
Mario Rudman 2017

回答:


211

ViewModelのファクトリクラスが必要です。

public class MyViewModelFactory implements ViewModelProvider.Factory {
    private Application mApplication;
    private String mParam;


    public MyViewModelFactory(Application application, String param) {
        mApplication = application;
        mParam = param;
    }


    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        return (T) new MyViewModel(mApplication, mParam);
    }
}

そして、ビューモデルをインスタンス化するとき、あなたはこのようにします:

MyViewModel myViewModel = ViewModelProvider(this, new MyViewModelFactory(this.getApplication(), "my awesome param")).get(MyViewModel.class);

kotlinの場合、委任されたプロパティを使用できます。

val viewModel: MyViewModel by viewModels { MyViewModelFactory(getApplication(), "my awesome param") }

もう1つの新しいオプションもあります。ファクトリのインスタンス化を実装HasDefaultViewModelProviderFactoryしてオーバーライドするgetDefaultViewModelProviderFactory()と、ファクトリを呼び出すViewModelProvider(this)かどうかに関係by viewModels()なく、


4
すべてのViewModelクラスにViewModelFactoryが必要ですか?
dmlebron 2018年

6
しかし、すべてのViewModelDIが異なる可能性があります。create()メソッドで返されるインスタンスをどのようにして知ることができますか?
dmlebron 2018年

1
ViewModelは、向きを変更した後に再作成されます。あなたは毎回工場を作ることができません。
Tim

3
それは真実ではない。新規ViewModel作成はメソッドを防ぎget()ます。ドキュメントに基づく:「このViewModelProviderに関連付けられているスコープ(通常はフラグメントまたはアクティビティ)に既存のViewModelを返すか、新しいViewModelを作成します。」参照:developer.android.com/reference/android/arch/lifecycle/...
mlyko

2
return modelClass.cast(new MyViewModel(mApplication, mParam))警告を取り除くためにを使用するのはどう
ですか

22

依存性注入で実装

これは、より高度で、量産コードに適しています。

Dagger2、SquareのAssistedInjectは、ネットワークやデータベースのリクエストを処理するリポジトリなどの必要なコンポーネントを注入できるViewModelsの本番環境対応の実装を提供します。また、アクティビティ/フラグメントに引数/パラメーターを手動で挿入することもできます。これは、Gabor Varadiの詳細な投稿であるDagger Tipsに基づいて、コードGistsで実装する手順の簡潔な概要です。

Dagger Hiltは7/12/20のアルファ版の次世代ソリューションであり、ライブラリがリリースステータスになると、同じユースケースでセットアップがより簡単になります。

KotlinにLifecycle 2.2.0で実装する

引数/パラメーターを渡す

// Override ViewModelProvider.NewInstanceFactory to create the ViewModel (VM).
class SomeViewModelFactory(private val someString: String): ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T = SomeViewModel(someString) as T
} 

class SomeViewModel(private val someString: String) : ViewModel() {
    init {
        //TODO: Use 'someString' to init process when VM is created. i.e. Get data request.
    }
}

class Fragment: Fragment() {
    // Create VM in activity/fragment with VM factory.
    val someViewModel: SomeViewModel by viewModels { SomeViewModelFactory("someString") } 
}

引数/パラメーターを使用してSavedStateを有効にする

class SomeViewModelFactory(
        private val owner: SavedStateRegistryOwner,
        private val someString: String) : AbstractSavedStateViewModelFactory(owner, null) {
    override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, state: SavedStateHandle) =
            SomeViewModel(state, someString) as T
}

class SomeViewModel(private val state: SavedStateHandle, private val someString: String) : ViewModel() {
    val feedPosition = state.get<Int>(FEED_POSITION_KEY).let { position ->
        if (position == null) 0 else position
    }
        
    init {
        //TODO: Use 'someString' to init process when VM is created. i.e. Get data request.
    }
        
     fun saveFeedPosition(position: Int) {
        state.set(FEED_POSITION_KEY, position)
    }
}

class Fragment: Fragment() {
    // Create VM in activity/fragment with VM factory.
    val someViewModel: SomeViewModel by viewModels { SomeViewModelFactory(this, "someString") } 
    private var feedPosition: Int = 0
     
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        someViewModel.saveFeedPosition((contentRecyclerView.layoutManager as LinearLayoutManager)
                .findFirstVisibleItemPosition())
    }    
        
    override fun onViewStateRestored(savedInstanceState: Bundle?) {
        super.onViewStateRestored(savedInstanceState)
        feedPosition = someViewModel.feedPosition
    }
}

ファクトリでcreateをオーバーライドすると、チェックされていないキャスト 'ItemViewModel to T'
Ssenyonjo

1
その警告は今のところ私にとって問題ではありません。ただし、フラグメントを介してインスタンスを作成するのではなく、Daggerを使用して注入するようにViewModelファクトリをリファクタリングするときは、さらに調査します。
Adam Hurwitz

15

複数の異なるビューモデル間で共有される1つのファクトリについて、mlykoの答えを次のように拡張します。

public class MyViewModelFactory extends ViewModelProvider.NewInstanceFactory {
    private Application mApplication;
    private Object[] mParams;

    public MyViewModelFactory(Application application, Object... params) {
        mApplication = application;
        mParams = params;
    }

    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        if (modelClass == ViewModel1.class) {
            return (T) new ViewModel1(mApplication, (String) mParams[0]);
        } else if (modelClass == ViewModel2.class) {
            return (T) new ViewModel2(mApplication, (Integer) mParams[0]);
        } else if (modelClass == ViewModel3.class) {
            return (T) new ViewModel3(mApplication, (Integer) mParams[0], (String) mParams[1]);
        } else {
            return super.create(modelClass);
        }
    }
}

そしてビューモデルをインスタンス化します:

ViewModel1 vm1 = ViewModelProviders.of(this, new MyViewModelFactory(getApplication(), "something")).get(ViewModel1.class);
ViewModel2 vm2 = ViewModelProviders.of(this, new MyViewModelFactory(getApplication(), 123)).get(ViewModel2.class);
ViewModel3 vm3 = ViewModelProviders.of(this, new MyViewModelFactory(getApplication(), 123, "something")).get(ViewModel3.class);

さまざまなコンストラクターを持つさまざまなビューモデル。


8
いくつかの理由があるため、この方法はお勧めしません。1)ファクトリーのパラメーターがタイプセーフではない-この方法で、実行時にコードを破壊できます。2)ビューモデルのタイプをチェックすることは、実際にはOOPの方法ではありません。ViewModelは基本型にキャストされるため、コンパイル時に警告なしに実行時にコードを中断することができます。この場合、デフォルトのandroidファクトリを使用して、すでにインスタンス化されたビューモデルにパラメーターを渡すことをお勧めします。
mlyko

@mlyko確かに、これらはすべて有効な異論であり、viewmodelデータを設定する独自のメソッドは常にオプションです。ただし、ビューモデルが初期化されていることを確認したい場合があるため、コンストラクタを使用します。それ以外の場合は、「viewmodelがまだ初期化されていない」状況を自分で処理する必要があります。たとえば、viewmodelにLivedDataを返すメソッドがあり、オブザーバーがさまざまなViewライフサイクルメソッドでそれにアタッチされている場合。
rzehan

3

@ vilpe89に基づいて、AndroidViewModelケース用の上記のKotlinソリューション

class ExtraParamsViewModelFactory(private val application: Application, private val myExtraParam: String): ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T = SomeViewModel(application, myExtraParam) as T

}

次に、フラグメントは次のようにviewModelを開始できます。

class SomeFragment : Fragment() {
 ....
    private val myViewModel: SomeViewModel by viewModels {
        ExtraParamsViewModelFactory(this.requireActivity().application, "some string value")
    }
 ....
}

そして実際のViewModelクラス

class SomeViewModel(application: Application, val myExtraParam:String) : AndroidViewModel(application) {
....
}

またはいくつかの適切な方法で...

override fun onActivityCreated(...){
    ....

    val myViewModel = ViewModelProvider(this, ExtraParamsViewModelFactory(this.requireActivity().application, "some string value")).get(SomeViewModel::class.java)

    ....
}

質問は、上記が従わないコンテキストを使用せずに引数/パラメーターを渡す方法を尋ねます:アプリケーションコンテキストを除いて、カスタムAndroidViewModelコンストラクターに追加の引数を渡す方法はありますか?
アダムHurwitz

3

作成済みのオブジェクトが渡されるクラスにしました。

private Map<String, ViewModel> viewModelMap;

public ViewModelFactory() {
    this.viewModelMap = new HashMap<>();
}

public void add(ViewModel viewModel) {
    viewModelMap.put(viewModel.getClass().getCanonicalName(), viewModel);
}

@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
    for (Map.Entry<String, ViewModel> viewModel : viewModelMap.entrySet()) {
        if (viewModel.getKey().equals(modelClass.getCanonicalName())) {
            return (T) viewModel.getValue();
        }
    }
    return null;
}

その後

ViewModelFactory viewModelFactory = new ViewModelFactory();
viewModelFactory.add(new SampleViewModel(arg1, arg2));
SampleViewModel sampleViewModel = ViewModelProviders.of(this, viewModelFactory).get(SampleViewModel.class);

パラメータをコンストラクタに渡すには、すべてのViewModelにViewModelFactoryが必要ですか?
K Pradeep Kumar Reddy

いいえ。すべてのViewModelに対して1つのViewModelFactoryのみ
Danil

hashMapキーとして正規名を使用する理由はありますか?class.simpleNameを使用できますか?
Kプラディープクマーレディ

はい。ただし、重複する名前がないことを確認する必要があります
Danil

これは推奨されるコードの記述スタイルですか?あなたは自分でこのコードを思いついたのですか、それともアンドロイドのドキュメントで読んでいますか?
K Pradeep Kumar Reddy

1

私は、Daggerが依存関係として提供できるViewModel引数をシームレスに使用しながら、これをより簡単かつクリーンに行うライブラリを作成しました。マルチバインディングやファクトリボイラープレートは必要ありません。https: //github.com/radutopor/ViewModelFactory

@ViewModelFactory
class UserViewModel(@Provided repository: Repository, userId: Int) : ViewModel() {

    val greeting = MutableLiveData<String>()

    init {
        val user = repository.getUser(userId)
        greeting.value = "Hello, $user.name"
    }    
}

ビューで:

class UserActivity : AppCompatActivity() {
    @Inject
    lateinit var userViewModelFactory2: UserViewModelFactory2

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)
        appComponent.inject(this)

        val userId = intent.getIntExtra("USER_ID", -1)
        val viewModel = ViewModelProviders.of(this, userViewModelFactory2.create(userId))
            .get(UserViewModel::class.java)

        viewModel.greeting.observe(this, Observer { greetingText ->
            greetingTextView.text = greetingText
        })
    }
}

0

(KOTLIN)私のソリューションはリフレクションを少し使用しています。

いくつかの引数を必要とする新しいViewModelクラスを作成するたびに、同じように見えるFactoryクラスを作成したくないとしましょう。リフレクションを介してこれを実現できます。

たとえば、2つの異なるアクティビティがあるとします。

class Activity1 : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val args = Bundle().apply { putString("NAME_KEY", "Vilpe89") }
        val viewModel = ViewModelProviders.of(this, ViewModelWithArgumentsFactory(args))
            .get(ViewModel1::class.java)
    }
}

class Activity2 : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val args = Bundle().apply { putInt("AGE_KEY", 29) }
        val viewModel = ViewModelProviders.of(this, ViewModelWithArgumentsFactory(args))
            .get(ViewModel2::class.java)
    }
}

そしてそれらのアクティビティのViewModels:

class ViewModel1(private val args: Bundle) : ViewModel()

class ViewModel2(private val args: Bundle) : ViewModel()

次に、魔法の部分であるFactoryクラスの実装:

class ViewModelWithArgumentsFactory(private val args: Bundle) : NewInstanceFactory() {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        try {
            val constructor: Constructor<T> = modelClass.getDeclaredConstructor(Bundle::class.java)
            return constructor.newInstance(args)
        } catch (e: Exception) {
            Timber.e(e, "Could not create new instance of class %s", modelClass.canonicalName)
            throw e
        }
    }
}

0

次のようにしてみませんか。

public class MyViewModel extends AndroidViewModel {
    private final LiveData<List<MyObject>> myObjectList;
    private AppDatabase appDatabase;
    private boolean initialized = false;

    public MyViewModel(Application application) {
        super(application);
    }

    public initialize(String param){
      synchronized ("justInCase") {
         if(! initialized){
          initialized = true;
          appDatabase = AppDatabase.getDatabase(this.getApplication());
          myObjectList = appDatabase.myOjectModel().getMyObjectByParam(param);
    }
   }
  }
}

次に、このように2つのステップで使用します。

MyViewModel myViewModel = ViewModelProvider.of(this).get(MyViewModel.class)
myViewModel.initialize(param)

2
コンストラクターにパラメーターを配置する重要な点は、ビューモデルを1 だけ初期化することです。お電話の場合は、あなたの実装では、myViewModel.initialize(param)onCreateの活動の、例えば、それは同じで複数回呼び出すことができMyViewModel、ユーザーがデバイスを回転させると、インスタンス。
Sanlok Lee

@サンロック・リー・オーケー。関数に条件を追加して、不要なときに初期化されないようにしてください。編集した回答を確認してください。
Amr Berag

0
class UserViewModelFactory(private val context: Context) : ViewModelProvider.NewInstanceFactory() {
 
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return UserViewModel(context) as T
    }
 
}
class UserViewModel(private val context: Context) : ViewModel() {
 
    private var listData = MutableLiveData<ArrayList<User>>()
 
    init{
        val userRepository : UserRepository by lazy {
            UserRepository
        }
        if(context.isInternetAvailable()) {
            listData = userRepository.getMutableLiveData(context)
        }
    }
 
    fun getData() : MutableLiveData<ArrayList<User>>{
        return listData
    }

アクティビティでビューモデルを呼び出す

val userViewModel = ViewModelProviders.of(this,UserViewModelFactory(this)).get(UserViewModel::class.java)

詳細情報:Android MVVM Kotlinの例


質問は、上記が従わないコンテキストを使用せずに引数/パラメーターを渡す方法を尋ねます:アプリケーションコンテキストを除いて、カスタムAndroidViewModelコンストラクターに追加の引数を渡す方法はありますか?
アダムHurwitz

カスタムビューモデルコンストラクターで任意の引数/パラメーターを渡すことができます。ここでのコンテキストは単なる例です。コンストラクタで任意のカスタム引数を渡すことができます。
ドゥルミルシャー

わかった。コンテキスト、ビュー、アクティビティ、フラグメント、アダプター、ビューライフサイクルを渡さないこと、ビューライフサイクル対応のオブザーバブルを観察すること、またはビューが破棄され、ViewModelが古くなったままになるため、ViewModelでリソース(ドローアブルなど)を保持することをお勧めします。情報。
Adam Hurwitz
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.