Android-ViewModel+LiveData学习总结

发布于:2025-05-23 ⋅ 阅读:(21) ⋅ 点赞:(0)

面试话术模块:ViewModel 核心原理实战模拟

场景1:基础原理阐述

面试官​:
“我看你项目里用了 ViewModel,能说说它的核心优势吗?为什么选它而不是直接写在 Activity 里?”

候选人​(结合业务场景举例):
“ViewModel 最大的优势是数据与 UI 的解耦。比如我们做项目 App 的故事模块,用户查看故事详情后即使旋转屏幕,故事数据也不会丢失。这是因为 ViewModel 的生命周期和 Activity 的‘非配置生命周期’绑定——只要 Activity 不是彻底销毁(比如用户点返回键退出),ViewModel 就会一直存活。

技术上,ViewModel 通过 ViewModelStore 存储数据,而 ComponentActivity 在配置变更时会通过 onRetainNonConfigurationInstance() 保留这个容器。新 Activity 重建时,直接从 NonConfigurationInstance 恢复 ViewModelStore,复用原有实例。

对比直接写在 Activity 里,ViewModel 避免了数据重复加载的问题。比如用户浏览故事列表时,如果每次旋转屏幕都重新请求接口,体验会很差。用 ViewModel 缓存数据后,列表能瞬间恢复,流量和性能都优化了。”


场景2:生命周期与内存泄漏

面试官​:
“ViewModel 如果持有 Activity 的 Context,会不会导致内存泄漏?你们怎么防范?”

候选人​(结合防御性设计):
“这是个经典问题。ViewModel 设计上就规避了这个问题——它默认不持有 Activity 的引用,而是通过工厂类注入 Application 级别的 Context。比如我们项目中用户信息管理模块,需要用到 SharedPreferences,这时会用 AndroidViewModel

class UserViewModel(application: Application) : AndroidViewModel(application) {
    private val context = getApplication<Application>().applicationContext
    // 使用 context 操作 SharedPreferences
}

这样即使 Activity 销毁,ViewModel 也不会因为持有它的引用而泄漏。

另外,ViewModel 的 onCleared() 是资源释放的入口。比如在音视频播放模块,我们在这里释放 MediaPlayer 实例,避免后台常驻耗电。”


场景3:跨组件通信

面试官​:
“Activity 和多个 Fragment 要共享数据,用 ViewModel 怎么实现?”

候选人​(结合架构设计):
“这正好是 ViewModel 的强项。比如商品详情页的‘加入购物车’按钮在 Fragment A,而购物车数量显示在 Fragment B,我们可以让两个 Fragment 共享同一个 Activity 级别的 ViewModel:

// Fragment A 和 B 中
val sharedModel: CartViewModel by viewModels(requireActivity())

这样点击按钮时,Fragment A 更新 ViewModel 的数据,Fragment B 通过 LiveData 自动刷新 UI,完全不需要接口回调或者 EventBus。

扩展代码:

“在Activity和多个Fragment之间共享数据,是ViewModel的经典使用场景。我的做法是让所有需要共享数据的Fragment都绑定到宿主Activity的作用域,这样它们访问的是同一个ViewModel实例。具体分三步:

  1. 在ViewModel中定义共享数据源
    比如电商App的购物车场景:
class CartViewModel : ViewModel() {
    private val _cartItems = MutableLiveData<List<CartItem>>()
    val cartItems: LiveData<List<CartItem>> = _cartItems

    fun addItem(item: CartItem) {
        // 更新购物车数据
    }
}
  1. 在Activity中无需额外操作
    Activity只需要正常初始化,不需要直接持有ViewModel引用。

  2. 在Fragment中通过宿主Activity获取ViewModel
    比如商品列表Fragment和购物车Fragment:

// 商品列表Fragment(添加商品)
class ProductListFragment : Fragment() {
    // 关键点:通过activityViewModels()获取Activity作用域的ViewModel
    private val cartViewModel: CartViewModel by activityViewModels()

    fun onAddToCartClick(item: Product) {
        cartViewModel.addItem(convertToCartItem(item))
    }
}

// 购物车Fragment(显示数量)
class CartFragment : Fragment() {
    private val cartViewModel: CartViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 使用viewLifecycleOwner避免生命周期问题
        cartViewModel.cartItems.observe(viewLifecycleOwner) { items ->
            updateCartBadge(items.size)
        }
    }
}

避坑经验:​

  • 不要用默认的by viewModels(),这会让每个Fragment得到独立实例
  • 观察LiveData时务必使用viewLifecycleOwner而非this,防止Fragment视图销毁后仍收到更新导致空指针
  • 复杂数据建议结合SavedStateHandle处理进程死亡恢复

扩展场景:​
在金融App中,我们曾用这种方式实现投资确认页:

  • Fragment A:输入金额
  • Fragment B:选择支付方式
  • Fragment C:风险提示
    三个Fragment共享同一个Activity级的OrderViewModel,最终提交时聚合所有数据,避免层层传递参数的复杂度。”

场景4:数据持久化与进程恢复

面试官​:
“如果 App 进程被系统杀死,ViewModel 的数据还能恢复吗?怎么处理?”

候选人​(结合 SavedStateHandle):
“默认不行,但可以用 SavedStateHandle 实现进程级恢复。比如用户填了一半的注册表单,即使进程被杀也能恢复。我们在 ViewModel 构造函数里注入它:

class RegisterViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    val phoneNumber = savedStateHandle.getLiveData<String>("phone")
    
    fun saveInput(phone: String) {
        savedStateHandle["phone"] = phone
    }
}

底层原理是 SavedStateHandle 会把数据写入系统管理的 Bundle,进程重建时自动恢复。这比 onSaveInstanceState 更灵活,适合表单、草稿等场景。”


场景5:高级优化与踩坑

1. 生命周期感知导致的内存泄漏

现象​:ViewModel 持有 Activity/Fragment 的引用,导致无法释放。
原因​:在 ViewModel 中直接传递 Context 或 View 对象,或在协程中未正确取消任务。
解决​:

class MyViewModel(private val appContext: Application) : AndroidViewModel(appContext) {
    // 使用 AndroidViewModel 获取 Application Context
    private val scope = viewModelScope // 自动绑定生命周期

    fun fetchData() {
        scope.launch {
            // 协程会在 ViewModel 销毁时自动取消
        }
    }
}

关键点​:使用 AndroidViewModel 获取 Application Context,避免持有 UI 组件的引用。


2. 数据倒灌(LiveData 重复触发)​

现象​:屏幕旋转后,LiveData 观察者再次收到相同数据。
原因​:LiveData 默认缓存最新数据,重新订阅时会触发回调。
解决​:

// 使用 SingleLiveEvent 或自定义事件包装类
class SingleLiveEvent<T> : MutableLiveData<T>() {
    private val pending = AtomicBoolean(false)

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        super.observe(owner) { t ->
            if (pending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        }
    }

    override fun setValue(value: T) {
        pending.set(true)
        super.setValue(value)
    }
}

// ViewModel 中使用
val navigateEvent = SingleLiveEvent<Unit>()

关键点​:通过事件包装类(如 SingleLiveEvent)或 Kotlin Channel 实现一次性事件传递。


3. Fragment 间共享 ViewModel 的数据竞争

现象​:多个 Fragment 通过 Activity 共享 ViewModel 时数据更新不同步。
原因​:未正确处理多 Fragment 对同一数据的并发修改。
解决​:

// 使用原子操作或 MutableStateFlow 确保线程安全
class SharedViewModel : ViewModel() {
    private val _data = MutableStateFlow("")
    val data: StateFlow<String> = _data.asStateFlow()

    fun updateData(newValue: String) {
        _data.value = newValue // 线程安全更新
    }
}

关键点​:使用 StateFlowLiveData 的原子操作保证数据一致性。


4. onCleared() 未正确释放资源

现象​:ViewModel 销毁时未关闭数据库连接、取消协程等。
解决​:

class MyViewModel : ViewModel() {
    private val customScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    override fun onCleared() {
        super.onCleared()
        customScope.cancel() // 手动取消自定义协程作用域
        // 释放其他资源(如数据库连接)
    }
}

关键点​:在 onCleared() 中释放资源,优先使用 viewModelScope 自动管理协程。


5. SavedStateHandle 数据恢复问题

现象​:应用被杀后,通过 SavedStateHandle 恢复的数据不符合预期。
解决​:

class SavedStateViewModel(
    private val savedState: SavedStateHandle
) : ViewModel() {
    init {
        // 从 SavedStateHandle 恢复数据
        val restoredValue = savedState.get<String>("key") ?: "default"
        savedState.setSavedStateProvider("key") { 
            // 提供需持久化的数据
            Bundle().apply { putString("key", currentValue) }
        }
    }
}

关键点​:通过 SavedStateHandle 处理进程死亡后的数据恢复,验证默认值逻辑。


6. 单元测试中的生命周期模拟

问题​:测试 ViewModel 时无法直接控制生命周期。
解决​:

// 使用 InstantTaskExecutorRule 和 TestCoroutineDispatcher
@RunWith(AndroidJUnit4::class)
class MyViewModelTest {
    @get:Rule
    val rule = InstantTaskExecutorRule()

    @Test
    fun testViewModel() = runBlockingTest {
        val viewModel = MyViewModel()
        viewModel.fetchData()
        // 验证 LiveData 或 StateFlow 的结果
    }
}

关键点​:通过测试库模拟主线程和协程调度。


大厂面试场景模拟 LiveData 深度对话

​面试官​:

“LiveData 是 Android 架构组件的核心之一,你能简单说说它的设计初衷和主要优势吗?”


​候选人​:

“LiveData 的设计初衷是为了解决数据驱动 UI 更新时的生命周期安全问题。它是一个可观察的数据持有者,能感知 Activity 或 Fragment 的生命周期,确保只在活跃状态(比如 STARTEDRESUMED)下通知 UI 更新。这样一来,开发者不用手动处理反注册观察者,避免了内存泄漏和崩溃问题。比如网络请求结果通过 LiveData 传递给 UI 时,如果页面已经销毁,LiveData 会自动忽略回调,特别省心。”​


面试官:

“提到生命周期感知,LiveData 的观察者绑定到 LifecycleOwner 后,具体是如何做到自动取消订阅的?”


候选人​:

“LiveData 内部通过 Lifecycle 对象跟踪 LifecycleOwner 的状态变化。当调用 observe() 方法时,LiveData 会创建一个 LifecycleBoundObserver 包装类,监听生命周期状态。比如当页面进入 DESTROYED 状态时,LiveData 会主动移除观察者,并释放相关引用。这种机制完全解耦了 UI 层和数据层,开发者几乎不需要写模板代码。”​


面试官:

“在实际项目中,LiveData 和 Kotlin 的 StateFlow 经常被拿来比较,你们团队是如何做技术选型的?”


候选人​:

        这个问题我们确实纠结过。LiveData 的优势是开箱即用,和 ViewModel、DataBinding 无缝配合,适合纯 Java/Kotlin 的 Android 项目;

        而 StateFlow 需要配合协程,更适合全 Kotlin 且重度使用 Flow 的架构。比如我们项目里有些复杂数据流需要组合操作(如 combineflatMapLatest),这时候用 Flow 更灵活。但如果是简单的 UI 状态管理,比如控制加载进度条,LiveData 反而更轻量。​


面试官​:

“LiveData 的‘数据倒灌’问题你们遇到过吗?比如屏幕旋转后,观察者再次收到旧数据。”


候选人​:

        当然遇到过!比如一个点击按钮触发导航事件,旋转屏幕后 LiveData 会把最后一次事件又推给新的 Activity,导致重复跳转。我们当时的解法是参考了 Google 的 SingleLiveEvent,封装一个事件包装类,在数据被消费后标记为‘已处理’。不过后来发现这种方式不够优雅,比如多个观察者可能漏掉事件。现在更倾向于用 Kotlin 的 SharedFlow,设置 replay=0,这样新订阅者不会收到旧事件,配合 lifecycle.repeatOnLifecycle 控制订阅范围,彻底解决问题。​​


面试官​:

“假设现在有一个实时更新的股票价格页面,用 LiveData 频繁更新 UI 会导致卡顿,你们会如何优化?”


候选人​:

        这种情况 LiveData 可能不是最优解。首先,频繁更新可以考虑数据防抖(比如 500ms 内的多次更新合并为一次);其次,如果数据量太大,可以改用 DiffUtil 配合 RecyclerView 局部更新,而不是刷新整个列表。另外,LiveData 的 postValue 方法在子线程批量更新时可能会有丢数据的问题,可以换成 setValue 在主线程同步更新,或者直接切到 StateFlow,它的 emitcollect 本身是挂起函数,更适合高频数据流。

大厂面试场景模拟:ViewModel + LiveData 深度对话


Q1:ViewModel 如何做到配置变更(如屏幕旋转)时数据不丢失?​

面试官​:
“你在项目里用 ViewModel 解决过屏幕旋转数据丢失的问题吧?说说它背后是怎么存活的?”

候选人​(结合源码与业务场景):
ViewModel 的存活秘诀在于数据容器与生命周期的解耦。当 Activity 因配置变更(比如旋转)被销毁时,系统会通过 onRetainNonConfigurationInstance() 保留一个 NonConfigurationInstance 对象,里面装着 ViewModelStore——所有 ViewModel 的保险箱。新 Activity 重建时,直接从这保险箱里取出原来的 ViewModel 实例,数据自然不丢。

比如我们电商 App 的购物车页面,用户加购商品后即使旋转屏幕,购物车列表也不会清空。这得益于 ViewModelStore 的持久化机制,它的生命周期和 Activity 的‘非配置销毁’绑定。只有当用户真正退出页面(比如点返回键),ViewModel 才会被清除。

底层源码中,ComponentActivitygetViewModelStore() 方法会检查是否有旧的 NonConfigurationInstance,有就直接复用。这种设计让 ViewModel 的数据完全摆脱了 Activity 的生命周期束缚,性能比 onSaveInstanceState 快 30 倍,尤其适合存储复杂对象(比如商品列表)。


Q2:LiveData 为什么会出现‘粘性事件’?如何解决?​

面试官​:
“用户反馈我们的启动页快速切换横竖屏时,UI 会闪变。你们排查是 LiveData 的粘性事件导致的,怎么解决的?”

候选人​(结合问题定位与优化方案):
“粘性事件的本质是 ​LiveData 的版本号机制。每次调用 setValue()mVersion 自增,而新观察者的 lastVersion 初始为 -1。当新观察者订阅时,只要 mVersion > lastVersion,就会触发历史数据回调。比如启动页的横竖屏切换会导致 Fragment 重建,新观察者会收到上一次的数据,造成 UI 重复刷新。

我们的解决方案是自定义 NonStickyLiveData,通过包装观察者并控制版本号同步。比如在社交 App 的私信列表页,我们重写 observe() 方法,在新观察者订阅时将 lastVersion 强制设为当前 mVersion,这样只有后续的新数据才会触发回调。代码关键点如下:

class NonStickyLiveData<T> : MutableLiveData<T>() {
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        super.observe(owner) { value ->
            // 只有当数据版本号 > 观察者的 lastVersion 才触发
            if (version > observer.lastVersion) {
                observer.onChanged(value)
            }
        }
    }
}

上线后重复通知率从 42% 降到 3%,彻底解决了闪屏问题。”


Q3:Fragment 间共享 ViewModel 的正确姿势

面试官​:
“如果两个 Fragment 要同步用户昵称,用 ViewModel 怎么实现?用错作用域会有什么后果?”

候选人​(踩坑经验 + 最佳实践):
作用域选择是核心。比如个人资料页的 Fragment A(展示昵称)和 Fragment B(编辑昵称),必须共享同一个 ViewModel 实例。这时候要用 by activityViewModels(),让 ViewModel 绑定到 Activity 作用域:

// Activity 中无需额外代码,Fragment 直接获取
class ProfileFragment : Fragment() {
    private val sharedVM: UserViewModel by activityViewModels()
    // 观察昵称变化并更新 UI
    sharedVM.nickname.observe(viewLifecycleOwner) { name -> 
        tvNickname.text = name 
    }
}

如果误用 by viewModels(),每个 Fragment 会创建独立实例,导致编辑页修改的数据无法同步到展示页。之前教育 App 就因此出现 28% 的数据不同步 Bug,后来通过 Code Review 强制规范作用域才解决。”


Q4:LiveData 的 observe() 必须传 LifecycleOwner,否则会怎样?​

面试官​:
“听说在 Fragment 里用 this 代替 viewLifecycleOwner 有风险,能具体说说吗?”

候选人​(内存泄漏案例 + 源码解析):
生命周期错配是隐形炸弹。比如在 Fragment 的 onCreateView 里观察 LiveData,如果传 this(即 Fragment 的 lifecycleOwner),观察者会绑定到整个 Fragment 的生命周期。即使 Fragment 的 View 被销毁(onDestroyView),只要 Fragment 未彻底销毁,观察者依然存活,导致 UI 组件无法释放。

某新闻 App 的详情页就因为这个漏洞,用户返回列表页后,详情页的图片请求仍在后台执行,内存暴涨 20%。正确做法是用 viewLifecycleOwner,它的生命周期和 View 绑定,View 销毁时自动移除观察者:

viewModel.data.observe(viewLifecycleOwner) { data -> 
    // 安全更新 UI,避免 View 销毁后空指针
}

阿里 P8 在团队规范中明确要求:​所有 Fragment 的 LiveData 观察必须使用 viewLifecycleOwner,从源头杜绝泄漏。”


Q5:ViewModel 如何注入带参数的依赖?对比 Hilt 有什么优劣?​

面试官​:
“如果 ViewModel 需要 UserId 参数,你们是怎么实现的?手动写 Factory 和用 Hilt 哪个更好?”

候选人​(技术选型思考):
依赖注入是架构设计的核心。比如直播间的 ViewModel 需要房间 ID,我们通过自定义 Factory 传递参数:

class LiveRoomViewModelFactory(private val roomId: String) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return LiveRoomViewModel(roomId) as T
    }
}

// Activity 中获取
val viewModel: LiveRoomViewModel by viewModels { 
    LiveRoomViewModelFactory("123") 
}

这种方式适合简单场景,但项目复杂后容易产生样板代码。后来我们引入 ​Hilt,用 @HiltViewModel 自动注入:

@HiltViewModel
class LiveRoomViewModel @Inject constructor(
    private val roomRepo: RoomRepository, // 自动注入仓库
    @Assisted private val savedState: SavedStateHandle // 自动处理 SavedState
) : ViewModel()

Hilt 减少了 60% 的模板代码,但编译时间增加了 15%。金融类 App 对编译速度敏感,我们保留了手动工厂;而社交 App 追求开发效率,全面切到了 Hilt。”


Q6:进程被杀后如何恢复 ViewModel 数据?​

面试官​:
“ViewModel 只能在内存存活,如果进程被系统杀死,数据怎么恢复?”

候选人​(结合系统机制与业务方案):
SavedStateHandle 是最后的防线。比如用户的草稿箱功能,我们通过 SavedStateHandle 保存临时内容:

class DraftViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    val content = savedStateHandle.getLiveData<String>("draft_content")
    
    fun saveDraft(text: String) {
        savedStateHandle["draft_content"] = text // 自动持久化到 Bundle
    }
}

SavedStateHandle 有 1MB 限制,大文件得另寻他法。在电商 App 的商品编辑页,我们结合 ​MMKV​ 存储图片路径,进程重启后从本地加载,数据恢复速度提升 80%。”


LiveData数据倒灌与数据丢失解决方案


面试官​:

“听说LiveData有个‘数据倒灌’的问题,你们项目里是怎么解决的?还有postValuesetValue用不好会丢数据,有什么优化方案?”


候选人​:

“这两个问题我们都踩过坑!先说数据倒灌——比如用户点击按钮触发一个导航事件,屏幕旋转后LiveData又把旧事件发一次,导致重复跳转。这就像快递员把昨天的包裹又送了一遍,收件人一脸懵。

我们试过三种方案:

  1. SingleLiveEvent​(临时工方案):
    自己封装一个只能消费一次的事件包装类:

    class SingleLiveEvent<T> : MutableLiveData<T>() {
        private val pending = AtomicBoolean(false)
    
        override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
            super.observe(owner) { t ->
                if (pending.compareAndSet(true, false)) {
                    observer.onChanged(t)
                }
            }
        }
    
        override fun setValue(value: T) {
            pending.set(true)
            super.setValue(value)
        }
    }

    但它的缺点是只能有一个观察者,多个Fragment监听时会丢事件,就像多个收件人抢一个包裹。

  2. 事件包装类+资源ID​(升级方案):
    Event类包装数据,加标记防止重复消费:

    open class Event<out T>(private val content: T) {
        var hasBeenHandled = false
        fun getContentIfNotHandled(): T? = if (hasBeenHandled) null else {
            hasBeenHandled = true
            content
        }
    }
    
    // ViewModel中
    val navigateEvent = MutableLiveData<Event<String>>()
    
    // Activity观察
    viewModel.navigateEvent.observe(this) { event ->
        event?.getContentIfNotHandled()?.let { route ->
            navigateTo(route)
        }
    }

    这相当于给包裹贴了“已签收”标签,避免重复处理。

  3. Kotlin的SharedFlow​(终极方案):
    直接弃用LiveData,改用协程的SharedFlow

    private val _toastEvents = MutableSharedFlow<String>()
    val toastEvents = _toastEvents.asSharedFlow()
    
    fun showToast(text: String) {
        viewModelScope.launch {
            _toastEvents.emit(text)
        }
    }
    
    // Activity中监听(需结合lifecycleScope)
    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            viewModel.toastEvents.collect { text ->
                Toast.makeText(context, text, LENGTH_SHORT).show()
            }
        }
    }

    SharedFlow默认不保留事件(replay=0),就像快递员只送新件,彻底根治倒灌。


postValuesetValue的数据丢失问题

面试官:

“那postValuesetValue的数据丢失问题呢?比如快速调用postValue导致只更新最后一次数据。”


候选人​:

“这个问题就像快餐店叫号——如果顾客疯狂连续按取餐铃,店员可能只处理最后一次请求。

问题根源​:

  • setValue()同步更新,直接在主线程修改数据,但只能在主线程调用。
  • postValue()异步更新,通过Handler切到主线程,但连续调用时会覆盖中间值。

解决方案​:

  1. ​主线程强制用setValue
    如果确定在主线程,直接使用setValue避免异步延迟:

    fun updateData() {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            liveData.value = newData
        } else {
            liveData.postValue(newData)
        }
    }
  2. 队列化更新​(解决postValue覆盖):
    使用ChannelFlow缓冲更新请求:

    private val _dataChannel = Channel<Data>()
    val dataFlow = _dataChannel.receiveAsFlow()
    
    fun updateData(newData: Data) {
        viewModelScope.launch {
            _dataChannel.send(newData)
        }
    }
    
    // Activity中合并到LiveData
    lifecycleScope.launch {
        viewModel.dataFlow.collectLatest { data ->
            liveData.value = data
        }
    }
  3. 合并高频更新​:
    使用conflate操作符合并快速连续的值:

    val liveData = MutableLiveData<Data>()
    val dataFlow = liveData.asFlow().conflate()
    
    // 收集时会自动合并中间值
    lifecycleScope.launch {
        dataFlow.collect { data ->
            updateUI(data)
        }
    }

面试官​:

“如果一定要用postValue,如何确保中间数据不丢失?比如实时统计用户输入字符数。”


候选人​:

“这个场景需要保证顺序性和完整性,我们做过一个输入法统计功能,解决方案是:

  1. 使用原子操作累加​:
    在ViewModel中维护原子计数器,避免直接传递中间值:

    private val _inputCount = AtomicInteger(0)
    val inputCount: LiveData<Int> get() = _liveCount
    private val _liveCount = MutableLiveData<Int>()
    
    fun onInput(text: String) {
        // 子线程更新
        val count = text.length
        _inputCount.set(count)
        _liveCount.postValue(count) // 虽然可能覆盖,但最终值正确
    }
  2. 差异更新​:
    只有数据变化超过阈值时才通知UI:

    var lastSentCount = 0
    fun onInput(text: String) {
        val newCount = text.length
        if (abs(newCount - lastSentCount) > 5) { // 每变化5个字符才更新
            lastSentCount = newCount
            _liveCount.postValue(newCount)
        }
    }
  3. ​**改用StateFlow**​:
    StateFlow天然支持并发安全更新:

    private val _inputCount = MutableStateFlow(0)
    val inputCount: StateFlow<Int> = _inputCount
    
    fun onInput(text: String) {
        viewModelScope.launch(Dispatchers.Default) {
            _inputCount.emit(text.length) // 线程安全
        }
    }

网站公告

今日签到

点亮在社区的每一天
去签到