MVVM架构下Activity和Fragment之间数据传递和点击事件回调的新姿势

背景

传统的activity和fragment之间、fragment和fragment之间的数据传递有以下方式
1、直接使用类似eventbus、livedatabus这类事件总线的方式
2、使用接口回调,找到实现实现该接口的fragment或activity进行强制类型转换再进行方法调用和参数传递
第一种方式如果项目大了,没法调试,代码可读性差,管理维护麻烦
第二种方式显示的持有具有生命周期的fragment或activity理论上都需要套一层WeakReference防止内存泄露,麻烦。
今天我们使用livedata+viewmodel的方式解决数据传递和点击事件回调,总之两个字:优雅!

实现方式

第一步:依赖引入
app的build.gradle下引入livedata和viewmodel依赖

android {
    //...
    compileOptions {
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
}
dependencies {
    ///...
    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel- ktx:$lifecycle_version"
    // LiveData
    implementation "androidx.lifecycle:lifecycle-livedata- ktx:$lifecycle_version"
    // For using KTX Delegates extensions for fragments
    implementation "androidx.fragment:fragment-ktx:$fragment_version"
}

第二步,viewmodel里声明事件


import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MainViewModel(val data: String) : ViewModel() {

    val showSharedVMToast = MutableLiveData<Unit>()
    
    // Shows a Toast using SharedViewModel implementation
    fun onShowVMToastClick() {
        showSharedVMToast.value = Unit
    }
}

第三步,activity里响应事件

import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer

// Adding the interface that communicates with the fragment
class MainActivity : AppCompatActivity() {

    // Initializing the viewModel on call using KTX-Fragments extension.
    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Observing viewModel's LiveData
        viewModel.showSharedVMToast.observe(this, Observer {
            Toast.makeText(this, "Hello from sharedVM", Toast.LENGTH_SHORT).show()
        })

        // Adding the DemoFragment to a container FrameLayout
        supportFragmentManager.beginTransaction().add(R.id.demoFragmentContainer, DemoFragment()).commit()
    }
}

第四步,fragment里调用

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels

class DemoFragment : Fragment() {

    // We create an instance of MainViewModel using specific KTX-Fragments extension to
    // tell the program that we are willing to use the instance of the Activity this fragment is attached to.
    private val sharedVM: MainViewModel by activityViewModels()

    // We don't need Constructor to pass any extra to this fragment because we have access to the data from SharedVM
    // already.
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_demo, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        //We can tap directly to the viewModel without using Interface callbacks!
        sharedVMButton.setOnClickListener {
            sharedVM.onShowVMToastClick()
        }
    }
}

这样就完成了fragment里点击调用activity事件或数据传递(反之亦如此),核心就是这两个东西

    val showSharedVMToast = MutableLiveData<Unit>()
    
    private val sharedVM: MainViewModel by activityViewModels()

如果要传递普通的内容,如String,Unit换成String即可

扩展

上述点击事件和数据传递使用的是liveData,liveData有个特性就是存储的内容会回显,简单点说就是fragment被销毁后,如果对应的viewmodel没有被销毁,所持有的livedata对象在fragment/activity被重新创建好后会主动回调一次,如果业务逻辑是只想消费一次,如点击事件,不希望数据回显或数据回流,这个时候需要改造liveData成SingleEvent类型,核心思路就是内部给一个标记位Event Wrapper,代码如下:

方式一:内部标记位

    class SingleLiveEvent<T> : MutableLiveData<T>() {
    
    private val mPending = AtomicBoolean(false)
    
    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        if (hasActiveObservers()) {
            Log.w("SingleLiveEvent", "Multiple observers registered but only one will be notified of changes.")
        }
        // Observe the internal MutableLiveData
        super.observe(owner) { t ->
            if (mPending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        }
    }
    
    @MainThread
    override fun setValue(t: T?) {
        mPending.set(true)
        super.setValue(t)
    }
}
    

使用方式

    //MutableLiveData换成SingleLiveEvent即可
    val mediatorLiveData = SingleLiveEvent<String>()

方式二:Event Wrappter

    /**
 * Used as a wrapper for data that is exposed via a LiveData that represents an event.
 */
open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

使用方式

class ListViewModel : ViewModel {
    private val _navigateToDetails = MutableLiveData<Event<String>>()

    val navigateToDetails : LiveData<Event<String>>
        get() = _navigateToDetails


    fun userClicksOnButton(itemId: String) {
        _navigateToDetails.value = Event(itemId)  // Trigger the event by setting a new Event as a new value
    }
}

观察的地方

myViewModel.navigateToDetails.observe(this, Observer {
    it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
        startActivity(DetailsActivity...)
    }
})
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,590评论 6 517
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 95,157评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 169,301评论 0 362
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 60,078评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 69,082评论 6 398
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,682评论 1 312
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,155评论 3 422
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,098评论 0 277
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,638评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,701评论 3 342
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,852评论 1 353
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,520评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,181评论 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,674评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,788评论 1 274
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,279评论 3 379
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,851评论 2 361

推荐阅读更多精彩内容