异步加载的页面框架

875 阅读3分钟

一般来说,我们创建一个页面大概是以下步骤:

  • 1 初始化UI,通过findViewById()或者ViewBinding
  • 2 请求服务器数据。
  • 3 数据到了之后渲染到UI上。

说白了就是:创建UI;请求数据;渲染UI。

如此简单的逻辑,却要写很多重复代码,那,有没有啥方法能简化一下呢?

那必须有。

不但有,而且可以让创建UI和请求数据一起发生,两步都搞定了再去渲染UI,写着简单,速度还快,最重要的是:能装逼。

首先,我们需要定义一个页面枚举,用它来区分不同的页面类型。

// 定义页面类型
enum class PageType(val tag: String) {
    PAGE_OTHER("OTHER"),
    PAGE_ACTIVITY("Activity"),
    PAGE_FRAGMENT("Fragment"),
    PAGE_DIALOG("Dialog");
}
// 定义页面接口,作为页面(Activity,Fragment,Dialog等)的上级
interface IPage {
    /**
     * 页面类型
     */
    fun pageType(): PageType
}

然后,我们需要定义一个数据标记,用于统一所有数据的类型。

// 定义数据标记,作为所有请求结果的统一点
data class RspResult<T>(val code: Int, val msg: String?, val result: T?) {
    fun success(): Boolean {
        return code == 200
    }
}

接着,我们来定义页面的顶层渲染逻辑。

// 制定UI渲染协议
interface IUIRender<VB : ViewBinding, T> {
    /**
     * 创建UI
     */
    fun createVB(inflater: LayoutInflater? = null, container: ViewGroup? = null): VB?

    /**
     * 请求数据
     */
    suspend fun getData(): RspResult<T>

    /**
     * 渲染数据
     */
    fun renderUI(vb: VB?, data: RspResult<T>)

    /**
     * 由于View创建速度快于数据,在View添加完毕后会回调这个,用来执行一些UI的初始化工作,比如点击事件。
     */
    fun onContentViewAdded(vb: VB) {

    }

    /**
     * 有的页面需要刷新行为,提供给页面调用,这会重新触发[renderUI]
     */
    fun refresh() {

    }
}

只看这个定义我们就知道页面需要做什么了。创建UI得到一个VB,请求数据得到一个RspResult,然后用这俩去渲染数据就完事了。这就是核心逻辑,也是我们一开始要解决的问题。

好,顶层逻辑定义完了,我们现在可以来实现具体的Activity的逻辑了。

abstract class AsyncVBActivity<VB : ViewBinding, T> : BaseActivity(), IUIRender<VB, T>, IPage {

    @Volatile
    protected var vb: VB? = null

    @Volatile
    protected var data: RspResult<T>? = null

    private val ioDispatcher = Dispatchers.IO
    private val uiDispatcher = Dispatchers.Main

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 在IO线程中创建出VB 和 Data,同时到达后切换到UI线程去处理
        lifecycleScope.launch(ioDispatcher) {
            val uiJob = async { createVBInternal() }
            val dataJob = async { getDataInternal() }
            renderUIInternal(uiJob.await(), dataJob.await())
        }
    }

    private fun createVBInternal(inflater: LayoutInflater? = null, container: ViewGroup? = null): VB? {
        val realVB = createVB(inflater, container)
        log("createVBInternal: $realVB")
        if (realVB == null) return null
        this@AsyncVBActivity.vb = realVB
        lifecycleScope.launch(uiDispatcher) {
            setContentView(realVB.root)
            onContentViewAdded(realVB)
        }
        return realVB
    }

    private suspend fun getDataInternal(): RspResult<T> {
        val realData = getData()
        log("getDataInternal: $realData")
        this.data = realData
        return realData
    }

    /**
     * 创建默认VB,子类可以自行创建从而避免反射带来的开销
     */
    override fun createVB(inflater: LayoutInflater?, container: ViewGroup?): VB? {
        log("createVB: by $this")
        // 这里默认是通过反射来创建ViewBinding
        return createViewBinding(this)
    }

    /**
     * 这里可以做一些校验检测
     * 1 vb是否添加空校验
     * 2 数据校验/拦截处理
     */
    private suspend fun renderUIInternal(vb: VB?, data: RspResult<T>) {
        withContext(uiDispatcher) {
            log("renderUIInternal: $vb -> $data")
            renderUI(vb, data)
        }
    }

    override fun onContentViewAdded(vb: VB) {
        log("onContentViewAdded: $vb")
    }

    override fun refresh() {
        log("refresh")
        lifecycleScope.launch(ioDispatcher) {
            renderUIInternal(this@AsyncVBActivity.vb, getDataInternal())
        }
    }

    override fun pageType(): PageType {
        return PageType.PAGE_ACTIVITY
    }

    private fun log(msg: String) {
        Ln.debug("[AsyncVBActivity]: {$msg}")
    }
}

这里的逻辑也很简单,我们在IO线程创建UI以及请求数据。这两个都拿到后,就放在UI线程去进行渲染。到这里问题其实已经解决了。

这里附上反射创建ViewBinding的代码。

// 通过反射来创建ViewBinding
fun <T : ViewBinding?> createViewBinding(activity: Activity): T? {
        var viewBinding: T? = null
        var cls: Class<*>? = null
        var type: Type? = activity.javaClass.genericSuperclass ?: return viewBinding
        while (type !is ParameterizedType && type is Class<*>) {
            type = type.genericSuperclass
        }
        if (type is ParameterizedType) {
            if (type.actualTypeArguments[0] is Class<*>) {
                cls = type.actualTypeArguments[0] as Class<*>
            }
        } else {
            return viewBinding // 如果所有的父类都不是ViewBinding 的泛型类型,直接返回null
        }
        try {
            if (cls == null) return viewBinding
            val inflate = cls.getDeclaredMethod("inflate", LayoutInflater::class.java)
            viewBinding = inflate.invoke(null, activity.layoutInflater) as T
        } catch (e: NoSuchMethodException) {
            Ln.e(e)
        } catch (e: IllegalAccessException) {
            Ln.e(e)
        } catch (e: InvocationTargetException) {
            Ln.e(e)
        }
        return viewBinding
    }

逻辑已经写完了,来个例子:

class TestActivity : AsyncVBActivity<ActivityTestBinding, User>() {
 
    override suspend fun getData(): RspResult<User> {
        // 这里是一个挂起函数,返回用户信息,使用suspendCancellableCoroutine实现。
        return ApiUserService.getUserInfo(uid=10086)
    }

    override fun renderUI(vb: ActivityTestAbBinding?, data: RspResult<User>) {
        vb ?: return
        if (!data.success()) return

        // 这里就可以直接拿到user,来渲染UI了。
        vb.tvName.text = data.result.name
        vb.tvAge.text = "${data.result.age}"

        // 可以直接刷新
        vb.refresh.setSafeClickListener {
            refresh()
        }
    }
}

只需要重载两个函数,一个请求数据的,一个渲染UI的。

Fragment也是一样的,只不过FragmentonCreateView()需要返回一个View,而我们的View又是异步创建的,怎么返回呢?这里可以先创建一个空的View(FrameLayout)来返回,同时去异步创建真实的View,等到真实的View创建完了,再添加到这个FrameLayout上即可。

abstract class AsyncVBFragment<VB : ViewBinding, T> : Fragment(), IUIRender<VB, T>, IPage {

    @Volatile
    private var vb: VB? = null

    @Volatile
    private var data: RspResult<T>? = null

    private val ioDispatcher = Dispatchers.IO
    private val uiDispatcher = Dispatchers.Main

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        // 使用一个空的ViewGroup占位,真实的View创建完成再来替换
        val emptyView = inflater.inflate(R.layout.layout_empty_view, container, true) as ViewGroup
        // 在IO线程中创建出VB 和 Data,同时到达后切换到UI线程去处理
        viewLifecycleOwner.lifecycleScope.launch(ioDispatcher) {
            val uiJob = async { createVBInternal(emptyView) }
            val dataJob = async { getDataInternal() }
            renderUIInternal(uiJob.await(), dataJob.await())
        }
        return emptyView
    }

    private fun createVBInternal(
        holderView: ViewGroup,
        inflater: LayoutInflater? = null,
        container: ViewGroup? = null
    ): VB? {
        val realVB = createVB(inflater, container)
        log("createVBInternal: $realVB")
        if (realVB == null) return null
        this@AsyncVBFragment.vb = realVB
        viewLifecycleOwner.lifecycleScope.launch(uiDispatcher) {
            holderView.addView(realVB.root)
            onContentViewAdded(realVB)
        }
        return realVB
    }

    private suspend fun getDataInternal(): RspResult<T> {
        val realData = getData()
        log("getDataInternal: $realData")
        this.data = realData
        return realData
    }

    override fun createVB(inflater: LayoutInflater?, container: ViewGroup?): VB? {
        log("createVB: by $this")
        if(inflater == null) return null
        return ViewBindingUtils.createViewBinding(this, inflater, container)
    }

    /**
     * 这里会等到数据过来后,才把真实的View添加到Fragment上。
     * 如果需要提前添加,则可以在uiJob完成后就添加
     */
    private suspend fun renderUIInternal(vb: VB?, data: RspResult<T>) {
        withContext(uiDispatcher) {
            log("renderUIInternal: $vb -> $data")
            renderUI(vb, data)
        }
    }

    override fun onContentViewAdded(vb: VB) {
        log("onContentViewAdded: $vb")
    }

    override fun refresh() {
        log("refresh")
        lifecycleScope.launch(ioDispatcher) {
            renderUIInternal(this@AsyncVBFragment.vb, getDataInternal())
        }
    }

    override fun pageType(): PageType {
        return PageType.PAGE_FRAGMENT
    }

    private fun log(msg: String) {
        Ln.debug("[AsyncVBFragment]: {$msg}")
    }
}

写到这里,相信你都懂了,我就不BB了,具体自己总结吧。