Kotlin的魔能机甲——KtArmor网络调用封装(四)

2,042 阅读4分钟

前言

继上次分享KtArmor的基础使用方法, 在网络请求逻辑上,在调用上,总感觉不够优雅直观嵌套过深的问题,这样使得代码看起来臃肿,不美观。所以在这篇中,分享一下我在网络请求调用方面的 封装之路。希望大家喜欢~

准备

在演示实例过程,我才用采用的是 玩Android 提供的接口 API。 框架方面,我采用的是 Retrofit + OkHttp + Coroutine,示例演示是以 Kotlin + MVP 架构。如有不了解的同学,可以应当先去学习相关框架,不然观看效果不佳。

阶段一

class LoginActivity : MvpActivity<LoginContract.Presenter>(), LoginContract.View {

    ... 省略部分代码

    override fun initListener() {
        super.initListener()

        mBtnLogin.setOnClickListener {
            presenter.login(mEtAccount.str(), mEtPassword.str())
        }
    }
    
    ... 省略部分代码
}

以上是部分节选代码。这里我以登录功能为例,一般情况下在 LoginActivity 调用 Presenter 层发起 login 请求。

Presenter

class LoginPresenter(view: LoginContract.View) : BasePresenter<LoginContract.View>(view),
    LoginContract.Presenter {
    
    val presenterScope: CoroutineScope by lazy {
        CoroutineScope(Dispatchers.Main + Job())
    }

    override fun login(account: String, password: String) {

        ... 省略部分代码

        // 启动一个 ui Coroutine
        presenterScope.launch {
            tryCatch({
                view?.showLoading()
                
                val response = LoginModel.login(account, password)

                if (response.isSuccess()) {
                    response.data?.let { view?.loginSuc(it) }
                } else {
                    view?.loginFail(response.errorMsg)
                }
            }, {
                view?.loginError(it.toString())
            })
        }
        
        ... 省略部分代码
    }
}

Model

object LoginModel : BaseModel() {

    suspend fun login(account: String, password: String): BaseResponse<LoginRsp> {
        return launchIO { ApiManager.apiService.loginAsync(account, password).await() }
    }
}

在Presenter 层,启动一个ui Coroutine协程发起 login 请求,model 层就是简单调用 apiService 发起网络请求,然后根据response 判断是否 success 来调用对于 view 的逻辑。这是相对的 “直观” , 调用方式。 这里存在重复的代码 loginFail 这里 “一般” 都是显示一个 toast 提示用户相关信息。 代码上看起来臃肿,接下来是把这些步骤封装成Kt 扩展函数

阶段二(扩展)

fun launchUI(block: suspend CoroutineScope.() -> Unit, error: ((Throwable) -> Unit)? = null) {
        presenterScope.launch {
            tryCatch({
                block()
            }, {
                error?.invoke(it) ?: showException(it.toString())
            })
        }
}

我们先抽取一个 launchUI 方法到 Presenter 基类中,封装 Coroutine 启动方式,方便管理 coroutine。 并添加一个 error Block 默认方法, 如果没有传入 error 参数,默认显示 log (showException

fun <R> KResponse<R>.execute(success: ((R?) -> Unit)?, error: ((String) -> Unit)? = null) {
        if (this.isSuccess()) {
            success?.invoke(this.getKData())
        } else {
            error?.invoke(this.getKMessage()) ?: showError(this.getKMessage())
        }
}

然后添加一个 Response 扩展, 处理网络请求的逻辑。这里也是添加两个参数,一个 success,一个 error (可选参数,默认显示 toast)。下面是 替换成功扩展方法后代码。

class LoginPresenter(view: LoginContract.View) : BasePresenter<LoginContract.View>(view),
    LoginContract.Presenter {
    
    val presenterScope: CoroutineScope by lazy {
        CoroutineScope(Dispatchers.Main + Job())
    }

    override fun login(account: String, password: String) {

        ... 省略部分代码

        // 启动一个 ui Coroutine
       launchUI({
            view?.showLoading()
            LoginModel.login(account, password).execute({ loginRsp ->
                loginRsp?.let { view?.loginSuc(it) }
            }, {
                // TODO loginFail
            })
        }, {
            // TODO loginError
        })
        
        // 省略 TODO 后
        launchUI({
            view?.showLoading()
            LoginModel.login(account, password).execute({ loginRsp ->
                loginRsp?.let { view?.loginSuc(it) }
            })
        })
        
        ... 省略部分代码
    }
}

以上是抽取成 一个 扩展函数,简化了 Presenter 处理逻辑。TODO 标识的默认是可以省略,默认是 toast 一个 message。这样调用相对“清晰”一些。但如果 遇到逻辑复杂的话,会存在 嵌套过深的情况。最近学习了 DSL (domain-specific language 领域特定语言),引入 DSL 方式优化这些过程。Perfect !!

阶段三 (DSL)

fun <R> quickLaunch(block: Execute<R>.() -> Unit) {
        Execute<R>().apply(block)
}

inner class Execute<R> {

        private var successBlock: ((R?) -> Unit)? = null
        private var failBlock: ((String?) -> Unit)? = null
        private var exceptionBlock: ((Throwable?) -> Unit)? = null

        fun request(block: suspend CoroutineScope.() -> KResponse<R>?) {
            // LoginModel.login(account, password)
        
        
            launchUI({
                block()?.execute(successBlock, failBlock)
            }, exceptionBlock)
        }

        fun onSuccess(block: (R?) -> Unit) {
            // loginRsp?.let { view?.loginSuc(it) }
            
            this.successBlock = block
        }

        fun onFail(block: (String?) -> Unit) {
            //  message?.let { view?.loginFail(it) }
        
            this.failBlock = block
        }

        fun onException(block: (Throwable?) -> Unit) {
            // throwable?.let { view?.loginError(it.toString()) }
        
            this.exceptionBlock = block
        }
    }

DSL看起来比较抽象,在 Presenter 基类里,创建一个 内部类 Execute,声明对应的方法(request, onSuccess, onFail, onException), (在方法下面注释就是外面 Presenter 传入的 block,便于理解)。先存储对应 block,然后在 request 方法统一处理,具体逻辑和扩展差不多,这里就不多赘述了。我们看看封装成 DSL 后效果。

效果

quickLaunch<LoginRsp> {
        request { LoginModel.login(account, password) }

        onSuccess { loginRsp ->
            loginRsp?.let { view?.loginSuc(it) }
        }

        onFail { message ->
            message?.let { view?.loginFail(it) }
        }

        onException { throwable ->
            throwable?.let { view?.loginError(it.toString()) }
        }
}

最终效果是不是很“优雅”,减少了层级嵌套,从上而下,直观明了,反正我爱了哈哈哈哈。

最后

现已加入肯德基(KtArmor-MVP)豪华午餐,欢迎各位客官品尝~

还是那句话,KtArmor-MVP 封装了基本 MVP结构的框架,是一款小而美的框架,麻雀虽小五章俱全。封装了基础的功能,小的项目,或者测试项目可以直接拿来用,省时省力。希望大家喜欢~

最后,若有不妥,望小伙伴们指出。

Login例子源码

BasePresenter源码

Kotlin的魔能机甲——KtArmor(一)

Kotlin的魔能机甲——KtArmor插件篇(二)

Kotlin的魔能机甲——KtArmor(三)

KtArmor-MVP 源码传送门

感谢阅读,下次再见