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)}
}
使用
- 定义状态、事件、响应
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
}
- 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("网络错误")}}}
}
- 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(初版)