Argomenti aggiuntivi di Android ViewModel


108

C'è un modo per passare argomenti aggiuntivi al mio AndroidViewModelcostruttore personalizzato eccetto il contesto dell'applicazione. Esempio:

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

E quando voglio utilizzare la mia ViewModelclasse personalizzata , utilizzo questo codice nel mio frammento:

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

Quindi non so come passare argomenti aggiuntivi String paramnella mia abitudine ViewModel. Posso solo passare il contesto dell'applicazione, ma non argomenti aggiuntivi. Apprezzerei davvero qualsiasi aiuto. Grazie.

Modifica: ho aggiunto del codice. Spero sia meglio adesso.


aggiungi più dettagli e codice
hugo

Qual è il messaggio di errore?
Moses Aprico

Nessun messaggio di errore. Semplicemente non so dove impostare gli argomenti per il costruttore poiché ViewModelProvider viene utilizzato per creare oggetti AndroidViewModel.
Mario Rudman

Risposte:


214

Devi avere una classe di fabbrica per il tuo 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);
    }
}

E quando crei un'istanza del modello di visualizzazione, ti piace:

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

Per kotlin, puoi utilizzare la proprietà delegata:

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

C'è anche un'altra nuova opzione: implementare HasDefaultViewModelProviderFactorye sovrascrivere getDefaultViewModelProviderFactory()con l'istanza della tua fabbrica e poi chiameresti ViewModelProvider(this)o by viewModels()senza la fabbrica.


4
Ogni ViewModelclasse ha bisogno del proprio ViewModelFactory?
dmlebron

6
ma ognuno ViewModelpotrebbe / avrà un DI diverso. Come fai a sapere quale istanza restituisce il create()metodo?
dmlebron

1
Il tuo ViewModel verrà ricreato dopo la modifica dell'orientamento. Non puoi creare una fabbrica ogni volta.
Tim

3
Non è vero. La nuova ViewModelcreazione impedisce il metodo get(). In base alla documentazione: "Restituisce un ViewModel esistente o ne crea uno nuovo nell'ambito (di solito, un frammento o un'attività), associato a questo ViewModelProvider." vedi: developer.android.com/reference/android/arch/lifecycle/…
mlyko

2
che ne dici di usare return modelClass.cast(new MyViewModel(mApplication, mParam))per sbarazzarti dell'avvertimento
jackycflau

23

Implementa con Dependency Injection

Questo è più avanzato e migliore per il codice di produzione.

Dagger2 , AssistedInject di Square offre un'implementazione pronta per la produzione per ViewModels che può iniettare i componenti necessari come un repository che gestisce le richieste di rete e database. Consente inoltre l'inserimento manuale di argomenti / parametri nell'attività / frammento. Ecco uno schema conciso dei passaggi da implementare con i Gists di codice basati sul post dettagliato di Gabor Varadi, Dagger Tips .

Dagger Hilt , è la soluzione di nuova generazione, in alpha dal 12/07/20, che offre lo stesso caso d'uso con una configurazione più semplice una volta che la libreria è in stato di rilascio.

Implementa con Lifecycle 2.2.0 in Kotlin

Passaggio di argomenti / parametri

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

Abilitazione di SavedState con argomenti / parametri

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

Durante l'override di create in fabbrica, ricevo un avviso che dice Cast non controllato "ItemViewModel to T"
Ssenyonjo

1
Finora questo avvertimento non è stato un problema per me. Tuttavia, lo esaminerò ulteriormente quando refactoring la fabbrica ViewModel per iniettarlo utilizzando Dagger piuttosto che crearne un'istanza tramite il frammento.
Adam Hurwitz

15

Per una fabbrica condivisa tra più modelli di visualizzazione diversi, estenderei la risposta di mlyko in questo modo:

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

E istanziare i modelli di visualizzazione:

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

Con diversi modelli di visualizzazione con costruttori diversi.


8
Non consiglio questo modo perché un paio di ragioni: 1) i parametri in fabbrica non sono indipendenti dai tipi - in questo modo puoi rompere il tuo codice in runtime. Cerca sempre di evitare questo approccio quando possibile 2) controllare i tipi di modelli di visualizzazione non è davvero un modo OOP di fare le cose. Dal momento che i ViewModels sono sottoposti a cast sul tipo di base, ancora una volta puoi rompere il codice durante il runtime senza alcun avviso durante la compilazione .. In questo caso suggerirei di utilizzare la fabbrica Android predefinita e passare i parametri al modello di visualizzazione già istanziato.
mlyko

@mlyko Certo, queste sono tutte obiezioni valide e il proprio metodo (i) per impostare i dati del viewmodel è sempre un'opzione. Ma a volte vuoi assicurarti che viewmodel sia stato inizializzato, da qui l'uso di constructor. Altrimenti devi gestire tu stesso la situazione "viewmodel non ancora inizializzato". Ad esempio, se viewmodel ha metodi che restituiscono LivedData e gli osservatori sono collegati a quello in vari metodi del ciclo di vita di visualizzazione.
rzehan

3

Basato su @ vilpe89, la soluzione Kotlin sopra per i casi 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

}

Quindi un frammento può avviare viewModel come

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

E poi l'attuale classe ViewModel

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

O in qualche metodo adatto ...

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

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

    ....
}

La domanda chiede come passare argomenti / parametri senza utilizzare il contesto che quanto sopra non segue: c'è un modo per passare argomenti aggiuntivi al mio costruttore AndroidViewModel personalizzato eccetto il contesto dell'applicazione?
Adam Hurwitz

3

Ne ho fatto una classe in cui viene passato l'oggetto già creato.

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

E poi

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

Dovremmo avere un ViewModelFactory per ogni ViewModel per passare i parametri al costruttore ??
K Pradeep Kumar Reddy,

No. Solo un ViewModelFactory per tutti i ViewModels
Danil

C'è qualche motivo per usare il nome canonico come chiave hashMap? Posso usare class.simpleName?
K Pradeep Kumar Reddy,

Sì, ma devi assicurarti che non ci siano nomi duplicati
Danil

È questo lo stile consigliato per scrivere il codice? Hai inventato questo codice da solo o l'hai letto nei documenti Android?
K Pradeep Kumar Reddy,

1

Ho scritto una libreria che dovrebbe rendere questa operazione più semplice e pulita, senza multibinding o boilerplate di fabbrica necessari, mentre lavoravo perfettamente con gli argomenti ViewModel che possono essere forniti come dipendenze da 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"
    }    
}

Nella vista:

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

1

(KOTLIN) La mia soluzione utilizza un po 'di riflessione.

Diciamo che non si desidera creare la stessa classe Factory dall'aspetto ogni volta che si crea una nuova classe ViewModel che necessita di alcuni argomenti. Puoi farlo tramite Reflection.

Ad esempio avresti due diverse attività:

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

E ViewModels per queste attività:

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

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

Quindi la parte magica, l'implementazione della 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
        }
    }
}

0

Perché non farlo in questo modo:

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

e poi usalo in questo modo in due passaggi:

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

2
Il punto centrale dell'inserimento dei parametri nel costruttore è inizializzare il modello di visualizzazione solo una volta . Con l'implementazione, se si chiama myViewModel.initialize(param)in onCreatedell'attività, per esempio, può essere chiamato più volte sulla stessa MyViewModelistanza come l'utente ruota il dispositivo.
Sanlok Lee il

@Sanlok Lee Ok. Che ne dici di aggiungere una condizione alla funzione per impedire l'inizializzazione quando non è necessaria. Controlla la mia risposta modificata.
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
    }

Chiama Viewmodel in Activity

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

Per ulteriori riferimenti: Android MVVM Kotlin Example


La domanda chiede come passare argomenti / parametri senza utilizzare il contesto che quanto sopra non segue: c'è un modo per passare argomenti aggiuntivi al mio costruttore AndroidViewModel personalizzato eccetto il contesto dell'applicazione?
Adam Hurwitz

È possibile passare qualsiasi argomento / parametro nel costruttore del viewmodel personalizzato. Qui il contesto è solo un esempio. Puoi passare qualsiasi argomento personalizzato nel costruttore.
Dhrumil Shah

Inteso. È buona norma non passare contesto, viste, attività, frammenti, adattatori, visualizzare il ciclo di vita, osservare osservabili osservabili in base al ciclo di vita della vista o conservare risorse (disegnabili, ecc.) Nel ViewModel poiché la vista potrebbe essere distrutta e il ViewModel persisterà con obsoleti informazione.
Adam Hurwitz
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.