Android使用多模块+MVI+Koin+Flow构建项目框架

news/2025/1/15 6:47:10/

Android使用多模块+MVI+Koin+Flow构建项目框架

  • 前言
    • 模块路由
      • 核心接口,用于在模块中绑定路由对应关系
      • 使用建造者模式定义传递的参数
      • 创建路由加载核心类, 本质上包含了一个全局路由表
      • 跳转类
      • 使用
    • MVI封装介绍,本质上使用flow作为核心
      • 定义数据类型,该类为抽象类为了适应不同的结果集
      • 定义接口
      • 核心类,实现mvi,带有状态
      • 拓展BaseViewModel
      • 使用
    • 总结
    • 下载

前言

目前mvi架构挺火的,结合其它人提供的思路将其整合到了新框架中, 本想在路由框架上使用ARouter的, 但ARouter不支持AndroidX虽然也能用, 但是作为强迫症患者还是想着简化与替代, 挺看好TheRouter但是当把项目配置升级到最新版本也很难导入进来就只能尝试自己手写了,路由模块可以自行替代,网络请求层也可以自行替换成如ktor等,毕竟mvi的核心是使用一个异步的函数,如果是同步回调方式的异步可以安装示例中的代码将回调转换成协程的异步函数。

模块路由

模块路由是采用手动注入的方式, 不想使用反射这种性能损耗较高的方案, 如果项目中使用其它框架可以忽略,路由设计思路比较简单,本质上也是使用Android原生跳转方式,采用模块注入activity的方式。

核心接口,用于在模块中绑定路由对应关系

interface IRegModule {// key: 路由,value: activityClassNamefun regRoute(): Map<String, Class<*>>
}

使用建造者模式定义传递的参数

class RouterBundle private constructor(private val builder: Builder) {companion object {inline fun build(block: Builder.() -> Unit = {}) =Builder().apply(block).build()}/** 数据 **/fun bundle() = builder.bundle/** 是否有回调 **/fun activityResult() = builder.registerForActivityResultclass Builder {// 用于android序列化数据val bundle = Bundle()// 如果指定该参数则表示有回调var registerForActivityResult:  ActivityResultLauncher<Intent>? = null/** 外部无需在调用该函数 **/fun build() = RouterBundle(this)fun setString(key: String, value: String) =bundle.putString(key, value)fun setStringArray(key: String, value: Array<out String>) =bundle.putStringArray(key, value)fun setBool(key: String, value: Boolean) =bundle.putBoolean(key, value)fun setFloat(key: String, value: Float) =bundle.putFloat(key, value)fun setLong(key: String, value: Long) =bundle.putLong(key, value)fun setDouble(key: String, value: Double) =bundle.putDouble(key, value)fun setSerializable(key: String, value: Serializable) =bundle.putSerializable(key, value)fun setParcelable(key: String, value: Parcelable) =bundle.putParcelable(key, value)fun setParcelableArray(key: String, value: Array<out Parcelable>) {bundle.putParcelableArray(key, value)}fun setActivityResult(result: ActivityResultLauncher<Intent>) {registerForActivityResult = result}}
}

创建路由加载核心类, 本质上包含了一个全局路由表

class RouterCore private constructor(private val builder: Builder){companion object {inline fun build(block: Builder.() -> Unit = {}) =Builder().apply(block).build()}fun routeTable() = builder.routeModuleclass Builder {val routeModule = mutableMapOf<String, Class<*>>()fun build() = RouterCore(this)/*** 注册模块*/fun regionModule(module: IRegModule) {routeModule.putAll(module.regRoute())}}
}

跳转类

class RouterAction {private lateinit var core: RouterCorecompanion object {private val instance = RouterActionHolder.holder/** 初始化所有模块, 未初始化禁止加载 **/fun init(block: RouterCore.Builder.() -> Unit = {}) =instance.init(block)fun start(ctx: Context, path: String, block: RouterBundle.Builder.() -> Unit = {}) =instance.start(ctx, path, block)}private object RouterActionHolder {val holder = RouterAction()}/*** 初始化所有模块, 未初始化静止加载*/fun init(block: RouterCore.Builder.() -> Unit = {}) {core = RouterCore.build(block)}/*** 启动跳转*/fun start(ctx: Context, path: String, block: RouterBundle.Builder.() -> Unit = {}) {val cls = core.routeTable()[path] ?: throw NullPointerException("未找到对应路由地址")val bundle = RouterBundle.build(block)ActivityAction.startActivity(ctx, cls, bundle)}
}

使用

// Application
fun onCreate() {// 配置路由RouterAction.init {regionModule(AppRouterTable())}
}
// 注册
class AppRouterTable: IRegModule {// 返回当前模块中的路由与activity对应关系override fun regRoute(): Map<String, Class<*>> = mutableMapOf(RouterManager.BANNER to BannerActivity::class.java)
}
// 跳转
fun initListener() = bindingRun {btnNext.setOnClickListener {startRouter(RouterManager.BANNER) {params("hello" to "这是测试数据传递")}}
}

MVI封装介绍,本质上使用flow作为核心

定义数据类型,该类为抽象类为了适应不同的结果集

abstract class BaseData<T> {/*** 适用于当前请求是否成功, 子类必须要重写*/abstract fun isSuccess(): Boolean/*** 用于返回实际数据*/abstract fun data(): T?/*** 可以是业务错误, 也可以是http状态码*/abstract fun errCode(): Int?/*** 请求成功但返回失败*/abstract fun errMsg(): String?
}

定义接口

/*** 需要展示的状态,对应 UI 需要的数据*/
interface IUiState/*** 来自用户和系统的是事件,也可以说是命令*/
interface IUiEvent/*** 单次状态,即不是持久状态,类似于 EventBus ,例如加载错误提示出错、或者跳转到登录页,它们只执行一次,通常在 Compose 的副作用中使用*/
interface IUiEffect

核心类,实现mvi,带有状态

abstract class BaseViewModel<UiState : IUiState, UiEvent : IUiEvent, UiEffect : IUiEffect> :ViewModel() {private val initialState: UiState by lazy { initialState() }private val _uiState: MutableStateFlow<UiState> by lazy { MutableStateFlow(initialState) }/** 对外暴露需要改变ui的控制 */val uiState: StateFlow<UiState> by lazy { _uiState }// 使用Channel创建数据流, Channel是消费者模式的, 保证了请求的正确性private val _uiEvent: Channel<UiEvent> = Channel()private val uiEvent: Flow<UiEvent> = _uiEvent.receiveAsFlow()// 状态private val _uiEffect: Channel<UiEffect> = Channel()val uiEffect: Flow<UiEffect> = _uiEffect.receiveAsFlow()init {// 初始化viewModelScope.launch {uiEvent.collect {// flow.collect 接受数据handleEvent(_uiState.value, it)}}}/*** 配置响应数据, 表示接受到数据后需要更新ui*/protected abstract fun initialState(): UiState/*** 处理响应*/protected abstract suspend fun handleEvent(state: UiState, event: UiEvent)/*** 通知数据流改变状态*/protected fun sendState(copy: UiState.() -> UiState) {_uiState.update { copy(_uiState.value) }}/*** 发送事件, 外部调用*/fun sendEvent(event: UiEvent) {viewModelScope.launch {_uiEvent.send(event)}}/*** 发送状态*/protected fun sendEffect(effect: UiEffect) {viewModelScope.launch { _uiEffect.send(effect) }}
}

拓展BaseViewModel

object BaseViewModelExt {/*** 简化状态调用*/fun <S : IUiState, E : IUiEvent, F : IUiEffect> BaseViewModel<S, E, F>.collectSideEffect(lifecycleOwner: LifecycleOwner,lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,sideEffect: (suspend (sideEffect: F) -> Unit),): Job = lifecycleOwner.lifecycleScope.launch {uiEffect.flowWithLifecycle(lifecycleOwner.lifecycle, lifecycleState).collect { sideEffect(it) }}/*** 拓展 Flow的使用, 用于替代, 同时将flow需要协程作用域提取出来, 以同步方式对外直接调用*      lifecycleScope.launchWhenStarted { } // 不要使用这种过时的方式*/fun <T> Flow<T>.collectIn(lifecycleOwner: LifecycleOwner,minActiveState: Lifecycle.State = Lifecycle.State.STARTED,collector: FlowCollector<T>): Job = lifecycleOwner.lifecycleScope.launch {// 必须在协程的作用域里面flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState).collect(collector)}
}

使用

  1. 定义状态、事件、响应
internal data class TestThreeState(val banner: MyUIBanner,val testBanner: MyUITestBanner
) : IUiStateinternal sealed class MyUIBanner {object INIT : MyUIBanner()data class SUCCESS(val models: List<BannerDto>?) : MyUIBanner()
}internal sealed class MyUITestBanner {object INIT : MyUITestBanner()data class SUCCESS(val models: String?) : MyUITestBanner()
}internal sealed interface TestThreeEvent : IUiEvent {object Banner : TestThreeEventobject TestBanner: TestThreeEvent
}/*** 加载动画事件*/
internal sealed interface LoadingEffect : IUiEffect {/*** 用于判断是否需要显示加载动画*/data class IsLoading(val show: Boolean) : LoadingEffect/*** 如果http状态是401则会触发该函数*/data class OnAuthority(val code: Int) : LoadingEffect
}
  1. view_model
internal class TestThreeViewModel : BaseViewModel<TestThreeState, TestThreeEvent, LoadingEffect>() {// 这里可以使用koin依赖注入private val response: IBannerApi by lazy {RetrofitManager.getService(IBannerApi::class.java)}// 初始化override fun initialState(): TestThreeState =TestThreeState(MyUIBanner.INIT, MyUITestBanner.INIT)// 处理事件override suspend fun handleEvent(state: TestThreeState, event: TestThreeEvent) = when (event) {TestThreeEvent.Banner -> banner(true, true,request = { response.getBanner() },onSuccess = { sendState { copy(banner = MyUIBanner.SUCCESS(it)) } })TestThreeEvent.TestBanner -> banner(true, false,request = { response.getTestBanner() },onSuccess = { sendState { copy(testBanner = MyUITestBanner.SUCCESS(it)) } })}private suspend fun <T> banner(isStart: Boolean, isClone: Boolean,request: suspend () -> BaseData<T>,onSuccess: (T?) -> Unit) {if (isStart) {sendEffect(LoadingEffect.IsLoading(true))}try {val body = request()if (body.isSuccess()) { // 请求成功onSuccess(body.data())
//                sendState { copy(banner = MyUIBanner.SUCCESS(body.data())) }} else { // 请求失败failCallback(body.errCode()) {sendEffect(LoadingEffect.OnAuthority(it))}}} catch (e: Exception) {errorCallback(e) {sendEffect(LoadingEffect.OnAuthority(it))}} finally {// 不管请求是否成功, 最终都需要关闭dialog加载动画if (isClone) {sendEffect(LoadingEffect.IsLoading(false))}}}/*** 通用异常处理*/private suspend fun errorCallback(e: Exception, onAuthority: suspend (Int) -> Unit) {when (e) {is HttpException -> { // 请求异常failCallback(e.code(), onAuthority)}is ConnectException -> ToastUtils.showShort("当前无网络连接,请连接网络后再试")is InterruptedIOException ->ToastUtils.showShort("当前连接超时,请检查网络是否可用")is JsonParseException, is JSONException, is ParseException ->ToastUtils.showShort("数据解析错误,请稍后再试!")else -> ToastUtils.showShort("未知异常")}}/*** 处理请求成功, 但是实际上是失败的返回, 如401等*/private suspend fun failCallback(errCode: Int?, onAuthority: suspend (Int) -> Unit) {errCode?.let {when (it) {400 -> ToastUtils.showShort("请求错误")401 -> onAuthority(it)404 -> ToastUtils.showShort("无法找到服务器")403 -> ToastUtils.showShort("您还没有权限访问该功能")500 -> ToastUtils.showShort("服务器异常")else -> ToastUtils.showShort("网络错误")}}}
}
  1. activity使用
override fun initObserve() = bindingRun {// 拓展函数, 用于简化调用viewModel.collectSideEffect(this@TestThreeActivity) { sideEffect ->when(sideEffect) {is LoadingEffect.IsLoading -> logErr("LoadingEffect.IsLoading = ${sideEffect.show}")is LoadingEffect.OnAuthority -> logErr("LoadingEffect.OnAuthority = ${sideEffect.code}")}}// 拓展函数用于只处理单个事件viewModel.uiState.map { it.banner }.collectIn(this@TestThreeActivity, Lifecycle.State.STARTED) { uiState ->when(uiState) {MyUIBanner.INIT -> { logErr("初始化状态") }is MyUIBanner.SUCCESS -> {logErr("请求成功 >>>>> success")val body = uiState.modelsif (body != null) {tvResult.text = body.gsonToJson()}}}}viewModel.uiState.map { it.testBanner }.collectIn(this@TestThreeActivity, Lifecycle.State.STARTED) { uiState ->when(uiState) {MyUITestBanner.INIT -> { logErr("初始化状态") }is MyUITestBanner.SUCCESS -> {logErr("请求成功 >>>>> success")logErr(">>>>>>>>>>> 开始执行第二次网络请求 ")viewModel.sendEvent(TestThreeEvent.Banner)
//                        val body = uiState.models
//                        if (body != null) {
//                            tvResult.text = body.gsonToJson()
//                        }}}}}

总结

Android最近几年项目框架发展比较快速,从mvp的百花齐放,到viewmodel+databinding的mvvm,在到mvi,本质上是为了解决耦合与项目可维护性以及编码规范的问题,关于ui的一些思考:可以考虑使用自定义view将部分业务逻辑封装在自定义view中,这样既减少activity的代码量也更容易定期问题的发生一定程度上减少耦合度。

下载

示例链接1: MVI_DEMO(初版)


http://www.ppmy.cn/news/104269.html

相关文章

I2C通信协议MPU6050

目录 I2C通信协议 硬件 软件 I2C时序 MPU6050 I2C通信协议 硬件 为了避免总线没协调好导致电源短路&#xff0c;配置为开漏输出&#xff0c;所有设备输出低电平不输出高电平&#xff0c;即右图。又为了避免高电平造成的引浮空&#xff0c;&#xff08;第三点&#xff09;总…

浏览器兼容性:CSS 回退属性

一个 CSS 类可以由许多声明组成&#xff0c;每个声明都具有property: value语法的语法&#xff1a; .cls {property: value; } 可以为同一个属性设置不同的值。稍后出现的值会覆盖它之前的值。浏览器将尝试使用最后的声明。在无法识别声明的情况下&#xff0c;它将回退到以前…

MapReduce【数据压缩】

目录 概述 压缩的优缺点 优点 缺点 压缩的原则 MapReduce支持的压缩编码 压缩算法对比 压缩性能比较 压缩方式的选择 Gzip 压缩 Bzip2 压缩 Lzo 压缩 Snappy 压缩 压缩位置选择 压缩位置选择 1、输入端采用压缩 2、Mapper输出采用压缩 3、Reducer输出采用压缩…

SpringBoot框架面试专题(初级-中级)-第一节

欢迎大家一起探讨相关问题&#xff0c;我们共同进步&#xff0c;喜欢的话可以关注点赞&#xff0c;后续会持续更新&#xff0c;谢谢&#xff5e; 问题&#xff1a; 1.Spring Boot是什么&#xff1f;它与Spring Framework有什么区别&#xff1f; 解析&#xff1a; Spring Bo…

Rust 笔记:WebAssembly 的 JavaScript API

WebAssembly WebAssembly 的 JavaScript API 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263?spm1001.2101.3001.5343 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/ar…

【研究生学术英语读写教程翻译 中国科学院大学Unit10】

研究生学术英语读写教程翻译 中国科学院大学Unit10 Unit 10 The Doctors Dilemma: ls lt Ever Good to Do Harm?医生的困境:伤害永远是好事吗?Unit 10 The Doctor’s Dilemma: ls lt Ever Good to Do Harm? 医生的困境:伤害永远是好事吗? Gwen Adshead Medical knowled…

Clion开发STM32之OTA升级模块(最新完整版)

前言 程序分为上位机部分、BootLoader、App程序上位机程序使用的是C#进行开发&#xff0c;目前只做成控制台部分开发环境依然选择Clion芯片采用的是stm32f103vet6升级模块已和驱动层逻辑进行分离 BootLoader程序 Flash分区定义 头文件 #ifndef STM32F103VET6_PROJECT_APP_FL…

认识.Net MAUI跨平台框架

.NET MAUI概念: 全称: .NET 多平台应用 UI (.NET MAUI) 是一个开源的跨平台框架&#xff0c;前身是Xamarin.Forms ! 用于使用 C# 和 XAML 创建本机移动和桌面应用。 NET MAUI&#xff0c;共享代码库,可在 Android、iOS、macOS 和 Windows 上运行的应用 应用架构: github 地址…