23、空视图处理:列表为空时显示“暂无数据”提示。
做列表开发,有个场景特别常见——数据还没加载完,或者压根就没有数据。这时候你给用户看一个空白页面,说实话,挺不友好的。用户会想:「这 App 是不是卡死了?」或者「我操作错了?」。
我个人习惯,只要列表可能为空,就一定给它配一个空视图。说白了,就是告诉用户:「嘿,现在没东西,但我知道你没做错什么。」
为什么需要空视图?
你想想看,一个 RecyclerView 绑定了空的数据集,它渲染出来就是一块白板。用户盯着白板,心里没底。我遇到过不止一次,测试同学提 bug 说「页面加载不出来」,结果一看,是数据为空,但没有任何提示。
空视图的作用其实就三点:
- 消除用户焦虑——明确告知当前状态
- 引导下一步操作——比如「点击刷新」或「去添加数据」
- 提升专业感——空白页面显得很粗糙,有提示才完整
核心原则:永远不要让用户面对一个空白的 RecyclerView。要么显示数据,要么显示提示。
两种实现思路
实现空视图,我一般用两种方式。哪种顺手就用哪种,没有绝对的对错。
方式一:直接在布局中控制可见性
这是最直接的做法。在 Activity 或 Fragment 的布局里,把 RecyclerView 和一个空视图布局放在一起。然后根据数据量,切换它们的可见性。
举个例子:
<!-- fragment_list.xml -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="暂无数据"
android:textSize="16sp"
android:textColor="#999999" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="刷新"
android:onClick="onRefreshClick" />
</LinearLayout>
</FrameLayout>
然后在代码里:
fun updateList(data: List<Item>) {
if (data.isEmpty()) {
recyclerView.visibility = View.GONE
emptyView.visibility = View.VISIBLE
} else {
recyclerView.visibility = View.VISIBLE
emptyView.visibility = View.GONE
adapter.submitList(data)
}
}
嗯,这里要注意:submitList 调用时如果传空列表,RecyclerView 不会崩溃,但会显示空白。所以判断空数据的时机,最好在调用 submitList 之前。
方式二:使用 Adapter 的观察者模式
这种方式更优雅一些。利用 AdapterDataObserver 监听数据变化,自动切换空视图。我比较推荐这种方式,因为它把逻辑封装在了 Adapter 或 ViewModel 层,UI 层只需要设置一次监听。
看代码:
class MyAdapter : ListAdapter<Item, MyAdapter.ViewHolder>(DiffCallback()) {
private var emptyView: View? = null
fun attachEmptyView(view: View) {
emptyView = view
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
checkEmpty()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
checkEmpty()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
checkEmpty()
}
})
}
private fun checkEmpty() {
emptyView?.visibility = if (itemCount == 0) View.VISIBLE else View.GONE
}
}
使用的时候:
adapter.attachEmptyView(findViewById(R.id.emptyView))
这样做的好处是,你不需要在每次更新数据时都手动判断可见性。Adapter 自己会搞定。我在项目中用过这种方式,代码干净很多。
小技巧:如果你用 ListAdapter,记得在 onItemRangeInserted 和 onItemRangeRemoved 里也触发检查。因为 submitList 可能触发的是增量更新,不是全量 onChanged。
空视图应该长什么样?
不只是「暂无数据」四个字。我建议至少包含:
- 图标——一个简单的空盒子或文件夹图标,视觉上更友好
- 主文案——「暂无数据」或「这里空空如也」
- 副文案——解释原因或给出建议,比如「请检查网络后重试」
- 操作按钮——「刷新」或「去添加」,让用户有路可走
举个例子:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="暂无数据"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="当前没有内容,下拉刷新试试"
android:textSize="14sp"
android:layout_marginTop="8dp" />
避坑指南
我曾经踩过一个坑:在 onCreate 里直接判断数据源是否为空,然后设置空视图可见性。结果数据是异步加载的,初始时确实是空,但加载完成后数据来了,空视图却没隐藏。因为我没有在数据回调里更新可见性。
所以记住:空视图的切换逻辑,一定要和数据更新的回调绑定在一起。不要在初始化时一次性判断。
另外,如果你用 LiveData 或 Flow 观察数据,空视图的切换可以放在观察者的回调里。这样最安全。
SVG 流程图:空视图处理逻辑
下面这张图展示了从数据加载到空视图展示的完整流程:
这个流程其实很简单:数据来了,先判断是否为空。空就隐藏列表显示提示,非空就显示列表隐藏提示。关键是「判断」这个节点,一定要放在数据更新的回调里。
总结一下
空视图处理,说白了就是给用户一个交代。别让用户对着白板发呆。我个人建议优先使用 Adapter 观察者的方式,代码更解耦。如果你项目简单,直接控制可见性也没问题。
最后提醒一句:空视图的文案要友好,别写「数据为空」这种冷冰冰的提示。加个图标,加个操作按钮,用户体验会好很多。
额外建议:如果你做的是下拉刷新列表,空视图里可以提示「下拉刷新试试」。这样用户就知道下一步该做什么了。
公众号:蓝海资料掘金营,微信deep3321