解决Android开发中的痛点问题用Kotlin Flow

分析问题

根据上述总结,LiveData的确适合用来表示“状态”,但“事件”不应该是由某单个值表示。想要让View层顺序地消费每条事件,与此同时又不影响事件的发送,我的第一反应是使用一个阻塞队列来承载事件。但选型时我们要考虑以下问题,也是LiveData被推荐使用的优势 :

  1. 是否会发生内存泄漏,观察者的生命周期遭到销毁后能否自我清理

  2. 是否支持线程切换,比如LiveData保证在主线程感知变化并更新UI

  3. 不会在观察者非活跃状态下消费事件,比如LiveData防止因Activity停止时消费导致crash

方案一:阻塞队列

ViewModel持有阻塞队列,View层在主线程死循环读取队列内容。需要手动添加lifecycleObserver来保证线程的挂起和恢复,并且不支持协程。考虑使用kotlin协程中的Channel替代。

方案二: Kotlin Channel

Kotlin Channel和阻塞队列很类似,区别在于Channel用挂起的send操作代替了阻塞的put,用挂起的receive操作代替了阻塞的take。然后开启灵魂三问:

在生命周期组件中消费Channel是否会内存泄漏?

不会,因为Channel并不会持有生命周期组件的引用,并不像LiveData传入Observer式的使用。

是否支持线程切换?

支持,对Channel的收集需要开启协程,协程中可以切换协程上下文从而实现线程切换。

观察者非活跃状态下是否还会消费事件?

使用lifecycle-runtime-ktx库中的launchWhenX方法,对Channel的收集协程会在组件生命周期 < X时挂起,从而避免异常。也可以使用repeatOnLifecycle(State) 来在UI层收集,当生命周期 < State时,会取消协程,恢复时再重新启动协程。

看起来使用Channel承载事件是个不错的选择,并且一般来说事件分发都是一对一,因此并不需要支持一对多的BroadcastChannel(后者已经逐渐被废弃,被SharedFlow替代)

如何创建Channel?看一下Channel对外暴露可供使用的构造方法,考虑传入合适的参数。

public fun Channel(

// 缓冲区容量,当超出容量时会触发onBufferOverflow指定的策略

capacity: Int = RENDEZVOUS,

// 缓冲区溢出策略,默认为挂起,还有DROP_OLDEST和DROP_LATEST

onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,

// 处理元素未能成功送达处理的情况,如订阅者被取消或者抛异常

onUndeliveredElement: ((E) -> Unit)? = null

): Channel

首先Channel是热的,即任意时刻发送元素到Channel即使没有订阅者也会执行。所以考虑到存在订阅者协程被取消时发送事件的情况,即存在Channel处在无订阅者时的空档期收到事件情况。例如当Activity使用repeatOnLifecycle方法启动协程去消费ViewModel持有的Channel里的事件消息,当前Activity因为处于STOPED状态而取消了协程。

根据之前分析的诉求,空档期的事件不能丢弃,而应该在Activity回到活跃状态时依次消费。所以考虑当缓冲区溢出时策略为挂起,容量默认0即可,即默认构造方法即符合我们的需求

之前我们提到,BroadcastChannel已经被SharedFlow替代,那我们用Flow代替Channel是否可行呢?

方案三:普通Flow(冷流)

Flow is cold, Channel is hot。所谓流是冷的即流的构造器中的代码直到流被收集时才会执行,下面是个非常经典的例子:

fun fibonacci(): Flow = flow {

var x = BigInteger.ZERO

var y = BigInteger.ONE

while (true) {

emit(x)

x = y.also {

y += x

}

}

}

fibonacci().take(100).collect { println(it) }

如果flow构造器里的代码不依赖订阅者独立执行,上面则会直接死循环,而实际运行发现是正常输出。

那么回到我们的问题,这里用冷流是否可行?显然并不合适,因为首先直观上冷流就无法在构造器以外发射数据。

但实际上答案并不绝对,通过在flow构造器内部使用channel,同样可以实现动态发射,如channelFlow。但是channelFlow本身不支持在构造器以外发射值,通过Channel.receiveAsFlow操作符可以将Channel转换成channelFlow。这样产生的Flow“外冷内热”,使用效果和直接收集Channel几乎没有区别

private val testChannel: Channel = Channel()

private val testChannelFlow = testChannel.receiveAsFlow ()

复制代码

方案四:SharedFlow/StateFlow

首先二者都是热流,并支持在构造器外发射数据。简单看下它们的构造方法

public fun MutableSharedFlow(

// 每个新的订阅者订阅时收到的回放的数目,默认0

replay: Int = 0,

// 除了replay数目之外,缓存的容量,默认0

extraBufferCapacity: Int = 0,

// 缓存区溢出时的策略,默认为挂起。只有当至少有一个订阅者时,onBufferOverflow才会生效。当无订阅者时,只有最近replay数目的值会保存,并且onBufferOverflow无效。

onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND

)

复制代码

//MutableStateFlow等价于使用如下构造参数的SharedFlow

MutableSharedFlow(

replay = 1,

onBufferOverflow = BufferOverflow.DROP_OLDEST

)

复制代码

SharedFlow被Pass的原因主要有两个:

  1. SharedFlow支持被多个订阅者订阅,导致同一个事件会被多次消费,并不符合预期。

  2. 如果认为1还可以通过开发规范控制,SharedFlow的在无订阅者时会丢弃数据的特性则让其彻底无缘被选用承载必须被执行的事件

而StateFlow可以理解成特殊的SharedFlow,也就无论如何都会有上面两点问题。

当然,适合使用SharedFlow/StateFlow的场景也有很多,下文还会重点研究。

总结

对于想要在ViewModel层发射必须执行且只能执行一次的事件让View层执行时,不要再通过向LiveData postValue让View层监听实现。推荐使用Channel或者是通过Channel.receiveAsFlow方法创建的ChannelFlow来实现ViewModel层的事件发送。

解决问题

RoomViewModel.kt

class RoomViewModel : ViewModel() {

private val _effect = Channel = Channel ()

val effect = _effect. receiveAsFlow ()

private fun setEffect(builder: () -> Effect) {

val newEffect = builder()

viewModelScope.launch {

_effect.send(newEffect)

}

}

fun showToast(text : String) {

setEffect {

Effect.ShowToastEffect(text)

}

}

}

sealed class Effect {

data class ShowToastEffect(val text: String) : Effect()

}

RoomActivity.kt

class RoomActivity : BaseActivity() {

override fun initObserver() {

lifecycleScope.launchWhenStarted {

viewModel.effect.collect {

when (it) {

is Effect.ShowToastEffect -> {

showToast(it.text)

}

}

}

}

}

}

痛点二:Activity/Fragment通过共享ViewModel通信的问题


我们经常让Activity和其中的Fragment共同持有由Acitivity作为ViewModelStoreOwner构造的ViewModel,来实现Activity和Fragment、以及Fragment之间的通信。典型场景如下:

class MyActivity : BaseActivity() {

private val viewModel : MyViewModel by viewModels()

private fun initObserver() {

viewModel.countLiveData.observe { it->

updateUI(it)

}

}

private fun initListener() {

button.setOnClickListener {

viewModel.increaseCount()

}

}

}

class MyFragment : BaseFragment() {

private val activityVM : MyViewModel by activityViewModels()

private fun initObserver() {

activityVM.countLiveData.observe { it->

updateUI(it)

}

}

}

class MyViewModel : ViewModel() {

private val _countLiveData = MutableLiveData(0)

private val countLiveData : LiveData = _countLiveData

fun increaseCount() {

_countLiveData.value = 1 + _countLiveData.value ?: 0

}

}

简单来说就是通过让Activity和Fragment观察同一个liveData,实现一致性。

那如果是要在Fragment中调用Activity的方法,通过共享ViewModel可行吗?

发现问题

DialogFragment和Activity的通信

我们通常使用DialogFragment来实现弹窗,在其宿主Activity中设置弹窗的点击事件时,如果回调函数中引用了Activity对象,则很容易产生由横竖屏页面重建引发的引用错误。所以我们建议让Activity实现接口,在弹窗每次Attach时都会将当前附着的Activity强转成接口对象来设置回调方法。

class NoticeDialogFragment : DialogFragment() {

internal lateinit var listener: NoticeDialogListener

interface NoticeDialogListener {

fun onDialogPositiveClick(dialog: DialogFragment)

fun onDialogNegativeClick(dialog: DialogFragment)

}

override fun onAttach(context: Context) {

super.onAttach(context)

try {

listener = context as NoticeDialogListener

} catch (e: ClassCastException) {

throw ClassCastException((context.toString() +

" must implement NoticeDialogListener"))

}

}

}

class MainActivity : FragmentActivity(), NoticeDialogFragment.NoticeDialogListener {

fun showNoticeDialog() {

val dialog = NoticeDialogFragment()

dialog.show(supportFragmentManager, “NoticeDialogFragment”)

}

override fun onDialogPositiveClick(dialog: DialogFragment) {

// User touched the dialog’s positive button

}

override fun onDialogNegativeClick(dialog: DialogFragment) {

// User touched the dialog’s negative button

}

}

这样的写法不会有上述问题,但是随着页面上支持的弹窗变多,Activity需要实现的接口也越来越多,无论是对编码还是阅读代码都不是很友好。那有没有机会借用共享的ViewModel做点文章?

分析问题

我们想要向ViewModel发送事件,并让所有依赖它的组件接收到事件。比如在FragmentA点击按键触发事件A,其宿主Activity、相同宿主的FragmentB和FragmentA其本身都需要响应该事件。

有点像广播,且具有两个特性:

  1. 支持一对多,即一条消息支持被多个订阅者消费

  2. 具有时效性,过期的消息没有意义且不应该被延迟消费。

看起来EventBus是一种实现方法,但是已经有了ViewModel作为媒介再使用显然有些浪费,EventBus还是更适合跨页面、跨组件的通信。对比前面分析的几种模型的使用,发现SharedFlow在这个场景下非常有用武之地。

  1. SharedFlow类似BroadcastChannel,支持多个订阅者,一次发送多处消费。

  2. SharedFlow配置灵活,如默认配置 capacity = 0, replay = 0,意味着新订阅者不会收到类似LiveData的回放。无订阅者时会直接丢弃,正符合上述时效性事件的特点。

解决问题

class NoticeDialogFragment : DialogFragment() {

private val activityVM : MyViewModel by activityViewModels()

fun initListener() {

posBtn.setOnClickListener {

activityVM.sendEvent(NoticeDialogPosClickEvent(textField.text))

dismiss()

}

negBtn.setOnClickListener {

activityVM.sendEvent(NoticeDialogNegClickEvent)

dismiss()

}

}

}

class MainActivity : FragmentActivity() {

private val viewModel : MyViewModel by viewModels()

fun showNoticeDialog() {

val dialog = NoticeDialogFragment()

dialog.show(supportFragmentManager, “NoticeDialogFragment”)

}

fun initObserver() {

lifecycleScope.launchWhenStarted {

viewModel.event.collect {

when(it) {

is NoticeDialogPosClickEvent -> {

handleNoticePosClicked(it.text)

}

NoticeDialogNegClickEvent -> {

handleNoticeNegClicked()

}

}

}

}

}

}

class MyViewModel : ViewModel() {

private val _event: MutableSharedFlow = MutableSharedFlow ()

val event = _event. asSharedFlow ()

fun sendEvent(event: Event) {

viewModelScope.launch {

_event.emit(event)

}

}

}

这里通过_lifecycleScope.launchWhenX_启动协程其实并不是最佳实践,如果想要Activity在非活跃状态下直接丢弃收到的事件,应该使用repeatOnLifecycle来控制协程的开启和取消而非挂起。但考虑到DialogFragment的存活周期是宿主Activity的子集,所以这里没有大问题。

基于Flow/Channel的MVI架构


前面讲的痛点问题,实际上是为了接下来要介绍的MVI架构抛砖引玉。而MVI架构的具体实现,也就是将上述解决方案融合到模版代码中,最大程度发挥架构的优势。

MVI是什么

所谓MVI,对应的分别是Model、View、Intent

Model: 不是MVC、MVP里M所代指的数据层,而是指表征 UI 状态的聚合对象。Model是不可变的,Model与呈现出的UI是一一对应的关系。

View:和MVC、MVP里做代指的V一样,指渲染UI的单元,可以是Activity或者View。可以接收用户的交互意图,会根据新的Model响应式地绘制UI。

Intent:不是传统的Android设计里的Intent,一般指用户与UI交互的意图,如按钮点击。Intent是改变Model的唯一来源

对比MVVM的区别主要在哪?

  1. MVVM并没有约束View层与ViewModel的交互方式,具体来说就是View层可以随意调用ViewModel中的方法,而MVI架构下ViewModel的实现对View层屏蔽,只能通过发送Intent来驱动事件。

  2. MVVM架构并不强调对表征UI状态的Model值收敛,并且对能影响UI的值的修改可以散布在各个可被直接调用的方法内部。而MVI架构下,Intent是驱动UI变化的唯一来源,并且表征UI状态的值收敛在一个变量里。

基于Flow/Channel的MVI如何实现

抽象出基类BaseViewModel

UiState是可以表征UI的Model,用StateFlow承载(也可以使用LiveData)

UiEvent是表示交互事件的Intent,用SharedFlow承载

UiEffect是事件带来除了改变UI以外的副作用,用channelFlow承载

BaseViewModel.kt

abstract class BaseViewModel<State : UiState, Event : UiEvent, Effect : UiEffect> : ViewModel() {

/**

  • 初始状态

  • stateFlow区别于LiveData必须有初始值

*/

private val initialState: State by lazy { createInitialState() }

abstract fun createInitialState(): State

/**

  • uiState聚合页面的全部UI 状态

*/

private val _uiState: MutableStateFlow = MutableStateFlow(initialState)

val uiState = _uiState.asStateFlow()

/**

  • event包含用户与ui的交互(如点击操作),也有来自后台的消息(如切换自习模式)

*/

private val _event: MutableSharedFlow = MutableSharedFlow()

val event = _event.asSharedFlow()

/**

  • effect用作 事件带来的副作用,通常是 一次性事件 且 一对一的订阅关系

  • 例如:弹Toast、导航Fragment等

*/

private val _effect: Channel = Channel()

val effect = _effect.receiveAsFlow()

init {

subscribeEvents()

}

private fun subscribeEvents() {

viewModelScope.launch {

event.collect {

handleEvent(it)

}

}

}

protected abstract fun handleEvent(event: Event)

fun sendEvent(event: Event) {

viewModelScope.launch {

_event.emit(event)

}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

学习宝典

对我们开发者来说,一定要打好基础,随时准备战斗。不论寒冬是否到来,都要把自己的技术做精做深。虽然目前移动端的招聘量确实变少了,但中高端的职位还是很多的,这说明行业只是变得成熟规范起来了。竞争越激烈,产品质量与留存就变得更加重要,我们进入了技术赋能业务的时代。

不论遇到什么困难,都不应该成为我们放弃的理由!

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我针对Android程序员,我这边给大家整理了一套学习宝典!包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【算法合集】

【延伸Android必备知识点】

【Android部分高级架构视频学习资源】

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

**

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

学习宝典

对我们开发者来说,一定要打好基础,随时准备战斗。不论寒冬是否到来,都要把自己的技术做精做深。虽然目前移动端的招聘量确实变少了,但中高端的职位还是很多的,这说明行业只是变得成熟规范起来了。竞争越激烈,产品质量与留存就变得更加重要,我们进入了技术赋能业务的时代。

不论遇到什么困难,都不应该成为我们放弃的理由!

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我针对Android程序员,我这边给大家整理了一套学习宝典!包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

[外链图片转存中…(img-nEVOpdrL-1712050805092)]

【算法合集】

[外链图片转存中…(img-SI6iL04e-1712050805092)]

【延伸Android必备知识点】

[外链图片转存中…(img-XNI00Din-1712050805093)]

【Android部分高级架构视频学习资源】

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以为你解答关于Android MVP模式和使用Kotlin模拟登录的问题。 首先,MVP模式是一种架构模式,它将应用程序分为三个部分:模型(Model),视图(View)和控制器(Presenter)。MVP模式的主要目的是将业务逻辑和用户界面分离,从而实现更好的代码可读性和可维护性。 在Android开发,MVP模式通常用于处理复杂的UI和业务逻辑。下面是一个使用Kotlin模拟登录的Android MVP模式示例: 1. 创建一个包含登录逻辑的模型 ```kotlin interface LoginModel { fun login(username: String, password: String, callback: LoginCallback) } interface LoginCallback { fun onSuccess() fun onFailure(error: String) } class LoginModelImpl : LoginModel { override fun login(username: String, password: String, callback: LoginCallback) { // 模拟登录 if (username == "admin" && password == "123456") { callback.onSuccess() } else { callback.onFailure("用户名或密码错误") } } } ``` 2. 创建一个视图接口和一个实现视图接口的Activity ```kotlin interface LoginView { fun showLoading() fun hideLoading() fun showToast(msg: String) } class LoginActivity : AppCompatActivity(), LoginView { private lateinit var presenter: LoginPresenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) presenter = LoginPresenterImpl(this, LoginModelImpl()) btn_login.setOnClickListener { presenter.login(et_username.text.toString(), et_password.text.toString()) } } override fun showLoading() { // 显示加载 } override fun hideLoading() { // 隐藏加载 } override fun showToast(msg: String) { // 显示Toast } } ``` 3. 创建一个控制器(Presenter)来连接模型和视图 ```kotlin interface LoginPresenter { fun login(username: String, password: String) } class LoginPresenterImpl(private val view: LoginView, private val model: LoginModel) : LoginPresenter { override fun login(username: String, password: String) { view.showLoading() model.login(username, password, object : LoginCallback { override fun onSuccess() { view.hideLoading() view.showToast("登录成功") } override fun onFailure(error: String) { view.hideLoading() view.showToast(error) } }) } } ``` 在此示例,LoginActivity是视图,LoginPresenterImpl是控制器(Presenter),LoginModelImpl是模型。当用户单击“登录”按钮时,LoginActivity将调用LoginPresenterImpl的login方法,该方法将调用LoginModelImpl的login方法进行模拟登录。如果登录成功,LoginPresenterImpl将通知LoginActivity以显示“登录成功”消息;否则,它将通知LoginActivity显示错误消息。 希望这个示例可以帮助你理解Android MVP模式和使用Kotlin模拟登录的实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值