Arguments supplémentaires Android ViewModel

111

Existe-t-il un moyen de passer un argument supplémentaire à mon AndroidViewModelconstructeur personnalisé à l' exception du contexte d'application. Exemple:

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);
    }
}

Et quand je veux utiliser ma ViewModelclasse personnalisée , j'utilise ce code dans mon fragment:

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

Donc je ne sais pas comment passer un argument supplémentaire String paramdans ma coutume ViewModel. Je ne peux passer que le contexte d'application, mais pas d'arguments supplémentaires. J'apprécierais vraiment n'importe quelle aide. Je vous remercie.

Edit: j'ai ajouté du code. J'espère que c'est mieux maintenant.

Mario Rudman
la source
ajouter plus de détails et code
hugo
Quel est le message d'erreur?
Moses Aprico
Il n'y a pas de message d'erreur. Je ne sais tout simplement pas où définir les arguments du constructeur car ViewModelProvider est utilisé pour créer des objets AndroidViewModel.
Mario Rudman

Réponses:

217

Vous devez avoir une classe d'usine pour votre 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);
    }
}

Et lors de l'instanciation du modèle de vue, procédez comme suit:

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

Pour kotlin, vous pouvez utiliser la propriété déléguée:

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

Il y a aussi une autre nouvelle option - pour implémenter HasDefaultViewModelProviderFactoryet remplacer getDefaultViewModelProviderFactory()avec l'instanciation de votre usine, puis vous appelleriez ViewModelProvider(this)ou by viewModels()sans l'usine.

Mlyko
la source
4
Chaque ViewModelclasse a- t-elle besoin de son ViewModelFactory?
dmlebron
6
mais chacun ViewModelpourrait / aura un DI différent. Comment sauriez-vous quelle instance retourne sur la create()méthode?
dmlebron
1
Votre ViewModel sera recréé après le changement d'orientation. Votre ne peut pas créer d'usine à chaque fois.
Tim
3
Ce n'est pas vrai. La nouvelle ViewModelcréation empêche la méthode get(). D'après la documentation: «Renvoie un ViewModel existant ou en crée un nouveau dans l'étendue (généralement, un fragment ou une activité), associé à ce ViewModelProvider.» voir: developer.android.com/reference/android/arch/lifecycle/…
mlyko
2
que diriez-vous d'utiliser return modelClass.cast(new MyViewModel(mApplication, mParam))pour se débarrasser de l'avertissement
jackycflau
23

Implémenter avec l'injection de dépendances

C'est plus avancé et meilleur pour le code de production.

Dagger2 , Square's AssistedInject offre une implémentation prête pour la production pour ViewModels qui peut injecter les composants nécessaires tels qu'un référentiel qui gère les demandes de réseau et de base de données. Il permet également l'injection manuelle d'arguments / paramètres dans l'activité / le fragment. Voici un aperçu concis des étapes à mettre en œuvre avec le code Gists basé sur l'article détaillé de Gabor Varadi, Dagger Tips .

Dagger Hilt , est la solution de nouvelle génération, en version alpha à partir du 12/07/20, offrant le même cas d'utilisation avec une configuration plus simple une fois que la bibliothèque est en état de sortie.

Implémenter avec Lifecycle 2.2.0 dans Kotlin

Passer des arguments / paramètres

// 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") } 
}

Activation de SavedState avec des arguments / paramètres

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
    }
}
Adam Hurwitz
la source
En écrasant create dans l'usine, je reçois un avertissement disant Cast non coché 'ItemViewModel to T'
Ssenyonjo
1
Cet avertissement n'a pas été un problème pour moi jusqu'à présent. Cependant, je l'examinerai plus en détail lorsque je refactoriserai l'usine ViewModel pour l'injecter à l'aide de Dagger plutôt que d'en créer une instance via le fragment.
Adam Hurwitz
15

Pour une usine partagée entre plusieurs modèles de vue différents, j'étendrais la réponse de mlyko comme suit:

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);
        }
    }
}

Et instancier des modèles de vue:

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);

Avec différents modèles de vue ayant différents constructeurs.

rzehan
la source
9
Je ne recommande pas cette façon pour deux raisons: 1) les paramètres d'usine ne sont pas de type sûr - de cette façon, vous pouvez casser votre code à l'exécution. Essayez toujours d'éviter cette approche lorsque cela est possible 2) vérifier les types de modèles de vue n'est pas vraiment une façon de faire les choses. Étant donné que les ViewModels sont convertis en type de base, vous pouvez à nouveau casser le code pendant l'exécution sans aucun avertissement lors de la compilation. Dans ce cas, je suggérerais d'utiliser l'usine Android par défaut et de transmettre les paramètres au modèle de vue déjà instancié.
mlyko
@mlyko Bien sûr, ce sont toutes des objections valides et la ou les méthodes propres à configurer les données du viewmodel sont toujours une option. Mais parfois, vous voulez vous assurer que viewmodel a été initialisé, d'où l'utilisation de constructor. Sinon, vous devez vous-même gérer la situation "viewmodel pas encore initialisé". Par exemple, si viewmodel a des méthodes qui renvoient LivedData et que les observateurs y sont attachés dans diverses méthodes de cycle de vie de View.
rzehan
3

Basé sur @ vilpe89, la solution Kotlin ci-dessus pour les cas AndroidViewModel

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

}

Ensuite, un fragment peut lancer le viewModel comme

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

Et puis la classe ViewModel réelle

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

Ou dans une méthode appropriée ...

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

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

    ....
}
MFAL
la source
La question demande comment passer des arguments / paramètres sans utiliser de contexte que ce qui précède ne suit pas: existe-t-il un moyen de passer un argument supplémentaire à mon constructeur AndroidViewModel personnalisé à l'exception du contexte d'application?
Adam Hurwitz le
3

J'en ai fait une classe dans laquelle l'objet déjà créé est passé.

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;
}

Puis

ViewModelFactory viewModelFactory = new ViewModelFactory();
viewModelFactory.add(new SampleViewModel(arg1, arg2));
SampleViewModel sampleViewModel = ViewModelProviders.of(this, viewModelFactory).get(SampleViewModel.class);
Danil
la source
Nous devrions avoir un ViewModelFactory pour chaque ViewModel pour passer les paramètres au constructeur ??
K Pradeep Kumar Reddy
Non. Un seul ViewModelFactory pour tous les ViewModels
Danil
Y a-t-il une raison d'utiliser le nom canonique comme clé hashMap? Puis-je utiliser class.simpleName?
K Pradeep Kumar Reddy
Oui, mais vous devez vous assurer qu'il n'y a pas de noms en double
Danil
Est-ce le style recommandé pour écrire le code? Vous avez créé ce code vous-même ou vous l'avez lu dans la documentation Android?
K Pradeep Kumar Reddy le
1

J'ai écrit une bibliothèque qui devrait rendre cela plus simple et plus propre, pas de multibindings ou de passe-partout d'usine nécessaire, tout en travaillant de manière transparente avec les arguments ViewModel qui peuvent être fournis en tant que dépendances par Dagger: 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"
    }    
}

Dans la vue:

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
        })
    }
}
Radu Topor
la source
1

(KOTLIN) Ma solution utilise un peu de réflexion.

Disons que vous ne voulez pas créer la même classe Factory à chaque fois que vous créez une nouvelle classe ViewModel qui a besoin de quelques arguments. Vous pouvez accomplir cela via Reflection.

Par exemple, vous auriez deux activités différentes:

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)
    }
}

Et ViewModels pour ces activités:

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

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

Puis la partie magique, l'implémentation de la classe 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
        }
    }
}
vilpe89
la source
0

Pourquoi ne pas le faire comme ça:

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);
    }
   }
  }
}

puis utilisez-le comme ceci en deux étapes:

MyViewModel myViewModel = ViewModelProvider.of(this).get(MyViewModel.class)
myViewModel.initialize(param)
Amr Berag
la source
2
L'intérêt de mettre des paramètres dans le constructeur est d'initialiser le modèle de vue une seule fois . Avec l' implémentation, si vous appelez myViewModel.initialize(param)à onCreatel'activité, par exemple, il peut être appelé plusieurs fois sur le même MyViewModelexemple que l'utilisateur fait tourner le dispositif.
Sanlok Lee
@Sanlok Lee Ok. Que diriez-vous d'ajouter une condition à la fonction pour empêcher l'initialisation lorsqu'elle n'est pas nécessaire. Vérifiez ma réponse modifiée.
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
    }

Appeler Viewmodel en activité

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

Pour plus de référence: Exemple Android MVVM Kotlin

Dhrumil Shah
la source
La question demande comment passer des arguments / paramètres sans utiliser de contexte que ce qui précède ne suit pas: existe-t-il un moyen de passer un argument supplémentaire à mon constructeur AndroidViewModel personnalisé à l'exception du contexte d'application?
Adam Hurwitz le
Vous pouvez transmettre n'importe quel argument / paramètre dans votre constructeur de modèle de vue personnalisé. Ici, le contexte n'est qu'un exemple. Vous pouvez passer n'importe quel argument personnalisé dans le constructeur.
Dhrumil Shah le
Compris. Il est recommandé de ne pas transmettre le contexte, les vues, les activités, les fragments, les adaptateurs, de visualiser le cycle de vie, d'observer les observables tenant compte du cycle de vie des vues ou de conserver les ressources (dessinables, etc.) dans le ViewModel car la vue peut être détruite et le ViewModel persistera avec information.
Adam Hurwitz le