您现在的位置是:首页 >技术交流 >Android使用多模块+MVI+Koin+Flow构建项目框架网站首页技术交流

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

吕子明 2024-07-04 11:18:00
简介Android使用多模块+MVI+Koin+Flow构建项目框架

前言

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

模块路由

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

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

interface IRegModule {
	// key: 路由,value: activityClassName
    fun 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.registerForActivityResult

    class 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.routeModule

    class Builder {
        val routeModule = mutableMapOf<String, Class<*>>()
        fun build() = RouterCore(this)

        /**
         * 注册模块
         */
        fun regionModule(module: IRegModule) {
            routeModule.putAll(module.regRoute())
        }
    }
}

跳转类

class RouterAction {

    private lateinit var core: RouterCore

    companion 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
) : IUiState

internal 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 : TestThreeEvent
    object 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.models
                        if (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(初版)

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。