【Android】UI搭积木,关于Shatter的改动与实战

872 阅读11分钟

Shatter碎片的改动与实战

前言

在前文中我们初步的实现的 Shatter 的定义【传送门】,支持了声明周期的感应能进行资源的释放。

我们也能获取到 lifecycleOwner 从而实现内部的 ViewModel 支持,从而实现布局与逻辑的绑定。

除此之外还有很多的小功能,例如异步加载,通知管理,子布局加载等等特性,有兴趣的可以回顾一下,

确实在一些复杂的可重用的布局中,我们是能很方便的开发与功能更新了。但是在实际开发的过程中我还是遇到了一些痛点,所以单独出一篇文章记录一下。

一、Shatter的痛点与改进

痛点如下:

  1. 正确的索引与布局位置的添加
  2. 移除和寻找Shatter
  3. 删除Shatter的逻辑
  4. 去掉Flow监听兼容Java代码

由于我们创建了 Shatter 之后,可能是重复的,就是说一个布局中可能线性的排列了多个 AShatter ,我们发现在异步布局加载的时候会有索引错乱的问题,这里做一次修复。

同步加载布局无所谓,直接加载完成设置数据即可,但是当我们选择异步加载布局的方案加载布局的时候,我们希望能有加载完成的回调,可以方便设置数据或监听,这里做了功能的扩展。

当我们移除布局或者刷新布局的时候,我们需要重新添加 Shatter,此时我们记录的 ShatterManager 中的数据是不准确的,所以我加上了移除 Shatter 的逻辑。

如果一个布局中有多个 AShatter ,AShatter 中有多个 BShatter,我们需要找出对应的Shatter类型的集合,这里我加上了按类型的查找。

如果查找出来的 Shatter 对应的索引不正确,那么获取它对应的数据的索引也是错误的,我们需要做出处理。

同事由于之前的 Flow 是 Kotlin 的特性,对于一些 Java 的老项目不友好,索引移除了内置的消息通知,让用户自行处理。

这些就是我开发中总结出来的痛点与解决方案,这里做出更新:

核心的改动代码,ShatterManager:

class ShatterManager(private val lifecycleOwner: LifecycleOwner) : LifecycleEventObserver {

    private val shatters = mutableListOf<Shatter>()

    internal val cache = ShatterCache()

    companion object {
        //管理全局上下文
        lateinit var applicationContext: Context

        //初始化框架,接收上下文
        @JvmStatic
        fun init(application: Application) {
            applicationContext = application.applicationContext
        }
    }

    init {
        lifecycleOwner.lifecycle.removeObserver(this)
        //添加了生命周期的监听
        lifecycleOwner.lifecycle.addObserver(this)
    }


    //这里只对Destroy生命周期回调做了处理,自动处理页面对应的Shatter销毁逻辑
    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        shatters.forEach { it.onStateChanged(source, event) }

        if (event == Lifecycle.Event.ON_START) {
            shatters.forEach { it.onStart() }

        } else if (event == Lifecycle.Event.ON_RESUME) {
            shatters.forEach { it.onResume() }

        } else if (event == Lifecycle.Event.ON_PAUSE) {
            shatters.forEach { it.onPause() }

        } else if (event == Lifecycle.Event.ON_STOP) {
            shatters.forEach { it.onStop() }

        } else if (event == Lifecycle.Event.ON_DESTROY) {

            shatters.forEach { it.onDestroy() }
            destroy()
            source.lifecycle.removeObserver(this)
        }
    }

    fun onNewIntent(intent: Intent?) {
        if (lifecycleOwner is FragmentActivity)
            shatters.forEach { it.onNewIntent(intent) }
    }

    fun onRestart() {
        if (lifecycleOwner is FragmentActivity)
            shatters.forEach { it.onRestart() }
    }

    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        shatters.forEach { it.onActivityResult(requestCode, resultCode, data) }
    }

    fun enableOnBackPressed(): Boolean {
        return if (lifecycleOwner is FragmentActivity) {
            shatters.find { !it.enableOnBackPressed() } == null
        } else {
            true
        }
    }

    fun onHiddenChanged(isHidden: Boolean) {
        if (lifecycleOwner is Fragment)
            shatters.forEach { it.onHiddenChanged(isHidden) }
    }

    /**
     * 入口1 添加Shatter到指定ID的容器中,
     *
     * @param containViewId 指定添加容器的布局Id
     * @param shatter 待添加的子碎片对象
     * @param isAsync 是否以异步加载的方式加载布局
     * @param index 如果有多个布局添加,指定添加到容器中的索引位置
     *
     */
    fun addShatter(@IdRes containViewId: Int, shatter: Shatter, isAsync: Boolean = false, index: Int = -1) = apply {
        addShatter(containViewId, shatter, isAsync, index, null)
    }

    fun addShatter(@IdRes containViewId: Int, shatter: Shatter, isAsync: Boolean = false, index: Int = -1, callback: (() -> Unit)?) = apply {
        shatter.shatterManager = this
        val view: View? = when (lifecycleOwner) {
            is FragmentActivity -> lifecycleOwner.findViewById(containViewId)
            is Fragment -> lifecycleOwner.requireView().findViewById(containViewId)
            else -> null
        }
        addShatter(view, shatter, isAsync, index, callback)
    }

    /**
     * 入口2 添加Shatter到指定的容器中,
     *
     * @param containView 指定添加容器的ViewGroup对象
     * @param shatter 待添加的子碎片对象
     * @param isAsync 是否以异步加载的方式加载布局
     * @param index 如果有多个布局添加,指定添加到容器中的索引位置
     *
     */
    fun addShatter(containView: View?, shatter: Shatter, isAsync: Boolean = false, index: Int = -1) = apply {
        addShatter(containView, shatter, isAsync, index, null)
    }

    /**
     * 总入口
     */
    fun addShatter(containView: View?, shatter: Shatter, isAsync: Boolean = false, index: Int = -1, callback: (() -> Unit)?) = apply {
        shatter.shatterManager = this
        shatter.containView = containView
        shatter.attachToLifecycleOwner(lifecycleOwner, isAsync, index, callback)

        if (index == -1) {
            shatters.add(shatter)
        } else {
            shatters.add(index, shatter)
        }

        cache.putShatter(shatter)
    }

    //快速入口
    fun addShatter(@IdRes containViewId: Int, shatter: Shatter) = apply {
        addShatter(containViewId, shatter, false, -1, null)
    }

    fun addShatter(containView: View?, shatter: Shatter) = apply {
        addShatter(containView, shatter, false, -1, null)
    }

    fun addShatter(@IdRes containViewId: Int, shatter: Shatter, callback: (() -> Unit)?) = apply {
        addShatter(containViewId, shatter, false, -1, callback)
    }

    fun addShatter(containView: View?, shatter: Shatter, callback: (() -> Unit)?) = apply {
        addShatter(containView, shatter, false, -1, callback)
    }

    /**
     *入口3 添加非布局类型的Shatter
     *
     * 不需要设置布局的加载方式和索引
     */
    fun addShatter(shatter: Shatter) = apply {
        shatter.shatterManager = this
        shatter.attachToLifecycleOwner(lifecycleOwner, false, -1, null)

        shatters.add(shatter)
        cache.putShatter(shatter)
    }

    /**
     * 入口4 预加载Shatter对象和布局
     *
     */
    fun preloadShatter(shatter: Shatter, isAsync: Boolean = false, callback: (() -> Unit)?) {
        shatter.shatterManager = this
        shatter.preload(lifecycleOwner, isAsync, callback)
    }

    //移除指定的Shatter
    fun remove(shatter: Shatter) {
        shatter.onDestroy()
        cache.removeShatter(shatter.getTag())
        shatters.remove(shatter)
    }

    // 移除所有Shatter对象
    fun removeAllShatter() {
        shatters.forEach { it.onDestroy() }
        shatters.clear()
        cache.clear()
    }

    // 移除特定类型的所有Shatter对象
    fun <T : Shatter> removeAllShatter(clazz: Class<T>) {
        val shattersToRemove = shatters.filterIsInstance(clazz)
        shattersToRemove.forEach { it.onDestroy() }
        shattersToRemove.forEach { cache.removeShatter(it.getTag()) }
        shatters.removeAll(shattersToRemove)
    }

    //找到指定的Shatter
    fun <T : Shatter> findShatter(clazz: Class<T>): T? {
        val tag = clazz.simpleName
        val shatter = shatters.find { it.getTag() == tag } ?: return null
        return shatter as T
    }

    fun <T : Shatter> findAllShatter(clazz: Class<T>): List<T>? {
        return shatters.filterIsInstance(clazz)
    }

    //销毁全部的Shatter,清空全部缓存
    private fun destroy() {
        cache.clear()
        shatters.clear()
    }

    fun interface OnAsyncCallback {
        fun OnSuccess();
    }

}

Shatter 的代码:

package com.view.shatter

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout
import androidx.annotation.LayoutRes
import androidx.asynclayoutinflater.view.AsyncLayoutInflater
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle

import androidx.lifecycle.LifecycleOwner


/**
 * 具体的一个XML的View的子碎片
 * 它作为碎片的基类,提供了一些生命周期方法和管理子碎片的功能
 */
abstract class Shatter : ShatterLifecycleListener, LifecycleOwner {

    var shatterManager: ShatterManager? = null
    var containView: View? = null
    private var lifecycleOwner: LifecycleOwner? = null
    private var preloadedView: View? = null
    private var isPreloading: Boolean = false
    private var isPreloadCompleted: Boolean = false
    private var onViewReadyListener: OnViewReadyListener? = null
    private var callback: (() -> Unit)? = null

    interface OnViewReadyListener {
        fun onViewReady(view: View)
    }

    // 通用的加载视图方法
    private fun loadView(lifecycleOwner: LifecycleOwner?, parentGroup: ViewGroup, asyncLoad: Boolean, completion: (View) -> Unit) {
        if (getLayoutResId() != 0) {
            if (asyncLoad) {
                // 异步加载
                val asyncLayoutInflater = AsyncLayoutInflater(ShatterManager.applicationContext)
                asyncLayoutInflater.inflate(getLayoutResId(), parentGroup) { view, _, _ ->
                    completion(view) // 使用回调返回加载完成的视图
                }
            } else {
                // 同步加载
                lifecycleOwner?.run {
                    val view = LayoutInflater.from(ShatterManager.applicationContext).inflate(getLayoutResId(), parentGroup, false)
                    completion(view) // 使用回调返回加载完成的视图
                }
            }
        }
    }

    // 预加载
    fun preload(lifecycleOwner: LifecycleOwner?, asyncLoad: Boolean, callback: (() -> Unit)?) {
        this.callback = callback
        this.lifecycleOwner = lifecycleOwner

        if (!isPreloading) {
            isPreloading = true
            loadView(lifecycleOwner, RelativeLayout(ShatterManager.applicationContext), asyncLoad) { view ->
                // 预加载完成
                preloadedView = view
                isPreloadCompleted = true
                isPreloading = false
                onViewReadyListener?.onViewReady(view)
                Log.w("Shatter", "预加载完成啦")
            }
        }
    }

    //绑定Lifecycle,加载布局,添加到容器
    fun attachToLifecycleOwner(lifecycleOwner: LifecycleOwner?, asyncLoad: Boolean, index: Int, callback: (() -> Unit)?) {
        this.callback = callback
        this.lifecycleOwner = lifecycleOwner

        if (getLayoutResId() != 0) {

            if (containView != null && containView is ViewGroup) {
                if (preloadedView != null && isPreloadCompleted) {
                    // 如果预加载已完成,则添加到containView
                    addView2Group(preloadedView, index)
                    containView = preloadedView
                    preloadedView = null // 清除引用

                    // 初始化Shatter调用onCreate等方法
                    onShatterCreate(lifecycleOwner)
                } else {

                    // 设置布局加载的监听
                    onViewReadyListener = object : OnViewReadyListener {
                        override fun onViewReady(view: View) {
                            addView2Group(view, index)
                            containView = view
                            // 在添加视图后清除回调,防止内存泄漏
                            onViewReadyListener = null

                            // 初始化Shatter调用onCreate等方法
                            onShatterCreate(lifecycleOwner)
                        }
                    }

                    // 如果当前没有进行预加载,则启动加载过程
                    if (!isPreloading) {
                        loadView(lifecycleOwner, containView as ViewGroup, asyncLoad) { view ->
                            onViewReadyListener?.onViewReady(view)
                        }
                    }
                }
            }

        } else {
            // 直接初始化无布局的Shatter
            onShatterCreate(lifecycleOwner)
        }
    }


    private fun addView2Group(view: View?, index: Int) {
        val groupView = containView as ViewGroup
        val childCount = groupView.childCount

        if (view == null) return

        if (index == -1) {
            groupView.addView(view)
        } else {
            // 如果索引在当前子视图数量的范围内
            if (index < childCount) {
                //无需移除之前的,直接按索引添加
                groupView.addView(view, index)
            } else {
                // 如果索引等于或大于当前子视图数量,添加空视图作为占位符,直到达到指定的索引,然后添加目标视图
                for (i in childCount until index) {
                    groupView.addView(View(ShatterManager.applicationContext).apply {
                        visibility = View.INVISIBLE
                    })
                }
                groupView.addView(view, index)
            }
        }
    }


    // 当碎片被创建时调用
    private fun onShatterCreate(lifecycleOwner: LifecycleOwner?) {
        if (lifecycleOwner is FragmentActivity) {
            val activity = lifecycleOwner
            initView(containView, activity.intent)
            onCreate(activity.intent)
            initData(activity.intent)

        } else if (lifecycleOwner is Fragment) {
            initView(containView, null)
            onCreate(null)
            initData(null)
        }

        //全部布局加载完成之后,可以尝试回调
        callback?.invoke()

    }

    // 获取布局资源ID
    @LayoutRes
    abstract fun getLayoutResId(): Int

    // 获取碎片标签
    open fun getTag(): String = this::class.java.simpleName

    // 查找指定类型的碎片
    @Suppress("UNCHECKED_CAST")
    open fun <T : Shatter> findShatter(clazz: Class<T>): T? {
        val tag = clazz.simpleName
        val shatter = shatterManager?.cache?.getShatter(tag)
        if (shatter != null) {
            return shatter as T
        }
        return null
    }

    // 接收处理碎片事件
    open fun onShatterEvent(key: String, data: Any?) {
    }

    open fun onCreate(intent: Intent?) {}

    open fun initView(view: View?, intent: Intent?) {}

    // 初始化数据
    open fun initData(intent: Intent?) {}

    // 处理新的Intent
    override fun onNewIntent(intent: Intent?) {
    }

    // 保存实例状态
    override fun onSaveInstanceState(outState: Bundle?) {
    }

    // 恢复实例状态
    override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
    }

    // 开始
    override fun onStart() {
    }

    //重新启动
    override fun onRestart() {
    }

    // 恢复
    override fun onResume() {
    }

    // 暂停
    override fun onPause() {
    }

    // 停止
    override fun onStop() {
    }

    // 销毁
    override fun onDestroy() {
    }

    // 处理ActivityResult
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    }

    // 是否允许返回键
    override fun enableOnBackPressed(): Boolean = true

    //Fragment 是否可见
    override fun onHiddenChanged(isHidden: Boolean) {
    }

    //给ShatterManager遍历调用
    fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
    }

    override fun getLifecycle(): Lifecycle {
        return lifecycleOwner!!.lifecycle
    }

    public fun getLifecycleOwner(): LifecycleOwner? {
        return lifecycleOwner
    }

    public fun getActivity(): FragmentActivity? {
        var activity: FragmentActivity? = null
        val lifecycleOwner = getLifecycleOwner()
        if (lifecycleOwner is Fragment) {
            activity = lifecycleOwner.activity
        } else if (lifecycleOwner is Activity) {
            activity = lifecycleOwner as FragmentActivity
        }
        return activity
    }
}

有兴趣查询完整代码可以到文章末尾查看源码。

二、Shatter的实战

关于 Shatter 的实战我们举一个很典型的示例,效果图如下:

Submit_Supporting_Documents.png

这是申请贫困学生的奖学金的其中一个页面,从上往下,需要用户下载表单,下载模板,并且完善信息之后上传到云端。

第二部分需要声明家庭成员的身份证明信息,根据不同的家庭成员选择不同类型的证件,上传不同的文件,可以选择多个类型

第三部分需要声明家庭成员的收入证明信息,根据不同的就业状态选择不同的类型,上传不同的文件

第四部分和第五部分是差不多了上传学生的证明和其他的证明,可以选择不同的类型,上传不同的文件,可以选择多个类型

大家可以停一下想一想,如果不使用Shatter,我们正常实现需要怎么做?

不管是使用 RV 列表实现还是使用自定义View来实习,我们的逻辑还是要抽离出来的,每次的操作还需要记录类型和多数据的索引。

我们的Activity代码是不是要奔着3000行去了,后期还能不能维护?必定是是屎山。

但是如果我们选择使用 Shatter 这种方式,就可以把页面分离,把页面对应的逻辑也分离,我们的 Activity 中只需要加载不同的 Shatter 就可以实现,只需要实现在 Add 和 Delete 的回调中处理添加 Shatter 或 删除 Shatter 的方法即可,在最后提交的时候我们获取对应类型的 Shatter 的数据组合起来就可以了。

这样不管是新增页面还是详情页面都是一套逻辑,类似效果图的布局我们就可以拆分为一下几个Shatter:

基本的 Shater:

DocFileShatter : 文件或图片的选择,内部是通过 RV 实现的九宫格添加删除,其对应的 DocFileViewModel 实现了文件的选择与上传。

DownloadShatter : 文件的下载,内部只展示了对应的布局,其对应的 DownloadViewModel 实现了文件的下载与保存。

SpinnerShatter : 下拉选择,根据传递的不同类型,选择对应的选项。

第二步的身份验证 Shatter 是由每一个家庭成员组成,所以我们定义了一下几种 Shatter:

IDMemberShatter : 对应每一个家庭成员,内部可以选择不同的类型自由的添加子 Shatter 。

IDDOCShatter : 由不同的类型选择与 DocFileShatter 组合而成,内部记录了当前的类型以及对应的图片/文件数据。

第二步的收入证明 Shatter 是由每一个家庭成员的工作状态组成,所以我们定义了不同工作状态的 Shatter :

IncomeShatter : 由每一个家庭成员的不同工作状态组成

IncomeEmployedShatter , SelfEmployedShatter, UnEmployedShatter 是不同的布局组成,具体也是由显示文本 TextView 与 对应的 DocFileShatter 组成。

第三步和第四步的文件上传就是很普通的二层 Shatter 组成。

一个是选择Type类型的 SpinnerShatter,一个是 DocFileShatter 两者组成。

其实拆分出来每一块的逻辑都不复杂,我相信大家肯定是都会的,只是由于是动态的布局和数据可能有点麻烦,这里就不给出代码,不太方便。

image.png

截图是我自己拆分的几个模块。

总结

这么看下来是不是很像搭积木呢?由基础的小积木组成中积木,再由中积木组成大积木,我们在页面中无需关心页面和逻辑,只需要添加和移除积木的操作即可,相比单独的UI积木更加的简洁了。

所以总的来说,特别是像这种比较典型多数据动态页面使用 Shatter 来拆分的话,不管是开发效率,还是后期维护都是比较方便的。

并且相比 RV 来实现的话,可以支持动态的布局异步加载,特别是在低端机型有相对较好的体验,通过 Shatter 可以比较方便的实现。

看到这里可以发现我前文中的痛点基本都是针对这次的需求做出的改动,对于一些动态数据的加减确定布局的排序与Shatter的排序一一对应,并且支持添加和删除的操作。

由于是公司项目确实是不方便开源,但是其中的逻辑和思路其实并不复杂,甚至可以说说很简单。如果你对 Shatter 有兴趣可以看看我的开源版本 【传送门】

那么今天的分享就到这里啦,当然如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。

如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我真的很重要。

Ok,这一期就此完结。