【问题标题】:MVVM pattern and startActivityMVVM 模式和 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?

【问题讨论】:

    标签: android mvvm android-architecture-components


    【解决方案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

    【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 是的。但是,在进行单元测试时,您必须模拟上下文,这会增加额外的(不必要的)工作。我最终采用了上述方法,其中包含一个仅用于导航的 LiveData 字段。
    • 两种方式都可以,只是添加了另一个选项。
    • 是的,但是如果您使用该上下文开始您的活动,您需要将 NEW_TASK 标志设置为意图,这很麻烦。不应使用应用程序上下文来启动活动。
    猜你喜欢
    • 1970-01-01
    • 2017-03-21
    • 1970-01-01
    • 2017-06-16
    • 2015-11-04
    • 1970-01-01
    • 1970-01-01
    • 2019-03-02
    • 2015-08-24
    相关资源
    最近更新 更多