MVVM框架搭建之——简单实现不同状态的空页面(三)

·  阅读 432

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

一、前言

当页面(特别是列表页面)的数据为空时,页面可能会一片空白,这很影响美观甚至影响功能正常使用。所以一般会展示一个默认的页面,用于提示用户当前没数据是网络问题还是其他原因。

如下图所示,空页面一般由一个图标、标题、副标题和一个按钮组合而成,这几个控件可选择展示一个多个。整个项目的空页面大体一致,变化的无非就是图标,文字,和各控件的展示状态。看起来非常简单,不过再简单的事重复10遍也是一个不小的工作量,因此封装一个空页面组件是很有必要的。

emptyView.png

二、需求分析

开始做一件事前首先得清楚要达成的目的、步骤分解和分析是很重要的事情。对于空页面组件,我们希望它:

  1. 添加使用简单;(最好一行代码能开启或关闭。)
  2. 可配置能力高;(每个控件都能当基础控件TextView/ImageView一样使用。)
  3. 能根据页面数据变化自动显示隐藏;(有数据时显示内容,无数据时显示空页面。)
  4. 能根据数据为空原因显示对应状态UI;(细分数据空的原因,服务端异常、空内容、网络未连接、网络不可用都能展示不同UI)
  5. 能设置每种空状态的全局通用样式,其他页面只需更改差异的地方。(全局配置一次,所有页面都使用这个配置的UI样式,页面特殊时只需要更改不一样的设置)

总之,就是既能满足项目需求,又要使用简单!!!

三、展示

多说无益,直接实现

1. 添加依赖,项目地址

dependencies {
    implementation "com.github.runnchild.Feature:emptyview:$latest_version" 
}
复制代码

2.配置全局通用样式

// 配置全局网络未连接样式
DefaultEmptyConfig.configNetDisconnectBuilder {
    icon {
        setImageResource(R.mipmap.empty_no_net)
        layoutParams = ViewGroup.LayoutParams(150.dp, 150.dp)
    }
    tip {
        text = "网络连接失败"
        textSize = 17f
        setTextColor(Color.GRAY)
    }
    subTip {
        text = "请检查你的网络设置后刷新"
    }
    refreshBtn {
        background = ContextCompat.getDrawable(context, R.drawable.round_background)
        text = "刷新"
    }
}
// 配置全局空数据样式
DefaultEmptyConfig.configEmptyDataBuilder {...}
// 配置全局网络不可用样式
DefaultEmptyConfig.configNetUnavailableBuilder {...}
复制代码

如此,空页面大概UI就是上图的样子。空数据和网络不可用同理。

3.页面中配置

(此处以依赖了Feature中的ListAbility为例,ListAbility实现了数据的订阅和空页面组件的关联,否则需自己实现这部分功能, 这是ListAbility的介绍

class RepoSearchFragment : BaseFragment<FragmentListBinding, RepoSearchViewModel>, IRecyclerHost {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        registerAbility(ListAbility(viewModel, this))
    }
    
    override fun setupEmptyView(builder: EmptyBuilder) {
        // 只设置空数据,其他情况使用默认配置
        builder.whenDataIsEmpty {
            tip {
                // 相较上面的全局配置,如果只有文案不一样只需要更改text内容
                text = "no repository found"
            }
        }
        
       // 如果其他情况也需更改
       builder.whenDisconnect {
           ...
       }.whenUnavailable {
           ...
       }
    }
    
    // 如果默认的页面无论怎么配置都满足不了你,返回实现了IEmptyView接口的自定义的View
    override fun providerEmptyView(context: Context): IEmptyView? {
        // default is EmptyView
        return super.providerEmptyView(context)
    }
}
复制代码

四、详解

其实都用不着详解什么,太简单我都不好意思说,随便看两眼源码就懂了。需要特别说明的是这是依赖DataBinding的组件,否则不能正常工作。

组件的类结构:

  • EmptyView
    • IEmptyView ---空页面UI接口
    • EmptyView ---空页面UI
    • EmptyViewConfig ---空页面数据配置类
    • EmptyState --- 空页面状态(原因)
    • DefaultEmptyConfig ---存储全局的默认配置
    • EmptyBuilder ---EmptyViewConfig的构建者

类关键代码

    1. IEptyView:空页面View需要实现的接口用于set/getConfig()
interface IEmptyView {
    var config: EmptyViewConfig?
}
复制代码
    1. EmptyView:简单的自定义View,包含空页面所需要的几个控件
class EmptyView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr), IEmptyView {
    
    private var binding = EmptyViewBinding.inflate(LayoutInflater.from(context), this, true)
    
    override var config: EmptyViewConfig?
        get() = binding.config
        set(value) {
            binding.config = value
        }
}
复制代码
    1. EmptyViewConfig:UI的状态配置,设置后将会自动更新到EmptyView上
class EmptyViewConfig {
    var refreshBuilder = ObservableField<TextView.() -> Unit>()
    var tipBuilder = ObservableField<TextView.() -> Unit>()
    var subTipBuilder = ObservableField<TextView.() -> Unit>()
    var iconBuilder = ObservableField<ImageView.() -> Unit>()
}
复制代码

一句话概括实现原理:

当数据为空的条件触发时①记录当前空数据状态,组件以当前空状态选择对应的全局默认配置②,在以此为基础传递给页面做差异化加工③,得到的最终EmptyViewConfig传递给EmptyView用于更改UI状态④。

①:数据为空时很好理解,比如ListAbility中,请求成功数据为空时状态为EmptyState.EMPTY_DATA, 请求发生错误时若网络问题状态为EmptyState.EMPTY_NET_DISCONNECT或EmptyState.EMPTY_NET_UNAVAILABLE,否则就是服务器错误EmptyState.EMPTY_SERVICE

fun <T> Resource<List<T>?>.emptyState(block: (EmptyState) -> Unit) {
    when (status) {
        Status.SUCCESS -> {
            if (data.isNullOrEmpty()) {
                block(EmptyState.EMPTY_DATA)
            }
        }
        Status.ERROR -> {
            AppExecutors.diskIO().execute {
                if (!NetworkUtils.isConnected()) {
                    block(EmptyState.EMPTY_NET_DISCONNECT)
                } else if (!NetworkUtils.isAvailable()) {
                    block(EmptyState.EMPTY_NET_UNAVAILABLE)
                } else {
                    if (data.isNullOrEmpty()) {
                        block(EmptyState.EMPTY_SERVICE)
                    }
                }
            }
        }
}
复制代码

②:得到EmptyState之后先取对应状态的全局配置:

val defaultBuilder = when (state) {
    EmptyState.EMPTY_NET_DISCONNECT -> DefaultEmptyConfig.noNetBuilder
    EmptyState.EMPTY_NET_UNAVAILABLE -> DefaultEmptyConfig.netUnavailableBuilder
    else -> DefaultEmptyConfig.emptyDataBuilder
}

val emptyBuilder = EmptyBuilder(state).apply(defaultBuilder)
复制代码

③:再将默认配置传递给页面继续加工

listHost.setupEmptyView(emptyBuilder)
复制代码

④:此时emptyBuilder就为最终的空配置,通过builder方法最终会传给EmptyView做ui变更

emptyConfig.builder(emptyBuilder)
复制代码

五、总结

我接手过很多项目,但并没有一个项目能像样地把一个像空页面这样简单的,重复使用的组件封装好,不是实现复杂就是配置繁琐或者稍微改点需求就不再适用。也许大家觉得成大事者不拘小节,大不了就是多点复制粘贴的操作。可能我比较懒,少写代码就是我的宗旨。所以我自己写了一个极简风格的MVVM框架Feature,emptyView就是其中的一个组件。更多组件感兴趣的朋友可以看这个框架,如果觉得有点意思麻烦帮我点个Star。

还是那话,我分享的都很简单,更多的是一个想法。如果你有好的建议或意见或者有更好的方案,还请不吝赐教。互相学习也是大家创作和开源的初衷。

MVVM框架搭建系列

《MVVM框架搭建之——如何优化多层继承造成的耦合问题(一)》

《MVVM框架搭建之——列表页面的高效实现(二)》

更多待续,欢迎关注。。。

分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改