Додаткові аргументи Android ViewModel


108

Чи є спосіб передати додатковий аргумент моєму користувацькому 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

Яке повідомлення про помилку?
Moses Aprico

Повідомлення про помилку відсутнє. Я просто не знаю, де встановлювати аргументи для конструктора, оскільки ViewModelProvider використовується для створення об’єктів AndroidViewModel.
Маріо Рудман,

Відповіді:


214

Для 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") }

Також є ще один новий варіант - реалізувати HasDefaultViewModelProviderFactoryта перевизначити getDefaultViewModelProviderFactory()інстанціювання вашої фабрики, і тоді ви зателефонуєте ViewModelProvider(this)або by viewModels()без фабрики.


4
Чи кожен ViewModelклас потребує свого ViewModelFactory?
dmlebron

6
але кожен ViewModelможе / буде мати різні DI. Як би ви знали, який екземпляр повертає create()метод?
dmlebron

1
Ваш ViewModel буде відтворений після зміни орієнтації. Ви не можете створювати фабрики щоразу.
Тім,

3
Це не правда. Новий ViewModelметод запобігає створенню get(). На підставі документації: "Повертає існуючий ViewModel або створює новий у межах (зазвичай, фрагмент або діяльність), пов'язаний з цим ViewModelProvider." див .: developer.android.com/reference/android/arch/lifecycle/…
mlyko

2
як щодо використання, return modelClass.cast(new MyViewModel(mApplication, mParam))щоб позбутися попередження
jackycflau

23

Реалізувати з ін’єкцією залежності

Це більш просунутий і кращий для виробничого коду.

Dagger2 , Square's AssistedInject пропонує готову до реалізації програму ViewModels, яка може вводити необхідні компоненти, такі як сховище, яке обробляє запити мережі та бази даних. Це також дозволяє вручну вводити аргументи / параметри в активність / фрагмент. Ось стислий виклад кроків щодо впровадження з кодовими списками на основі детального допису Габора Вараді, Dagger Tips .

Dagger Hilt - це рішення наступного покоління, в альфа-версії станом на 7/12/20, яке пропонує той самий варіант використання з більш простим налаштуванням, коли бібліотека переходить у статус випуску.

Реалізація з 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
    }
}

Під час перекреслення створення на заводі я отримую попередження: Невірно зареєстровано «ItemViewModel to T»
Ssenyonjo

1
Попередження для мене наразі не було проблемою. Однак я розгляну це далі, коли перероблю завод ViewModel, щоб ввести його за допомогою Dagger, а не створювати його екземпляр через фрагмент.
Адам Гурвіц,

15

Для однієї фабрики, що ділиться між кількома моделями різних видів, я б розширив відповідь 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) Перевірка типів моделей подання насправді не є методом ООП. Оскільки ViewModels приведені до базового типу, ви знову можете зламати код під час виконання без будь-якого попередження під час компіляції. У цьому випадку я б запропонував використовувати заводські заводу за замовчуванням та передати параметри вже створеній моделі перегляду.
mlyko

@mlyko Впевнений, це всі дійсні заперечення і власний метод (и) для настройки даних viewmodel - це завжди варіант. Але іноді потрібно переконатися, що viewmodel ініціалізовано, отже, використання конструктора. В іншому випадку ви самі повинні вирішити ситуацію "viewmodel ще не ініціалізована". Наприклад, якщо у viewmodel є методи, які повертають LivedData, а спостерігачі приєднані до них у різних методах життєвого циклу View.
Ржехан

3

На основі @ vilpe89 вищезазначеного рішення Kotlin для випадків 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

}

Тоді фрагмент може ініціювати 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, окрім контексту програми?
Адам Гурвіц

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

Ми повинні мати ViewModelFactory для кожного ViewModel для передачі параметрів конструктору ??
K Pradeep Kumar Reddy

Ні. Тільки один ViewModelFactory для всіх ViewModels
Даніл

Чи є якась причина використання канонічного імені як ключа hashMap? Чи можу я використовувати class.simpleName?
K Pradeep Kumar Reddy

Так, але ви повинні переконатися, що немає дублікатів імен
Даніл

Це рекомендований стиль написання коду? Ви придумали цей код самостійно чи читаєте його в андроїд-документах?
K Pradeep Kumar Reddy

1

Я написав бібліотеку, яка повинна зробити це більш простим та чистішим способом, не потрібні багатозв'язки чи заводські котлети, при цьому безперешкодно працюю з аргументами ViewModel, які можуть бути надані 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"
    }    
}

У поданні:

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) У моєму рішенні використовується трохи рефлексії.

Скажімо, ви не хочете створювати однаковий зовнішній клас Factory кожен раз, коли ви створюєте новий клас ViewModel, який потребує деяких аргументів. Ви можете досягти цього за допомогою Reflection.

Наприклад, у вас буде два різні види діяльності:

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

а потім використайте його таким чином у два етапи:

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

2
Вся суть введення параметрів у конструктор полягає в ініціалізації моделі подання лише один раз . З вашої реалізацією, якщо ви телефонуєте myViewModel.initialize(param)в onCreateдіяльності, наприклад, він може бути викликаний кілька разів на тому ж MyViewModelвипадку, коли користувач повертає пристрій.
Sanlok Lee

@Sanlok Lee Ok. Як щодо додавання умови до функції для запобігання ініціалізації, коли це непотрібно. Перевірте мою відредаговану відповідь.
Амр

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
    }

Зателефонуйте Viewmodel в Activity

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

Для отримання додаткової довідки: Приклад Android MVVM Kotlin


Питання задає питання про те, як передавати аргументи / параметри без використання контексту, чого вищезазначене не відповідає: Чи є спосіб передати додатковий аргумент моєму власному конструктору AndroidViewModel, окрім контексту програми?
Адам Гурвіц,

Ви можете передати будь-який аргумент / параметр у своєму користувацькому конструкторі viewmodel. Тут контекст - лише приклад. Ви можете передати будь-який спеціальний аргумент у конструкторі.
Друміл Шах

Зрозумів. Найкращою практикою є не передавати контекст, погляди, дії, фрагменти, адаптери, переглядати життєвий цикл, спостерігати за переглядаючими спостережуваними за життєвим циклом переглядом або утримувати ресурси (малюнки тощо) у ViewModel, оскільки подання може бути знищено, а ViewModel збережеться із застарілим інформація.
Адам Гурвіц,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.