本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!
一、前言
当页面(特别是列表页面)的数据为空时,页面可能会一片空白,这很影响美观甚至影响功能正常使用。所以一般会展示一个默认的页面,用于提示用户当前没数据是网络问题还是其他原因。
如下图所示,空页面一般由一个图标、标题、副标题和一个按钮组合而成,这几个控件可选择展示一个多个。整个项目的空页面大体一致,变化的无非就是图标,文字,和各控件的展示状态。看起来非常简单,不过再简单的事重复10遍也是一个不小的工作量,因此封装一个空页面组件是很有必要的。
二、需求分析
开始做一件事前首先得清楚要达成的目的、步骤分解和分析是很重要的事情。对于空页面组件,我们希望它:
- 添加使用简单;(最好一行代码能开启或关闭。)
- 可配置能力高;(每个控件都能当基础控件TextView/ImageView一样使用。)
- 能根据页面数据变化自动显示隐藏;(有数据时显示内容,无数据时显示空页面。)
- 能根据数据为空原因显示对应状态UI;(细分数据空的原因,服务端异常、空内容、网络未连接、网络不可用都能展示不同UI)
- 能设置每种空状态的全局通用样式,其他页面只需更改差异的地方。(全局配置一次,所有页面都使用这个配置的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的构建者
类关键代码
-
- IEptyView:空页面View需要实现的接口用于set/getConfig()
interface IEmptyView {
var config: EmptyViewConfig?
}
-
- 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
}
}
-
- 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框架搭建之——如何优化多层继承造成的耦合问题(一)》
更多待续,欢迎关注。。。