Dodatkowe argumenty ViewModel systemu Android

111

Czy istnieje sposób przekazania dodatkowego argumentu do mojego AndroidViewModelkonstruktora niestandardowego z wyjątkiem kontekstu aplikacji. Przykład:

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

A kiedy chcę ViewModelużyć mojej niestandardowej klasy, używam tego kodu w moim fragmencie:

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

Więc nie wiem, jak przekazać dodatkowy argument String paramdo mojego niestandardowego ViewModel. Mogę przekazać tylko kontekst aplikacji, ale nie dodatkowe argumenty. Naprawdę doceniłbym każdą pomoc. Dziękuję Ci.

Edycja: dodałem kod. Mam nadzieję, że teraz jest lepiej.

Mario Rudman
źródło
dodaj więcej szczegółów i kod
hugo
Jaki jest komunikat o błędzie?
Moses Aprico
Brak komunikatu o błędzie. Po prostu nie wiem, gdzie ustawić argumenty dla konstruktora, ponieważ ViewModelProvider służy do tworzenia obiektów AndroidViewModel.
Mario Rudman

Odpowiedzi:

217

Musisz mieć klasę fabryczną dla swojego 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);
    }
}

A podczas tworzenia wystąpienia modelu widoku postępujesz w następujący sposób:

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

W przypadku kotlin możesz użyć właściwości delegowanej:

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

Jest też inna nowa opcja - aby zaimplementować HasDefaultViewModelProviderFactoryi nadpisać getDefaultViewModelProviderFactory()za pomocą instancji fabryki, a następnie zadzwonić do fabryki ViewModelProvider(this)lub by viewModels()bez niej.

mlyko
źródło
4
Czy każda ViewModelklasa potrzebuje swojego ViewModelFactory?
dmlebron
6
ale każdy ViewModelmoże / będzie miał inny DI. Skąd wiesz, które wystąpienie powróci do create()metody?
dmlebron
1
Twój ViewModel zostanie odtworzony po zmianie orientacji. Nie możesz za każdym razem tworzyć fabryki.
Tim
3
To nieprawda. Nowe ViewModelstworzenie zapobiega metodzie get(). Na podstawie dokumentacji: „Zwraca istniejący ViewModel lub tworzy nowy w zakresie (zwykle fragment lub działanie), powiązany z tym ViewModelProvider.” zobacz: developer.android.com/reference/android/arch/lifecycle/…
mlyko
2
co powiesz na użycie, return modelClass.cast(new MyViewModel(mApplication, mParam))aby pozbyć się ostrzeżenia
jackycflau
23

Zaimplementuj z iniekcją zależności

Jest to bardziej zaawansowane i lepsze dla kodu produkcyjnego.

Dagger2 , Square's AssistedInject oferuje gotową do produkcji implementację ViewModels, która może wprowadzać niezbędne komponenty, takie jak repozytorium obsługujące żądania sieciowe i bazy danych. Pozwala również na ręczne wprowadzanie argumentów / parametrów w aktywności / fragmencie. Oto zwięzły zarys kroków do wdrożenia z kodem Gists na podstawie szczegółowego postu Gabor Varadi, Dagger Tips .

Dagger Hilt to rozwiązanie nowej generacji, w wersji alfa od 12.07.2020, oferujące ten sam przypadek użycia z prostszą konfiguracją, gdy biblioteka jest w stanie wydania.

Wdrożenie z Lifecycle 2.2.0 w Kotlin

Przekazywanie argumentów / parametrów

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

Włączanie SavedState z argumentami / parametrami

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
źródło
Podczas zastępowania tworzenia w fabryce otrzymuję ostrzeżenie z informacją, że Unchecked cast „ItemViewModel to T”
Ssenyonjo
1
Jak dotąd to ostrzeżenie nie stanowiło dla mnie problemu. Jednak przyjrzę się temu dokładniej, gdy refaktoryzuję fabrykę ViewModel, aby wstrzyknąć ją za pomocą Daggera, zamiast tworzyć jej instancję za pośrednictwem fragmentu.
Adam Hurwitz
15

W przypadku jednej fabryki współdzielonej przez wiele różnych modeli widoku rozszerzyłbym odpowiedź mlyko w ten sposób:

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

I tworzenie instancji modeli widoków:

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

Z różnymi modelami widoków posiadającymi różnych konstruktorów.

rzehan
źródło
9
Nie polecam tego sposobu, ponieważ jest kilka powodów: 1) parametry w fabryce nie są bezpieczne dla typu - w ten sposób możesz złamać kod w czasie wykonywania. Zawsze staraj się unikać tego podejścia, jeśli to możliwe. 2) sprawdzanie typów modeli widoków nie jest tak naprawdę sposobem OOP. Ponieważ ViewModels są rzutowane na typ podstawowy, ponownie możesz złamać kod w czasie wykonywania bez żadnego ostrzeżenia podczas kompilacji. W tym przypadku sugerowałbym użycie domyślnej fabryki Androida i przekazanie parametrów do już utworzonego modelu widoku.
mlyko
@mlyko Jasne, to są wszystkie ważne zastrzeżenia, a własne metody konfiguracji danych modelu widoku są zawsze dostępne. Ale czasami chcesz się upewnić, że viewmodel został zainicjowany, stąd użycie konstruktora. W przeciwnym razie musisz sobie poradzić z sytuacją „model widoku jeszcze nie zainicjowany”. Na przykład, jeśli Viewmodel ma metody, które zwracają LivedData i obserwatory są do nich dołączone w różnych metodach cyklu życia View.
rzehan
3

W oparciu o @ vilpe89 powyższe rozwiązanie Kotlin dla przypadków 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

}

Następnie fragment może zainicjować viewModel jako

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

A następnie rzeczywista klasa ViewModel

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

Lub jakąś odpowiednią metodą ...

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

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

    ....
}
MFAL
źródło
Pytanie dotyczy sposobu przekazywania argumentów / parametrów bez użycia kontekstu, którego nie widać
Adam Hurwitz
3

Zrobiłem z tego klasę, w której przekazywany jest już utworzony obiekt.

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

I wtedy

ViewModelFactory viewModelFactory = new ViewModelFactory();
viewModelFactory.add(new SampleViewModel(arg1, arg2));
SampleViewModel sampleViewModel = ViewModelProviders.of(this, viewModelFactory).get(SampleViewModel.class);
Danil
źródło
Powinniśmy mieć ViewModelFactory dla każdego ViewModel, aby przekazać parametry do konstruktora?
K Pradeep Kumar Reddy
Nie. Tylko jeden ViewModelFactory dla wszystkich ViewModels
Danil
Czy jest jakiś powód, aby używać nazwy kanonicznej jako klucza mapy mieszania? Czy mogę używać class.simpleName?
K Pradeep Kumar Reddy
Tak, ale musisz się upewnić, że nie ma zduplikowanych nazwisk
Danil
Czy to zalecany styl pisania kodu? Wymyśliłeś ten kod samodzielnie, czy czytasz go w Android Docs?
K Pradeep Kumar Reddy
1

Napisałem bibliotekę, która powinna uczynić to bardziej prostym i czystszym, bez konieczności stosowania wielu powiązań ani schematu fabrycznego, jednocześnie bezproblemowo pracując z argumentami ViewModel, które mogą być dostarczane jako zależności przez 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"
    }    
}

W widoku:

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
źródło
1

(KOTLIN) Moje rozwiązanie wykorzystuje odrobinę Refleksji.

Powiedzmy, że nie chcesz tworzyć tej samej wyglądającej klasy Factory za każdym razem, gdy tworzysz nową klasę ViewModel, która wymaga pewnych argumentów. Możesz to osiągnąć poprzez refleksję.

Na przykład miałbyś dwie różne aktywności:

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

I modele widoków dla tych działań:

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

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

Następnie część magiczna, implementacja klasy 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
źródło
0

Dlaczego nie zrobić tego w ten sposób:

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

a następnie użyj go w ten sposób w dwóch krokach:

MyViewModel myViewModel = ViewModelProvider.of(this).get(MyViewModel.class)
myViewModel.initialize(param)
Amr Berag
źródło
2
Cały sens umieszczania parametrów w konstruktorze polega na jednokrotnej inicjalizacji modelu widoku . Dzięki implementacji, jeśli zadzwonisz myViewModel.initialize(param)w onCreateaktywności, na przykład, może być wywołana wiele razy na tym samym MyViewModelprzykład, gdy użytkownik obraca urządzenie.
Sanlok Lee
@Sanlok Lee Ok. Co powiesz na dodanie warunku do funkcji, aby zapobiec inicjalizacji, gdy nie jest to konieczne. Sprawdź moją zredagowaną odpowiedź.
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
    }

Wywołaj Viewmodel w działaniu

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

Więcej informacji: Przykład systemu Android MVVM Kotlin

Dhrumil Shah
źródło
Pytanie dotyczy sposobu przekazywania argumentów / parametrów bez użycia kontekstu, którego nie widać
Adam Hurwitz
Możesz przekazać dowolny argument / parametr w swoim niestandardowym konstruktorze Viewmodel. Tutaj kontekst jest tylko przykładem. W konstruktorze można przekazać dowolny argument niestandardowy.
Dhrumil Shah
Zrozumiany. Najlepszą praktyką jest nieprzekazywanie kontekstu, widoków, działań, fragmentów, adapterów, przeglądanie cyklu życia, obserwowanie widoku obserwowalnych uwzględniających cykl życia lub wstrzymywanie zasobów (rysunki itp.) W ViewModel, ponieważ widok może zostać zniszczony, a ViewModel pozostanie nieaktualny Informacja.
Adam Hurwitz,