Zusätzliche Argumente für Android ViewModel

106

Gibt es eine Möglichkeit, meinem benutzerdefinierten AndroidViewModelKonstruktor außer dem Anwendungskontext zusätzliche Argumente zu übergeben? Beispiel:

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

Und wenn ich meine benutzerdefinierte ViewModelKlasse verwenden möchte, verwende ich diesen Code in meinem Fragment:

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

Ich weiß also nicht, wie ich zusätzliche Argumente String paramin meinen Brauch übergeben soll ViewModel. Ich kann nur den Anwendungskontext übergeben, aber keine zusätzlichen Argumente. Ich würde mich über jede Hilfe sehr freuen. Danke dir.

Bearbeiten: Ich habe Code hinzugefügt. Ich hoffe es ist jetzt besser.

Mario Rudman
quelle
Weitere Details und Code hinzufügen
Hugo
Was ist die Fehlermeldung?
Moses Aprico
Es gibt keine Fehlermeldung. Ich weiß einfach nicht, wo ich Argumente für den Konstruktor festlegen soll, da ViewModelProvider zum Erstellen von AndroidViewModel-Objekten verwendet wird.
Mario Rudman

Antworten:

211

Sie benötigen eine Factory-Klasse für Ihr 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);
    }
}

Und wenn Sie das Ansichtsmodell instanziieren, gefällt Ihnen Folgendes:

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

Für Kotlin können Sie delegierte Eigenschaften verwenden:

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

Es gibt noch eine weitere neue Option: Implementieren HasDefaultViewModelProviderFactoryund Überschreiben getDefaultViewModelProviderFactory()mit der Instanziierung Ihrer Fabrik und dann würden Sie anrufen ViewModelProvider(this)oder by viewModels()ohne die Fabrik.

Mlyko
quelle
4
ViewModelBenötigt jede Klasse ihre ViewModelFactory?
Dmlebron
6
aber jeder ViewModelkönnte / wird einen anderen DI haben. Wie würden Sie wissen, welche Instanz die create()Methode zurückgibt?
Dmlebron
1
Ihr ViewModel wird nach Änderung der Ausrichtung neu erstellt. Sie können nicht jedes Mal eine Fabrik erstellen.
Tim
3
Das ist nicht wahr. Neue ViewModelErstellung verhindert Methode get(). Basierend auf der Dokumentation: "Gibt ein vorhandenes ViewModel zurück oder erstellt ein neues im Bereich (normalerweise ein Fragment oder eine Aktivität), das diesem ViewModelProvider zugeordnet ist." siehe: developer.android.com/reference/android/arch/lifecycle/…
mlyko
2
Wie wäre es mit return modelClass.cast(new MyViewModel(mApplication, mParam)), um die Warnung loszuwerden
Jackycflau
22

Implementieren Sie mit Abhängigkeitsinjektion

Dies ist weiter fortgeschritten und besser für den Produktionscode.

Dagger2 , das AssistedInject von Square, bietet eine produktionsbereite Implementierung für ViewModels, mit der erforderliche Komponenten wie ein Repository, das Netzwerk- und Datenbankanforderungen verarbeitet, eingefügt werden können. Es ermöglicht auch die manuelle Einfügung von Argumenten / Parametern in die Aktivität / das Fragment. Hier ist ein kurzer Überblick über die Schritte, die mit Code Gists implementiert werden müssen, basierend auf Gabor Varadis detailliertem Beitrag Dagger Tips .

Dagger Hilt ist die Lösung der nächsten Generation in Alpha ab dem 12.07.20. Sie bietet denselben Anwendungsfall mit einer einfacheren Einrichtung, sobald sich die Bibliothek im Release-Status befindet.

Implementieren Sie mit Lifecycle 2.2.0 in Kotlin

Argumente / Parameter übergeben

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

Aktivieren von SavedState mit Argumenten / Parametern

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
quelle
Während ich create in the factory überschreibe, erhalte ich eine Warnung mit der Aufschrift "ItemViewModel to T"
Ssenyonjo
1
Diese Warnung war für mich bisher kein Thema. Ich werde mich jedoch weiter damit befassen, wenn ich die ViewModel-Factory so umgestalte, dass sie mit Dagger injiziert wird, anstatt eine Instanz davon über das Fragment zu erstellen.
Adam Hurwitz
15

Für eine Fabrik, die von mehreren verschiedenen Ansichtsmodellen gemeinsam genutzt wird, würde ich die Antwort von mlyko folgendermaßen erweitern:

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

Und Ansichtsmodelle instanziieren:

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

Mit unterschiedlichen Ansichtsmodellen mit unterschiedlichen Konstruktoren.

rzehan
quelle
8
Ich empfehle diesen Weg aus mehreren Gründen nicht: 1) Parameter in der Fabrik sind nicht typsicher - auf diese Weise können Sie Ihren Code zur Laufzeit brechen. Versuchen Sie immer, diesen Ansatz nach Möglichkeit zu vermeiden. 2) Das Überprüfen der Ansichtsmodelltypen ist keine echte OOP-Methode. Da die ViewModels in den Basistyp umgewandelt werden, können Sie den Code zur Laufzeit ohne Warnung während der Kompilierung erneut unterbrechen. In diesem Fall würde ich empfehlen, die Standard-Android-Factory zu verwenden und die Parameter an das bereits instanziierte Ansichtsmodell zu übergeben.
Mlyko
@mlyko Sicher, dies sind alles gültige Einwände und eigene Methoden zum Einrichten der Viewmodel-Daten sind immer eine Option. Manchmal möchten Sie jedoch sicherstellen, dass das Ansichtsmodell initialisiert wurde, daher die Verwendung des Konstruktors. Andernfalls müssen Sie die Situation "Ansichtsmodell noch nicht initialisiert" selbst behandeln. Wenn viewmodel beispielsweise über Methoden verfügt, die LivedData zurückgeben, und Beobachter in verschiedenen View-Lebenszyklusmethoden an diese angehängt sind.
Rzehan
3

Basierend auf @ vilpe89 die obige Kotlin-Lösung für AndroidViewModel-Fälle

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

}}

Dann kann ein Fragment das viewModel als initiieren

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

Und dann die eigentliche ViewModel-Klasse

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

Oder in einer geeigneten Methode ...

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

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

    ....
}
MFAL
quelle
In der Frage wird gefragt, wie Argumente / Parameter ohne Verwendung des Kontexts übergeben werden sollen, dem oben nicht folgt: Gibt es eine Möglichkeit, zusätzliche Argumente an meinen benutzerdefinierten AndroidViewModel-Konstruktor mit Ausnahme des Anwendungskontexts zu übergeben?
Adam Hurwitz
3

Ich habe es zu einer Klasse gemacht, in der das bereits erstellte Objekt übergeben wird.

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

Und dann

ViewModelFactory viewModelFactory = new ViewModelFactory();
viewModelFactory.add(new SampleViewModel(arg1, arg2));
SampleViewModel sampleViewModel = ViewModelProviders.of(this, viewModelFactory).get(SampleViewModel.class);
Danil
quelle
Wir sollten eine ViewModelFactory für jedes ViewModel haben, um die Parameter an den Konstruktor zu übergeben?
K Pradeep Kumar Reddy
Nein. Nur eine ViewModelFactory für alle ViewModels
Danil
Gibt es einen Grund, den kanonischen Namen als HashMap-Schlüssel zu verwenden? Kann ich class.simpleName verwenden?
K Pradeep Kumar Reddy
Ja, aber Sie müssen sicherstellen, dass es keine doppelten Namen gibt
Danil
Ist dies der empfohlene Stil zum Schreiben des Codes? Sie haben diesen Code selbst erfunden oder in Android-Dokumenten gelesen?
K Pradeep Kumar Reddy
1

Ich habe eine Bibliothek geschrieben, die dies einfacher und sauberer machen soll, ohne dass Multibindings oder Factory Boilerplate erforderlich sind, während ich nahtlos mit ViewModel-Argumenten arbeite, die von Dagger als Abhängigkeiten bereitgestellt werden können: 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"
    }    
}

In der Ansicht:

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
quelle
0

(KOTLIN) Meine Lösung verwendet ein wenig Reflexion.

Nehmen wir an, Sie möchten nicht jedes Mal dieselbe Factory-Klasse erstellen, wenn Sie eine neue ViewModel-Klasse erstellen, für die einige Argumente erforderlich sind. Sie können dies über Reflection erreichen.

Zum Beispiel hätten Sie zwei verschiedene Aktivitäten:

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

Und ViewModels für diese Aktivitäten:

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

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

Dann der magische Teil, die Implementierung der Factory-Klasse:

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
quelle
0

Warum nicht so machen:

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

und verwenden Sie es dann in zwei Schritten:

MyViewModel myViewModel = ViewModelProvider.of(this).get(MyViewModel.class)
myViewModel.initialize(param)
Amr Berag
quelle
2
Der springende Punkt beim Einfügen von Parametern in den Konstruktor besteht darin, das Ansichtsmodell nur einmal zu initialisieren . Mit der Implementierung, wenn Sie rufen myViewModel.initialize(param)in onCreateder Aktivität, zum Beispiel, kann es mehrere Male auf der gleichen aufgerufen wird MyViewModelInstanz wie der Benutzer das Gerät dreht.
Sanlok Lee
@ Sanlok Lee Ok. Wie wäre es, wenn Sie der Funktion eine Bedingung hinzufügen, um zu verhindern, dass sie initialisiert wird, wenn dies nicht erforderlich ist. Überprüfen Sie meine bearbeitete Antwort.
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
    }

Rufen Sie Viewmodel in Activity auf

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

Weitere Informationen: Android MVVM Kotlin Beispiel

Dhrumil Shah
quelle
In der Frage wird gefragt, wie Argumente / Parameter ohne Verwendung des Kontexts übergeben werden sollen, dem oben nicht folgt: Gibt es eine Möglichkeit, zusätzliche Argumente an meinen benutzerdefinierten AndroidViewModel-Konstruktor mit Ausnahme des Anwendungskontexts zu übergeben?
Adam Hurwitz
Sie können jedes Argument / jeden Parameter in Ihrem benutzerdefinierten Viewmodel-Konstruktor übergeben. Hier ist der Kontext nur ein Beispiel. Sie können jedes benutzerdefinierte Argument im Konstruktor übergeben.
Dhrumil Shah
Verstanden. Es wird empfohlen, Kontext, Ansichten, Aktivitäten, Fragmente, Adapter, Ansichtslebenszyklus, Beobachtungslebenszyklus-fähige Observablen nicht zu übergeben oder Ressourcen (Drawables usw.) im ViewModel zu speichern, da die Ansicht möglicherweise zerstört wird und das ViewModel veraltet bleibt Information.
Adam Hurwitz