MVVM 模式和 startActivity

Posted

技术标签:

【中文标题】MVVM 模式和 startActivity【英文标题】:MVVM pattern and startActivity 【发布时间】:2018-03-25 10:25:15 【问题描述】:

我最近决定仔细研究 Google 发布的新 android 架构组件,尤其是使用他们的 ViewModel 生命周期感知类到 MVVM 架构和 LiveData。

只要我处理单个 Activity 或单个 Fragment,一切都很好。

但是,我找不到处理活动切换的好解决方案。 比如说,为了一个简短的例子,A​​ctivity A 有一个启动 Activity B 的按钮。

startActivity() 会在哪里处理?

按照 MVVM 模式,clickListener 的逻辑应该在 ViewModel 中。但是,我们希望避免在其中引用 Activity。因此,将上下文传递给 ViewModel 不是一种选择。

我缩小了几个看起来“可以”的选项,但找不到“这里是如何做”的任何正确答案。

选项 1:在 ViewModel 中有一个枚举,其值映射到可能的路由(ACTIVITY_B、ACTIVITY_C)。将此与 LiveData 结合使用。 Activity 会观察这个 LiveData,当 ViewModel 决定应该启动 ACTIVITY_C 时,它只是 postValue(ACTIVITY_C)。然后Activity就可以正常调用startActivity()了。

选项 2 :常规界面模式。原理与选项 1 相同,但 Activity 将实现接口。不过,我觉得与此有更多的耦合。

选项 3 :消息选项,例如 Otto 或类似选项。 ViewModel 发送一个广播,Activity 接收它并启动它必须要做的事情。此解决方案的唯一问题是,默认情况下,您应该将该广播的注册/注销放在 ViewModel 中。所以没有帮助。

选项 4:在某处有一个大的路由类,如单例或类似的,可以调用它来将相关路由分派给任何活动。最终通过接口?所以每个活动(或 BaseActivity)都会实现

IRouting  void requestLaunchActivity(ACTIVITY_B); 

当你的应用开始有很多片段/活动时,这个方法让我有点担心(因为路由类会变得非常庞大)

就是这样。那是我的问题。你们怎么处理这个? 你会选择我没有想到的选择吗? 您认为哪个选项最相关,为什么? 推荐的 Google 方法是什么?

PS : 没有把我带到任何地方的链接 1 - Android ViewModel call Activity methods 2 - How to start an activity from a plain non-activity java class?

【问题讨论】:

【参考方案1】:

NSimon,你开始使用 AAC 真是太好了。

我之前在 aac's-github 上写了一个issue 关于那个。

有几种方法可以做到这一点。

一种解决方案是使用

WeakReference 到保存 Activity 上下文的 NavigationController。这是在 ViewModel 中处理上下文绑定内容的常用模式。

出于几个原因,我强烈拒绝这样做。首先:这通常意味着您必须保留对修复上下文泄漏的 NavigationController 的引用,但根本不能解决架构问题。

最好的方法(在我看来)是使用 LiveData,它具有生命周期意识,可以做所有想要的事情。

例子:

class YourVm : ViewModel()  

    val uiEventLiveData = SingleLiveData<Pair<YourModel, Int>>()
    fun onClick(item: YourModel) 
        uiEventLiveData.value = item to 3 // can be predefined values
    

之后,您可以在视图中监听变化。

class YourFragmentOrActivity  
     //assign your vm whatever
     override fun onActivityCreated(savedInstanceState: Bundle?)  
        var context = this
        yourVm.uiEventLiveData.observe(this, Observer 
            when (it?.second) 
                1 ->  context.startActivity( ... ) 
                2 ->  ..  
            

        )
    

请注意我使用了修改后的 MutableLiveData,否则它总是会为新的观察者发出最新的结果,这会导致不良行为。例如,如果您更改活动并返回,它将以循环结束。

class SingleLiveData<T> : MutableLiveData<T>() 

    private val mPending = AtomicBoolean(false)

    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<T>) 

        if (hasActiveObservers()) 
            Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
        

        // Observe the internal MutableLiveData
        super.observe(owner, Observer  t ->
            if (mPending.compareAndSet(true, false)) 
                observer.onChanged(t)
            
        )
    

    @MainThread
    override fun setValue(t: T?) 
        mPending.set(true)
        super.setValue(t)
    

    /**
     * Used for cases where T is Void, to make calls cleaner.
     */
    @MainThread
    fun call() 
        value = null
    

    companion object 
        private val TAG = "SingleLiveData"
    

为什么这种尝试比使用弱引用、接口或任何其他解决方案更好?

因为此事件将 UI 逻辑与业务逻辑分开。它也可以有多个观察者。它关心生命周期。它不会泄漏任何东西。

您也可以通过使用 RxJava 而不是 LiveData 通过使用 PublishSubject 来解决它。 (addTo 需要 RxKotlin)

注意不要通过在 onStop() 中释放订阅来泄露订阅。

class YourVm : ViewModel()  
   var subject : PublishSubject<YourItem>  = PublishSubject.create();


class YourFragmentOrActivityOrWhatever 
    var composite = CompositeDisposable() 
    onStart()  
         YourVm.subject 
             .subscribe(  Log.d("...", "Event emitted $it") ,  error("Error occured $it") ) 
               .addTo(compositeDisposable)         
          
       onStop() 
         compositeDisposable.clear()
       
    

还要注意 ViewModel 绑定到 Activity 或 Fragment。您不能在多个活动之间共享 ViewModel,因为这会破坏“Liv​​ecycle-Awareness”。

如果您需要使用room 之类的数据库来保存您的数据,或者使用包裹共享数据。

【讨论】:

感谢您提供非常详细的回答。我也倾向于 LiveData 方法,但没有想到你的 LiveData 调整。总的来说,这一切看起来都非常“hacky”,这样做感觉几乎很糟糕。不管怎样,谢谢 ! (编辑:只能在 20 小时内验证赏金) 不,这不是 hacky。这就是观察者模式的工作原理。你得到一个点击,将它推送到你的 ViewModel,如果有一个 View(这是有保证的),那么它会处理数据。它甚至不是一个补丁 :) 这就是它的工作原理。点击只是一个示例,它可以是每个“数据输入”。 @Suman,android架构蓝图描述的很详细。我认为您将能够在链接中找到此处实现的 SingleLiveData 类。 github.com/googlesamples/android-architecture/blob/… 这正是我自从引入 Android 架构组件以来一直在使用的解决方案 :) 当我很快找到时间时,我会更新解决方案(java+kotlin)。最好的,.. 伊曼纽尔【参考方案2】:

您应该从活动而不是视图模型中调用 startActivity。如果你想从 viewmodel 打开它,你需要在 viewmodel 中使用一些导航参数创建 livedata 并观察 Activity 内的 livedata

【讨论】:

检查这个问题可能对你有帮助。 ***.com/questions/46727276/…【参考方案3】:

您可以从具有应用程序引用的AndroidViewModel 扩展您的 ViewModel,并使用此上下文启动活动。

【讨论】:

是的。但是,在进行单元测试时,您必须模拟上下文,这会增加额外的(不必要的)工作。我最终采用了上述方法,其中包含一个仅用于导航的 LiveData 字段。 两种方式都可以,只是添加了另一个选项。 是的,但是如果您使用该上下文开始您的活动,您需要将 NEW_TASK 标志设置为意图,这很麻烦。不应使用应用程序上下文来启动活动。

以上是关于MVVM 模式和 startActivity的主要内容,如果未能解决你的问题,请参考以下文章

iOS 开发中的 MVVM 模式——实用进阶篇(整理)

mvvm模式的简单介绍

MVVM 模式

mvvm框架和mvc有啥不同

对比MVCMVP来聊聊MVVM模式的理解

wpf mvvm模式 Icommand接口应该如何理解?