前言
无论是用上古时代的 ListView 还是现在的 RecyclerView ,在原生 Android 开发中想要快速实现一个列表功能都挺麻烦的。哪怕只是想写一个简单列表做一些测试,从零开始整没有三分钟是搞不定的,这还得是比较熟悉的语法,时间久了写个简单的 Adapter 都得折腾半天。尤其是和其他语言相比,始终觉得 Android 官方提供的列表组件不是那么的完美。
这里借助 Kotlin 的语法糖,探索使用现有组件将列表实现变得简单一些的可能性。
借鉴
我们首先可以参考一下主流的前端语言是怎么实现列表功能的。
传统的 html
<!DOCTYPE html>
<html lang="en">
<body>
<div id="list-container">
<ul id="dynamic-list">
<!-- 初始列表项将在这里动态生成 -->
</ul>
</div>
<script>
const list = document.getElementById('dynamic-list');
// 初始列表项
let items = ['项目1', '项目2', '项目3'];
// 渲染列表
function renderList() {
list.innerHTML = ''; // 清空现有列表
items.forEach((item, index) => {
const listItem = document.createElement('li');
listItem.textContent = item;
list.appendChild(listItem);
});
}
// 初始渲染
renderList();
</script>
</body>
</html>
这种做法类似于在 Android 中通过 LinearLayout 动态添加子 view 的方式实现,内容较多时必定会产生性能问题。
React Native
const App = () => {
const data = Array.from({ length: 100 }, (_, index) => `Item ${index + 1}`);
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.text}>{item}</Text>
</View>
);
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item}
contentContainerStyle={styles.contentContainer}
/>
);
};
Flutter
List<String> items = List.generate(100, (index) => 'Item ${index + 1}');
class _ScrollableListState extends State<ScrollableList> {
...
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: ListView.builder(
controller: _scrollController,
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index]),
);
},
),
);
}
}
Jetpack Compose
val items = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5")
@Composable
fun ListItem(text: String) {
Text(text = text, modifier = Modifier.padding(16.dp))
}
@Composable
fun ScrollableList() {
LazyColumn {
items(items) { item ->
ListItem(text = item)
}
}
}
可以看到在 React Native/Flutter/Jetpack Compose 这些语言中,实现列表组件的语法是非常相似的,给列表组件的初始化方法传入数据集和构建列表项的函数 就可以了,后来者都在借鉴前辈们的优点。
Swift
import UIKit
class TableViewController: UITableViewController {
// 数据源
let items = ["Item 1", "Item 2", "Item 3", ...] // 你的列表项
override func numberOfSections(in tableView: UITableView) -> Int {
return 1 // 只有一个部分
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count // 列表项的数量
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "Cell" // 定义一个标识符
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
cell.textLabel?.text = items[indexPath.row] // 设置文本
return cell
}
}
可以看到在 swift 中,在创建 TableViewController 的同时,隐式的实现了数据集合的设置和列表项的创建的方法。看起来思路和 Android 的 Adapter 类似,都是通过模板模式要求上层实现特定的方法。
Recycler.Adapter
我们再回过头来看 Android 中实现列表的方法。
面对一个 RecyclerView 无论如何我们首先得创建一个 Adapter
class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName: TextView = view.findViewById(R.id.fruitName)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitImage.setImageResource(fruit.imageId)
holder.fruitName.text = fruit.name
}
override fun getItemCount() = fruitList.size
}
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = FruitAdapter(list)
这样列表数据绑定(Adapter)、列表项测量和布局风格实现(LayoutManager)分离的做法无可厚非,最大程度的实现了解耦,用一个 RecyclerView、同一份数据、不同的的 Adapter 和 LayoutManager 进行组合可以非常灵活的实现不同样式的列表。 但是,有时候只是想快速写一个列表做一些功能验证,这样的解耦就会让实现比较繁琐。
fast-list
有没有办法像写 compose 那样,非常快速的实现呢?有的,这里我们借鉴开源库 fast-list 的实现。我们可以感受一下使用 fast-list 之后,写一个列表有多简单。
val list = listOf(Item("fast", 1), Item("recycler", 2), Item("view", 1))
recycler_view.bind(list, R.layout.item) { it : Item, position: Int ->
item_text.text = it.value
}
稍微复杂点,需要根据数据的差异展示不同的样式列表项
val list = listOf(Item("fast", 1), Item("recycler", 2), Item("view", 1))
recycler_view.bind(list)
.map(layout = R.layout.item, predicate = { it: Item, _ -> it.type == 1}) { item: Item, p: Int ->
item_text.text = it.value
}
.map(layout = R.layout.item_second, predicate = { it: Item, _ -> it.type == 2}) { item: Item, p: Int ->
item_second_text.text = it.value
}
.layoutManager(LinearLayoutManager(this))
这里 item_text 其实是借助了 kotlin-android-extensions 来简化获取 view 的操作
创建 Adapter、ViewHolder 的细节都被隐藏了。只需要实现对一个列表组件来说最核心的部分
- 提供列表数据,
- 实现列表项内容的渲染
可以看到,fast-list 几乎从语法层面几乎实现了和 compose 一样的写法(当然,二者只是写法相似,实际情况可以说是雷锋和雷峰塔的区别,从渲染机制到代码实际所代表的差异是非常巨大的)。那么 fast-lit 是怎么做到的呢?我们可以看一下源码。
fast-list 原理
fast-list 这个库的源码文件只有一个 BastList.kt。 在 Kotlin 中函数是一等公民,因此基于扩展函数、高阶函数,可以实现更高层次的抽象,函数可以作为作为另个一方法的参数。正是借助这些便捷的特性,fast-list 才得以隐藏了 Adapter 的很多细节。
fast-list 还支持 ViewPager2,同时支持 RecyclerView.Adapter 内按照 DiffUtil 刷新数据的方式,以下只截取 Aapapter 和 ViewHolder 封装的逻辑,更多细节可以参考源码。
typealias BindingClosure<T> = (View.(item: T, position: Int) -> Unit)
class FastListViewHolder<T>(override val containerView: View, val holderType: Int) : RecyclerView.ViewHolder(containerView), LayoutContainer {
fun bind(entry: T, position: Int, func: BindingClosure<T>) {
containerView.apply {
func(entry, position)
}
}
}
fun <T> RecyclerView.bind(items: List<T>): FastListAdapter<T> {
layoutManager = LinearLayoutManager(context)
return FastListAdapter(items.toMutableList(), this)
}
fun <T> RecyclerView.bind(items: List<T>, @LayoutRes singleLayout: Int = 0, singleBind: BindingClosure<T>): FastListAdapter<T> {
layoutManager = LinearLayoutManager(context)
return FastListAdapter(items.toMutableList(), this).map(singleLayout, { item: T, position: Int -> true }, singleBind)
}
- 首先定义了一个名为 BindingClosure 的类型,这个类型的接受者是 View 。这个类型其实就是数据绑定的高阶函数。
- FastListViewHolder 内部定义了 bind 方法,实现数据绑定。在 Adapter 的 onBindViewHolder 方法中会调用这里定义的 bind 方法,传入实际进行数据绑定的函数,也就是上面定义的 BindingClosure 的某个具体实现,完成最终的数据绑定。
- bind 方法内会默认绑定 LayoutManager 为 LinearLayoutManager . 同时会基于传入的参数,创建一个 FastListAdapter 的实例,并调用其 map 方法。
FastListAdapter
open class FastListAdapter<T>(private var items: MutableList<T>, private var list: RecyclerView? = null, private var vpList: ViewPager2? = null)
: RecyclerView.Adapter<FastListViewHolder<T>>() {
private inner class BindMap(val layout: Int, var type: Int = 0, val bind: BindingClosure<T>, val predicate: (item: T, position: Int) -> Boolean) {
constructor(lf: LayoutFactory, type: Int = 0, bind: BindingClosure<T>, predicate: (item: T, position: Int) -> Boolean) : this(0, type, bind, predicate) {
layoutFactory = lf
}
var layoutFactory: LayoutFactory? = null
}
private var bindMap = mutableListOf<BindMap>()
private var typeCounter = 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FastListViewHolder<T> {
return bindMap.first { it.type == viewType }.let {
return FastListViewHolder(LayoutInflater.from(parent.context).inflate(it.layout,
parent, false), viewType)
}
}
override fun onBindViewHolder(holder: FastListViewHolder<T>, position: Int) {
val item = items.get(position)
holder.bind(item, position, bindMap.first { it.type == holder.holderType }.bind)
}
override fun getItemCount() = items.size
override fun getItemViewType(position: Int) = try {
bindMap.first { it.predicate(items[position], position) }.type
} catch (e: Exception) {
0
}
fun map(@LayoutRes layout: Int, predicate: (item: T, position: Int) -> Boolean, bind: BindingClosure<T>): FastListAdapter<T> {
bindMap.add(BindMap(layout, typeCounter++, bind, predicate))
list?.adapter = this
vpList?.adapter = this
return this
}
}
FastListAdapter 内部定义了 BindMap 这样一个类,这个类聚合了布局文件、viewType、数据绑定实现、和约束 viewType 映射关系的一个接口。
在 onCreateViewHolder 执行时,会基于 bindMap 集合中 viewType 的映射关系,返回符合当前位置的布局文件,从而实现 ViewHolder 的创建。
当 onBindViewHolder 执行时,会调用 FastViewHolder 的bind 方法。同时会根据当前 ViewHolder 内的 viewType 从 bindMap 集合中找到响应的 BindingClosure 这个高阶函数的实现作为参数传递给 bind 方法。
而 map 方法的作用就是聚合我们在调用方法时传入的参数。
看完原理之后,我们再反过来看看 fast-list 的用法
val datas: ArrayList<String> = ArrayList()
recyclerView.bind(datas,R.layout.list_item_avatar, object : (View, String, Int) -> Unit {
override fun invoke(p1: View, item: String, position: Int) {
val title: TextView = p1.findViewById(R.id.title_tv)
val index: TextView = p1.findViewById(R.id.index_tv)
title.text = item
index.text = position.toString()
}
})
这样写完整之后,再结合源码就能感受到这个封装真的很巧妙。
对于需要绑定不同列表样式的场景
recyclerView.bind(datas).map(R.layout.list_item, object : (String, Int) -> Boolean {
override fun invoke(p1: String, p2: Int): Boolean {
return p2 < 5
}
}, object : (View, String, Int) -> Unit {
override fun invoke(p1: View, p2: String, p3: Int) {
// view 和数据绑定
}
}).map(R.layout.list_item_avatar, object : (String, Int) -> Boolean {
override fun invoke(p1: String, p2: Int): Boolean {
return p2 >= 5
}
}, object : (View, String, Int) -> Unit {
override fun invoke(p1: View, p2: String, p3: Int) {
// view 和数据绑定
}
})
同样,对于 map 方法的几个参数,布局文件、返回布尔值的接口、数据绑定高阶函数实现,恰好就是 FastListAdapter 内 BindMap 的属性。每一次我们调用 map 方法都是在创建一个 BindMap 的实例,同时添加到 bindMap 的集合中。
而当 Adapter 的 getItemViewType 调用时会根据当前数据的内容,结合 predicate 这个接口的逻辑,返回合适的 viewType. 最核心的两个函数 onCreateViewHolder 和 onBindViewHolder 调用时,就会基于这个 viewType 驱动 view 的创建和数据的绑定。
可以看到,这个和我们面对这类问题写实现相应逻辑的思路是一样的,都是在 getItemViewType 中通过数据确定 viewType 的值,随后再根据 viewType 创建合适的 ViewHolder 并最终完成数据绑定。只不过这里 fast-list 内部结合扩展函数和高阶函数的用法,将这些核心逻辑抽象成了 函数 ,而函数可以作为方法的参数,这样我们就隐藏了 Adapter 和 ViewHolder 的细节,同时可以通过外部调用传入这些函数的具体实现,从而完成功能的闭环。
总结
理解了 RecyclerView Adapter 核心的内容,ViewHolder 的本质之后,借助 Kotlin 的语法特性,可以完成更高层次的抽象和代码封装,从而更进一步的减少模板代码的存在。像 fast-list 这样更便捷的实现一个列表组价。这类封装方法其实非常值得借鉴,可以大大介绍实际开发中模板代码的数量。