为啥 Android 需要视图模型工厂?

Posted

技术标签:

【中文标题】为啥 Android 需要视图模型工厂?【英文标题】:Why a viewmodel factory is needed in Android?为什么 Android 需要视图模型工厂? 【发布时间】:2019-06-22 11:21:51 【问题描述】:

我们一直在讨论这个问题,但我们不知道创建视图模型工厂以创建视图模型而不是直接实例化视图模型的原因。创建一个只创建视图模型的工厂有什么好处?

我只是举了一个简单的例子来说明我在没有工厂的情况下是如何做到的

这里是kodein模块:

val heroesRepositoryModel = Kodein 
    bind<HeroesRepository>() with singleton 
        HeroesRepository()
    

    bind<ApiDataSource>() with singleton 
        DataModule.create()
    

    bind<MainViewModel>() with provider 
        MainViewModel()
    

我在不使用工厂的情况下实例化视图模型的 Activity 部分

class MainActivity : AppCompatActivity() 
    private lateinit var heroesAdapter: HeroAdapter
    private lateinit var viewModel: MainViewModel
    private val heroesList = mutableListOf<Heroes.MapHero>()
    private var page = 0
    private var progressBarUpdated = false

    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewModel = ViewModelProviders.of(this)
                .get(MainViewModel::class.java)
        initAdapter()
        initObserver()
        findHeroes()
    

我直接实例化用例而不在构造函数中的 ViewModel

class MainViewModel : ViewModel(), CoroutineScope 

    private val heroesRepository: HeroesRepository = heroesRepositoryModel.instance()
    val data = MutableLiveData<List<Heroes.MapHero>>()

    private var job: Job = Job()
    override val coroutineContext: CoroutineContext
        get() = uiContext + job

    fun getHeroesFromRepository(page: Int) 
        launch 
            try 
                val response = heroesRepository.getHeroes(page).await()
                data.value = response.data.results.map  it.convertToMapHero() 
             catch (e: HttpException) 
                data.value = null
             catch (e: Throwable) 
                data.value = null
            
        
    

    override fun onCleared() 
        super.onCleared()
        job.cancel()
    

所以这里是一个使用工厂的例子

class ListFragment : Fragment(), KodeinAware, ContactsAdapter.OnContactListener 

    override val kodein by closestKodein()

    private lateinit var adapterContacts: ContactsAdapter

    private val mainViewModelFactory: MainViewModelFactory by instance()
    private val mainViewModel: MainViewModel by lazy 
        activity?.run 
            ViewModelProviders.of(this, mainViewModelFactory)
                .get(MainViewModel::class.java)
         ?: throw Exception("Invalid Activity")
    

    override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? 
        return inflater.inflate(R.layout.fragment_list, container, false)
    

视图模型工厂:

class MainViewModelFactory (private val getContacts: GetContacts) : ViewModelProvider.Factory 

    override fun <T : ViewModel?> create(modelClass: Class<T>): T 
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) 
            return MainViewModel(getContacts) as T
        
        throw IllegalArgumentException("Unknown ViewModel class")
    

还有视图模型:

class MainViewModel(private val getContacts: GetContacts) : BaseViewModel() 
    lateinit var gamesList: LiveData<PagedList<Contact>>
    var contactsSelectedData: MutableLiveData<List<Contact>> = MutableLiveData()
    var contactsSelected: ArrayList<Contact> = ArrayList()
    private val pagedListConfig by lazy 
        PagedList.Config.Builder()
                .setEnablePlaceholders(false)
                .setInitialLoadSizeHint(PAGES_CONTACTS_SIZE)
                .setPageSize(PAGES_CONTACTS_SIZE)
                .setPrefetchDistance(PAGES_CONTACTS_SIZE*2)
                .build()
    

这是完整的第一个示例:

https://github.com/ibanarriolaIT/Marvel/tree/mvvm

还有完整的第二个例子:

https://github.com/AdrianMeizoso/Payment-App

【问题讨论】:

见 this 和 this - 无论如何都使用工厂 那既然用了为什么还要自己创建呢? 【参考方案1】:

当我们只是使用 ViewModel 时,我们无法将参数传递给该 ViewModel

class GameViewModel() : ViewModel() 

    init 
        Log.d(TAG, "GameViewModel created")
    

但是,在某些情况下,我们需要将自己的参数传递给 ViewModel。这可以使用 ViewModelFactory 来完成。

class ScoreViewModel(finalScore: Int) : ViewModel() 

    val score = finalScore

    init 
        Log.d(TAG, "Final score: $finalScore")
    

要实例化这个 ViewModel,我们需要一个 ViewModelProvider.Factory,因为简单的 ViewModel 无法实例化它。

class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory 

    override fun <T : ViewModel?> create(modelClass: Class<T>): T 
        if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) 
            return ScoreViewModel(finalScore) as T
        
        throw IllegalArgumentException("Unknown ViewModel class")
    

在实例化此 ViewModel 的对象时,即使用 ViewModelProvider,我们将 ViewModelFactory 作为参数传递,其中包含有关我们想要传递的自定义参数的信息。它是这样的:

viewModelFactory = ScoreViewModelFactory(score)
viewModel = ViewModelProvider(this,viewModelFactory).get(ScoreViewModel::class.java)

这就是工厂方法存在的原因。

【讨论】:

【参考方案2】:

总之,

如果我们需要传递一些input dataviewModelconstructor,我们需要为viewModel 创建一个factory class

例如:-

class MyViewModelFactory constructor(private val repository: DataRepository): ViewModelProvider.Factory 

     override fun <T : ViewModel> create(modelClass: Class<T>): T 
        return if (modelClass.isAssignableFrom(MyViewModel::class.java!!)) 
            MyViewModel(this.repository) as T
         else 
            throw IllegalArgumentException("ViewModel Not Found")
        
    

原因

我们不能直接创建ViewModel 的对象,因为它不会知道lifecyclerOwner。所以我们使用:-

ViewModelProviders.of(this, MyViewModelFactory(repository)).get(MyViewModel::class.java)

【讨论】:

【参考方案3】:

我们一直在讨论这个问题,但我们不知道创建视图模型工厂以创建视图模型而不是直接实例化视图模型的原因。创建一个只创建视图模型的工厂有什么好处?

因为 android 只会在尚未为特定给定 ViewModelStoreOwner 创建的情况下为您提供一个新实例

我们也不要忘记 ViewModel 会在配置更改时保持活动状态,因此如果您旋转手机,则不应创建新的 ViewModel。

如果您要返回之前的 Activity 并重新打开此 Activity,那么之前的 ViewModel 应该会收到 onCleared(),而新的 Activity 应该有一个新的 ViewModel。

除非您自己这样做,否则您应该只信任 ViewModelProviders.Factory 来完成它的工作。

(而且你需要工厂,因为你通常不只是有一个 no-arg 构造函数,你的 ViewModel 有构造函数参数,并且 ViewModelProvider 必须知道如何在你使用非构造函数时填写构造函数参数-默认构造函数)。

【讨论】:

问题是我向视图模型注入了它需要的存储库,因此我不需要创建带有参数的构造函数,并且它尊重生命周期,因为它不会在我每次旋转时再次加载电话。所以它就像我有一个工厂一样工作,但不必创建它。 这不是注入,您正在从全局变量中进行查找。你应该使用构造函数参数。对于 Kodein,我认为您应该拥有 provider MainViewModel(instance()) ,然后在 ViewModelProviders.Factory 中调用提供程序。 这是唯一强调设置“初始状态”一次的重要性的答案,而不是在设备的每次旋转时设置。这就是为什么创建自定义工厂很重要的本质。【参考方案4】:

我们不能自己创建 ViewModel。我们需要 Android 提供的 ViewModelProviders 实用程序来创建 ViewModel。

但是 ViewModelProviders 只能实例化没有 arg 构造函数的 ViewModels。

因此,如果我有一个带有多个参数的 ViewModel,那么我需要使用一个可以传递给 ViewModelProviders 以在需要 MyViewModel 实例时使用的工厂。

例如——

public class MyViewModel extends ViewModel 
    private final MyRepo myrepo;
    public MyViewModel(MyRepo myrepo) 
         this.myrepo = myrepo;
    

要实例化这个 ViewModel,我需要有一个 ViewModelProviders 可以用来创建它的实例的工厂。

ViewModelProviders Utility 无法使用参数构造函数创建 ViewModel 的实例,因为它不知道如何以及在构造函数中传递什么对象。

【讨论】:

但是我在视图模型本身中实例化存储库,所以构造函数没有任何参数。那么这里的问题是,在 Activity 中实例化存储库是否有优势,创建工厂以将存储库传递给视图模型,或者只是在视图模型中实例化存储库并使用 ViewModelProviders 而无需创建自己的工厂。 I am instantiating the repository in the viewmodel itself 那你违反了 DI 原则,我想知道你为什么要使用 Kodein。 对于依赖倒置,一个类不应该创建它需要的依赖。这些依赖项应该提供给它。这里我们有一个构造函数注入的例子,我们通过对象构造函数注入我们的 repo。 阅读此内容以获得更多参考和清晰度 - en.wikipedia.org/wiki/Dependency_inversion_principle “ViewModelProviders Utility 无法使用参数构造函数创建 ViewModel 的实例,因为它不知道如何以及在构造函数中传递什么对象。”你能解释一下吗?为什么 ViewModelProviders 专门设计为仅使用工厂而不是普通构造函数来创建 ViewModel?

以上是关于为啥 Android 需要视图模型工厂?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的 Django 工厂函数在本地数据库中创建模型,而不是测试数据库?

模型图草图

Android设计模式之工厂模式

为啥是模型视图矩阵?

为啥模型在MVC模式中直接更新视图?

为啥 DateField 数据类型在模型方法和视图中不同?