Android ViewModel 附加参数

Posted

技术标签:

【中文标题】Android ViewModel 附加参数【英文标题】:Android ViewModel additional arguments 【发布时间】:2018-02-27 06:25:11 【问题描述】:

除了应用程序上下文之外,有没有办法将附加参数传递给我的自定义 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。我只能传递应用程序上下文,但不能传递其他参数。我真的很感激任何帮助。谢谢你。

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

【问题讨论】:

添加更多细节和代码 错误信息是什么? 没有错误信息。我根本不知道在哪里为构造函数设置参数,因为 ViewModelProvider 用于创建 AndroidViewModel 对象。 【参考方案1】:

您需要为您的 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()

【讨论】:

每个ViewModel 类都需要它的ViewModelFactory 吗? 但是每个ViewModel 可以/将会有不同的DI。您如何知道 create() 方法返回哪个实例? 您的 ViewModel 将在更改方向后重新创建。你不能每次都创建工厂。 这不是真的。新的ViewModel 创建阻止方法get()。基于文档:“返回现有的 ViewModel 或在范围内创建一个新的 ViewModel(通常是片段或活动),与此 ViewModelProvider 相关联。”见:developer.android.com/reference/android/arch/lifecycle/… 如何使用return modelClass.cast(new MyViewModel(mApplication, mParam)) 摆脱警告【参考方案2】:

使用依赖注入实现

这对于生产代码来说更高级和更好。

Dagger2,Square 的AssistedInject 为 ViewModel 提供了生产就绪的实现,可以注入必要的组件,例如处理网络和数据库请求的存储库。它还允许在活动/片段中手动注入参数/参数。这是基于 Gabor Varadi 的详细帖子 Dagger Tips 的 steps to implement 的简明大纲和代码要点。

Dagger Hilt,是下一代解决方案,自 2020 年 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
    

【讨论】:

在工厂中覆盖创建时,我收到一条警告说 Unchecked cast 'ItemViewModel to T' 到目前为止,该警告对我来说不是问题。但是,当我重构 ViewModel 工厂以使用 Dagger 注入它而不是通过片段创建它的实例时,我会进一步研究它。【参考方案3】:

对于在多个不同视图模型之间共享的一个工厂,我会像这样扩展 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);

不同的视图模型有不同的构造函数。

【讨论】:

我不推荐这种方式,因为有几个原因:1)工厂中的参数不是类型安全的——这样你可以在运行时破坏你的代码。尽可能避免这种方法 2)检查视图模型类型并不是真正的 OOP 做事方式。由于 ViewModel 被强制转换为基本类型,因此您可以再次在运行时中断代码而不会在编译期间发出任何警告。在这种情况下,我建议使用默认的 android 工厂并将参数传递给已经实例化的视图模型。 @mlyko 当然,这些都是有效的反对意见,并且自己设置视图模型数据的方法始终是一种选择。但有时你想确保 viewmodel 已经初始化,因此使用构造函数。否则,您必须自己处理“视图模型尚未初始化”的情况。例如,如果 viewmodel 具有返回 LivedData 的方法,并且在各种 View 生命周期方法中附加了观察者。【参考方案4】:

基于@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

然后一个fragment可以启动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 构造函数?【参考方案5】:

我把它变成了一个类,在其中传递了已经创建的对象。

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 将参数传递给构造函数?? 没有。所有 ViewModel 都只有一个 ViewModelFactory 是否有任何理由使用规范名称作为 hashMap 键?我可以使用 class.simpleName 吗? 可以,但必须确保没有重复的名称 这是推荐的代码编写方式吗?您自己想出了这段代码,还是在 android 文档中阅读了它?【参考方案6】:
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
    

在 Activity 中调用 Viewmodel

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

更多参考:Android MVVM Kotlin Example

【讨论】:

问题询问如何在不使用上述不遵循的上下文的情况下传递参数/参数:除了应用程序上下文之外,有没有办法将其他参数传递给我的自定义 AndroidViewModel 构造函数? 您可以在自定义视图模型构造函数中传递任何参数/参数。这里的上下文只是一个例子。您可以在构造函数中传递任何自定义参数。 明白。最佳实践是不要传递上下文、视图、活动、片段、适配器、视图生命周期、观察视图生命周期感知的可观察对象或在 ViewModel 中保存资源(可绘制对象等),因为视图可能会被破坏并且 ViewModel 将持续存在过时信息。【参考方案7】:

我编写了一个库,该库应该使执行此操作更加简单明了,不需要多重绑定或工厂样板,同时可以与 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
        )
    

【讨论】:

【参考方案8】:

(KOTLIN) 我的解决方案使用了一点点反射。

假设您不想在每次创建需要一些参数的新 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)
    

以及这些活动的 ViewModel:

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
        
    

【讨论】:

【参考方案9】:

为什么不这样做:

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)

【讨论】:

将参数放入构造函数的全部意义在于只初始化视图模型一次。在您的实现中,如果您在活动的onCreate 中调用myViewModel.initialize(param),例如,当用户旋转设备时,可以在同一个MyViewModel 实例上多次调用它。 @Sanlok Lee 好的。如何在函数中添加条件以防止在不必要时进行初始化。检查我编辑的答案。

以上是关于Android ViewModel 附加参数的主要内容,如果未能解决你的问题,请参考以下文章

Android MVVM startActivity 的最佳实践

Android Compose MVVM - 如何在不带参数的可组合函数中引用 viewModel 对象?

Android Jetpack -- ViewModel篇

一个ViewModel,多个Activity

如何从ViewModel访问附加到窗口的Behaviors属性

[Android ViewModel] public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory 问题