如何快速实现一个列表组件

553 阅读8分钟

前言

无论是用上古时代的 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. 最核心的两个函数 onCreateViewHolderonBindViewHolder 调用时,就会基于这个 viewType 驱动 view 的创建和数据的绑定。

可以看到,这个和我们面对这类问题写实现相应逻辑的思路是一样的,都是在 getItemViewType 中通过数据确定 viewType 的值,随后再根据 viewType 创建合适的 ViewHolder 并最终完成数据绑定。只不过这里 fast-list 内部结合扩展函数和高阶函数的用法,将这些核心逻辑抽象成了 函数 ,而函数可以作为方法的参数,这样我们就隐藏了 Adapter 和 ViewHolder 的细节,同时可以通过外部调用传入这些函数的具体实现,从而完成功能的闭环。

总结

理解了 RecyclerView Adapter 核心的内容,ViewHolder 的本质之后,借助 Kotlin 的语法特性,可以完成更高层次的抽象和代码封装,从而更进一步的减少模板代码的存在。像 fast-list 这样更便捷的实现一个列表组价。这类封装方法其实非常值得借鉴,可以大大介绍实际开发中模板代码的数量。