Дополнительные аргументы Android ViewModel

113

Есть ли способ передать моему настраиваемому 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
Что за сообщение об ошибке?
Моисей Априко
Сообщение об ошибке отсутствует. Я просто не знаю, где установить аргументы для конструктора, поскольку ViewModelProvider используется для создания объектов AndroidViewModel.
Марио Рудман

Ответы:

219

У вас должен быть фабричный класс для вашей 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);

Для котлина вы можете использовать делегированное свойство:

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 03
2
как насчет использования, return modelClass.cast(new MyViewModel(mApplication, mParam))чтобы избавиться от предупреждения
jackycflau
23

Реализация с внедрением зависимостей

Это более продвинуто и лучше для производственного кода.

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

Dagger Hilt - это решение следующего поколения в альфа-версии по состоянию на 7.12.20, предлагающее тот же вариант использования с более простой настройкой, когда библиотека находится в состоянии выпуска.

Внедрить с жизненным циклом 2.2.0 в Kotlin

Передача аргументов / параметров

// 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
    }
}
Адам Гурвиц
источник
При переопределении create на фабрике я получаю предупреждение Unchecked cast '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);

С разными моделями просмотра с разными конструкторами.

Рзехан
источник
9
Я не рекомендую этот способ по нескольким причинам: 1) параметры на фабрике небезопасны по типу - таким образом вы можете сломать свой код во время выполнения. Всегда старайтесь избегать этого подхода, когда это возможно. 2) проверка типов модели представления на самом деле не является методом ООП. Поскольку ViewModels приведен к базовому типу, вы снова можете сломать код во время выполнения без какого-либо предупреждения во время компиляции. В этом случае я бы предложил использовать фабрику Android по умолчанию и передать параметры уже созданной модели представления.
mlyko
@mlyko Конечно, это все обоснованные возражения, и собственный метод (ы) для настройки данных модели просмотра всегда возможен. Но иногда вы хотите убедиться, что модель представления была инициализирована, поэтому используется конструктор. В противном случае вы должны сами справиться с ситуацией «модель просмотра еще не инициализирована». Например, если у модели просмотра есть методы, возвращающие LivedData, и к ним прикреплены наблюдатели в различных методах жизненного цикла View.
rzehan
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)

    ....
}
MFAL
источник
В вопросе задается вопрос, как передавать аргументы / параметры без использования контекста, который не следует из приведенного выше: есть ли способ передать дополнительный аргумент моему пользовательскому конструктору 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, чтобы передавать параметры конструктору ??
К. Прадип Кумар Редди,
Нет. Только одна ViewModelFactory для всех ViewModels
Данил
Есть ли причина использовать каноническое имя в качестве ключа hashMap? Могу ли я использовать class.simpleName?
К. Прадип Кумар Редди,
Да, но вы должны убедиться, что нет повторяющихся имен
Данил
Это рекомендуемый стиль написания кода? Вы придумали этот код самостоятельно или читали его в документации по Android?
К. Прадип Кумар Редди,
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

(КОТЛИН) Мое решение использует немного Reflection.

Допустим, вы не хотите создавать один и тот же класс Factory каждый раз, когда вы создаете новый класс ViewModel, которому требуются некоторые аргументы. Вы можете сделать это с помощью Reflection.

Например, у вас будет два разных Activity:

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
        }
    }
}
vilpe89
источник
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случае, когда пользователь поворачивает устройство.
Санлок Ли, 04
@ Санлок Ли Хорошо. Как насчет добавления к функции условия для предотвращения инициализации в случае необходимости. Проверьте мой отредактированный ответ.
Амр Бераг
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
    }

Вызов модели просмотра в действии

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

Для получения дополнительной информации: Пример Android MVVM Kotlin

Друмил Шах
источник
В вопросе задается вопрос, как передавать аргументы / параметры без использования контекста, который не следует из приведенного выше: есть ли способ передать дополнительный аргумент моему пользовательскому конструктору AndroidViewModel, кроме контекста приложения?
Адам Гурвиц,
Вы можете передать любой аргумент / параметр в свой пользовательский конструктор модели представления. Здесь контекст - всего лишь пример. Вы можете передать любой настраиваемый аргумент в конструктор.
Друмил Шах,
Понятно. Рекомендуется не передавать контекст, представления, действия, фрагменты, адаптеры, жизненный цикл представления, наблюдать наблюдаемые объекты с учетом жизненного цикла представления или удерживать ресурсы (чертежи и т. Д.) В модели представления, поскольку представление может быть уничтожено, а модель представления будет сохраняться с устаревшими Информация.
Адам Гурвиц,