Android ViewModel附加参数


107

AndroidViewModel除了Application上下文,是否有办法将其他参数传递给我的自定义构造函数。例:

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。我只能传递应用程序上下文,而不能传递其他参数。我将非常感谢您的帮助。谢谢。

编辑:我添加了一些代码。我希望现在会更好。


添加更多详细信息和代码
雨果

错误消息是什么?
Moses Aprico

没有错误信息。我只是不知道在哪里设置构造函数的参数,因为ViewModelProvider用于创建AndroidViewModel对象。
马里奥·鲁德曼

Answers:


213

您需要为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。您每次都无法创建工厂。
蒂姆(Tim)

3
这不是真的。新的ViewModel创造防止方法get()。基于文档:“返回现有的ViewModel或在与该ViewModelProvider关联的范围内创建一个新的ViewModel(通常是片段或活动)。” 参见:developer.android.com/reference/android/arch/lifecycle/…–
mlyko

2
如何return modelClass.cast(new MyViewModel(mApplication, mParam))摆脱警告
jackycflau

23

实施依赖注入

这对于生产代码来说是更高级的更好的选择。

Square的Dagger2AssistedInject为ViewModels提供了生产就绪的实现,该实现可以注入必要的组件,例如处理网络和数据库请求的存储库。它还允许在活动/片段中手动插入参数/参数。这是基于Gabor Varadi的详细帖子Dagger Tips的代码Gists 实施步骤的简要概述。

Dagger Hilt是下一代解决方案,从20年7月12日开始使用Alpha,一旦库处于发布状态,它就会以更简单的设置提供相同的用例。

在Kotlin中使用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
    }
}

在工厂中覆盖create时,我收到一条警告,指出未选中的将“ ItemViewModel
强制转换

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)检查视图模型类型并不是真正的OOP处理方式。由于ViewModels被强制转换为基本类型,因此您可以再次在运行时中断代码,而在编译过程中不会发出任何警告。
mlyko

@mlyko当然,这些都是有效的反对意见,并且设置视图模型数据的自己的方法始终是一个选择。但有时您想确保viewmodel已初始化,因此要使用构造函数。否则,您必须自己处理“视图模型尚未初始化”的情况。例如,如果viewmodel具有返回LivedData的方法,并且观察者已附加到各种View生命周期方法中。
rzehan

3

基于@ vilpe89的上述针对AndroidViewModel案例的Kotlin解决方案

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

我们应该为每个ViewModel都有一个ViewModelFactory,以将参数传递给构造函数?
K Pradeep Kumar Reddy

否。所有ViewModel只能使用一个ViewModelFactory
Danil

是否有理由使用规范名称作为hashMap键?我可以使用class.simpleName吗?
K Pradeep Kumar Reddy

是的,但是您必须确保没有重复的名称
Danil

这是推荐的代码编写风格吗?您自己提出了此代码,还是在android文档中阅读了此代码?
K Pradeep Kumar Reddy

1

我编写了一个库,该库应该使此操作更直接,更简洁,不需要多重绑定或工厂样板,同时与可以由Dagger作为依赖项提供的ViewModel参数无缝地一起工作: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
        })
    }
}

0

(科特琳)我的解决方案很少使用反射。

假设您不想每次创建需要某些参数的新ViewModel类时都创建外观相同的Factory类。您可以通过反射来完成。

例如,您将有两个不同的活动:

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

然后魔术部分,工厂类的实现:

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。如何在函数中添加条件以防止不必要的初始化。检查我编辑的答案。
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
    }

在活动中调用Viewmodel

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

有关更多参考:Android MVVM Kotlin示例


该问题询问如何在不使用上面没有遵循的上下文的情况下传递参数/参数:除了应用程序上下文,是否可以将其他参数传递给我的自定义AndroidViewModel构造函数?
亚当·赫维兹

您可以在自定义viewmodel构造函数中传递任何参数/参数。这里的上下文只是一个例子。您可以在构造函数中传递任何自定义参数。
Dhrumil Shah

明白了 最好的做法是不要在ViewModel中传递上下文,视图,活动,片段,适配器,视图生命周期,观察视图生命周期感知的可观察对象或保留资源(可绘制对象等),因为该视图可能会被破坏并且ViewModel将过时地持久化信息。
亚当·赫维兹
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.