内容简介:Android 复杂的多类型列表视图新写法:MultiType 3.0
在开发我的 TimeMachine 时,我有一个复杂的聊天页面,于是我设计了我的类型池系统,它是完全解耦的,因此我能够轻松将它抽离出来分享,并给它取名为 MultiType .
从前, 比如我们写一个类似微博列表页面 ,这样的列表是十分复杂的:有纯文本的、带转发原文的、带图片的、带视频的、带文章的等等,甚至穿插一条可以横向滑动的好友推荐条目。不同的 item 类型众多,而且随着业务发展,还会更多。如果我们使用传统的开发方式,经常要做一些繁琐的工作,代码可能都堆积在一个 Adapter
中:我们需要覆写 RecyclerView.Adapter
的 getItemViewType
方法,罗列一些 type
整型常量,并且 ViewHolder
转型、绑定数据也比较麻烦。一旦产品需求有变,或者产品设计说需要增加一种新的 item 类型,我们需要去代码堆里找到原来的逻辑去修改,或找到正确的位置去增加代码。这些过程都比较繁琐,侵入较强,需要小心翼翼,以免改错影响到其他地方。
现在好了,我们有了 MultiType ,简单来说, MultiType 就是一个多类型列表视图的中间分发框架,它能帮助你快速并且清晰地开发一些复杂的列表页面。 它本是为聊天页面开发的,聊天页面的消息类型也是有大量不同种类,且新增频繁,而 MultiType 能够轻松胜任。
MultiType以灵活直观为第一宗旨进行设计,它内建了 类型
- View
的复用池系统,支持 RecyclerView
,随时可拓展新的类型进入列表当中,使用简单,令代码清晰、模块化、灵活可变。
因此,我写了这篇文章,目的有几个:一是以作者的角度对 MultiType 进行入门和进阶详解。二是传递我开发过程中的思想、设计理念,这些偏细腻的内容,即使不使用 MultiType ,想必也能带来很多启发。最后就是把自我觉得不错的东西分享给大家,试想如果你制造的东西很多人在用,即使没有带来任何收益,也是一件很自豪的事情。
目录
-
- 使用 MultiTypeTemplates 插件自动生成代码
- 一个类型对应多个 ItemViewBinder
- 与 ItemViewBinder 通讯
- 使用断言,比传统 Adapter 更加易于调试
- 支持 Google AutoValue
- MultiType 与下拉刷新、加载更多、HeaderView、FooterView、Diff
- 实现 RecyclerView 嵌套横向 RecyclerView
- 实现线性布局和网格布局混排列表
-
- 仿造微博的数据结构和二级 ViewBinder
- drakeet/about-page
- 线性和网格布局混排
- drakeet/TimeMachine
- 类似 Bilibili iOS 端首页
-
- Q: 觉得 MultiType 不够精简,应该怎么做?
- Q: 在
ItemViewBinder
中如何拿到Context
对象? - Q: 如何在
ItemViewBinder
中获取到 item position?
MultiType 的特性
- 轻盈,整个类库只有 14 个类文件,
aar
或jar
包大小只有 13 KB - 周到,支持 data type
<-->
item view binder 之间 一对一 和 一对多 的关系绑定 - 灵活,几乎所有的部件(类)都可被替换、可继承定制,面向接口 / 抽象编程
- 纯粹,只负责本分工作,专注多类型的列表视图 类型分发,绝不会去影响 views 的内容或行为
- 高效,没有性能损失,内存友好,最大限度发挥
RecyclerView
的复用性 - 可读,代码清晰干净、设计精巧,极力避免复杂化,可读性很好,为拓展和自行解决问题提供了基础
总览
MultiType能轻松实现如下页面,它们将在示例篇章具体提供:
MultiType 的源码关系:
MultiType 基础用法
可能有的新手看到以上特性介绍说什么 “一对多”、抽象编程等等,都不太懂,我想说完全不要紧,不懂可以回过头来再看,我们先从基础用法入手,其实 MultiType 使用起来特别简单。使用 MultiType 一般情况下只要 maven 引入 + 三个小步骤。之后还会介绍使用插件生成代码方式,步骤将更加简化:
引入
在你的 build.gradle
:
dependencies { compile 'me.drakeet.multitype:multitype:3.0.0' }
注: MultiType 内部引用了 recyclerview-v7:25.3.1
,如果你不想使用这个版本,可以使用 exclude
将它排除掉,再自行引入你选择的版本。示例如下:
dependencies { compile('me.drakeet.multitype:multitype:3.0.0', { exclude group: 'com.android.support' }) compile 'com.android.support:recyclerview-v7:你选择的版本' }
使用
Step 1. 创建一个 class
,它将是你的数据类型或 Java bean / model. 对这个类的内容没有任何限制。示例如下:
public class Category{ @NonNull public final String text; public Category(@NonNull String text){ this.text = text; } }
Step 2. 创建一个 class
继承 ItemViewBinder
.
ItemViewBinder
是个抽象类,其中 onCreateViewHolder
方法用于生产你的 Item View Holder, onBindViewHolder
用于绑定数据到 View
s. 一般一个 ItemViewBinder
类在内存中只会有一个实例对象,MultiType 内部将复用这个 binder 对象来生产所有相关的 item views 和绑定数据。示例:
public class CategoryViewBinder extends ItemViewBinder<Category, CategoryViewBinder.ViewHolder> { @NonNull @Override protectedViewHolderonCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent){ View root = inflater.inflate(R.layout.item_category, parent, false); return new ViewHolder(root); } @Override protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull Category category){ holder.category.setText(category.text); } static class ViewHolderextends RecyclerView.ViewHolder{ @NonNull private final TextView category; ViewHolder(@NonNull View itemView) { super(itemView); this.category = (TextView) itemView.findViewById(R.id.category); } } }
Step 3. 在 Activity
中加入 RecyclerView
和 List
并注册你的类型,示例:
public class MainActivityextends AppCompatActivity{ private MultiTypeAdapter adapter; /* Items 等同于 ArrayList<Object> */ private Items items; @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list); adapter = new MultiTypeAdapter(); /* 注册类型和 View 的对应关系 */ adapter.register(Category.class, new CategoryViewBinder()); adapter.register(Song.class, new SongViewBinder()); recyclerView.setAdapter(adapter); /* 模拟加载数据,也可以稍后再加载,然后使用 * adapter.notifyDataSetChanged() 刷新列表 */ items = new Items(); for (int i = 0; i < 20; i++) { items.add(new Category("Songs")); items.add(new Song("小艾大人", R.drawable.avatar_dakeet)); items.add(new Song("许岑", R.drawable.avatar_cen)); } adapter.setItems(items); adapter.notifyDataSetChanged(); } }
大功告成!这就是 MultiType 的基础用法了。其中 onCreateViewHolder
和 onBindViewHolder
方法名沿袭了使用 RecyclerView
的习惯,令人一目了然,减少了新人的学习成本。
设计思想
MultiType设计伊始,我给它定了几个原则:
-
要简单,便于他人阅读代码
因此我极力避免将它复杂化,避免加入许多不相干的内容。我想写人人可读的代码,使用简单的方式,去实现复杂的需求。过多不相干、没必要的代码,将会使项目变得令人晕头转向,难以阅读,遇到需要定制、解决问题的时候,无从下手。
-
要灵活,便于拓展和适应各种需求
很多人会得意地告诉我,他们把 MultiType 源码精简成三四个类,甚至一个类,以为代码越少就是越好,这我不能赞同。 MultiType 考虑得更远,这是一个提供给大众使用的类库,过度的精简只会使得大幅失去灵活性。 它或许不是使用起来最简单的,但很可能是使用起来最灵活的。 在我看来,”直观”、”灵活”优先级大于”简单”。因此, MultiType 以接口或抽象进行连接,这意味着它的角色、组件都可以被替换,或者被拓展和继承。如果你觉得它使用起来还不够简单,完全可以通过继承封装出更具体符合你使用需求的方法。它已经暴露了足够丰富、周到的接口以供拓展,我们不应该直接去修改源码,这会导致一旦后续发现你的精简版满足不了你的需求时,已经没有回头路了。
-
要直观,使用起来能令项目代码更清晰可读,一目了然
MultiType提供的
ItemViewBinder
沿袭了RecyclerView Adapter
的接口命名,使用起来更加舒适,符合习惯。另外,MultiType 很多地方放弃使用反射而是让用户显式指明一些关系,如:MultiTypeAdapter#register
方法,需要传递一个数据模型class
和ItemViewBinder
对象,虽然有很多方法可以把它精简成单一参数方法,但我们认为显式声明数据模型类与对应关系,更具直观。
高级用法
介绍了基础用法和设计思想后,我们可以来介绍一下 MultiType 的高级用法。这是一些典型需求和案例,它们是基础用法的延伸,也是设计思想的体现。也许一开始并不会使用到,但如若了解,能够拓宽使用 MultiType 的思路,也能过了解到我们考虑问题的角度。
使用 MultiTypeTemplates 插件自动生成代码
在基础用法中,我们了通过 3 个步骤完成 MultiType 的初次接入使用,实际上这个过程可以更加简化, MultiType 提供了 Android Studio 插件来自动生成代码:
MultiTypeTemplates,源码也是开源的, https://github.com/drakeet/MultiTypeTemplates 。这个插件不仅提供了一键生成 item 类文件和 ItemViewBinder
,而且 是一个很好的利用代码模版自动生成代码的示例。 其中使用到了官方提供的代码模版 API,也用到了我自己发明的更灵活修改模版内容的方法,有兴趣做这方面插件的可以看看。
话说回来,安装和使用 MultiTypeTemplates 非常简单:
Step 1.打开 Android Studio 的 设置
-> Plugin
-> Browse repositories
,搜索 MultiTypeTemplates
即可获得下载安装:
Step 2.安装完成后,重启 Android Studio. 右键点击你的 package,选择 New
-> MultiType Item
,然后输入你的 item 名字,它就会自动生成 item 模型类 和 ItemViewBinder
文件和代码。
比如你输入的是 “Category”,它就会自动生成 Category.java
和 CategoryViewBinder.java
.
特别方便,相信你会很喜欢它。未来这个插件也将会支持自动生成布局文件,这是目前欠缺的,但不要紧,其实 AS 在这方面已经很方便了,对布局 R.layout.item_category
使用 alt + enter
快捷键即可自动生成布局文件。
一个类型对应多个 ItemViewBinder
MultiType天然支持一个类型对应多个 ItemViewBinder
,注册方式也很简单,如下:
adapter.register(Data.class).to( new DataType1ViewBinder(), new DataType2ViewBinder() ).withClassLinker(new ClassLinker<Data>() { @NonNull @Override public Class<? extends ItemViewBinder<Data, ?>> index(@NonNull Data data) { if (data.type == Data.TYPE_2) { return DataType2ViewBinder.class; } else { return DataType1ViewBinder.class; } } });
或者:
adapter.register(Data.class).to( new DataType1ViewBinder(), new DataType2ViewBinder() ).withLinker(new Linker<Data>() { @Override public int index(@NonNull Data data){ if (data.type == Data.TYPE_2) { return 1; } else return 0; } });
如果你使用 Lambda 表达式,以上代码可以更简洁:
解释:
如上示例代码,对于一对多,我们需要使用 MultiType#register(class)
方法,它会返回一个 OneToManyFlow
让你紧接着绑定多个 ItemViewBinder
实例,最后再调用 OneToManyEndpoint#withLinker
或 OneToManyEndpoint#withClassLinker
操作符方法类设置 linker. 所谓 linker,是负责动态连接这个 “一” 对应 “多” 中哪一个 binder 的角色。
这个方案具有很好的性能表现,而且可谓十分直观。另外,我使用了 @CheckResult
注解来让编译器督促开发者一定要完整调用方法链才不至于出错。
更详细的”一对多”示例可以参考我的 sample 源码: https://github.com/drakeet/MultiType/tree/master/sample/src/main/java/me/drakeet/multitype/sample/one2many
使用 全局类型池
MultiType在 3.0 版本之前一直是支持全局类型池的,你可以往一个全局类型池中 register 类型和 view binder,然后让你的各个 MultiTypeAdapter
都能使用它。
但在 MultiType 3.0 之后,我们废弃并删除了内置的全局类型池。原因在于全局类型池容易对全局产生不可见影响,比如你注册了一堆全局类型关系并在多处引用它,某一天你的伙伴不小心修改了全局类型池的某个内容,将导致所有使用的地方皆受到变化,是我们不希望发生的。一个好的模块,应该是高内聚、自包含的,如果过多下放权力到外围,很容易遭受破坏或影响。
另外,全局类型池一般都是 static 形式的,如果我们给这个 static 容器传递了 Activity
或 Context
对象,而没有在退出时释放,就容易造出内存泄漏,这对新手来说很容易触犯。
因此我们删除了内置的全局类型池,当你创建一个 MultiTypeAdapter
对象时,默认情况下,它内部会自动创建一个局部类型池以供你接下来注册类型。当然了,如果你实在需要它,完全可以自己创建一个 static 的 MultiTypePool
,然后通过 MultiTypeAdapter#registerAll(pool)
将这个类型池传入,以此达到多个地方共同使用。
与 ItemViewBinder
通讯
ItemViewBinder
对象可以接受外部类型、回调函数,只要在使用之前,传递进去即可,例如:
OnClickListener listener = new OnClickListener() { @Override public void onClick(View v){ // ... } } adapter.register(Post.class, new PostViewBinder(xxx, listener));
但话说回来,对于点击事件,能不依赖 binder
外部内容的话,最好就在 binder
内部完成。 binder
内部能够拿到 Views 和 数据,大部分情况下,完全有能力不依赖外部 独立完成逻辑。这样能使代码更加模块化,实现解耦和内聚。例如下面便是一个完全自包含的例子:
public class SquareViewBinderextends ItemViewBinder<Square,SquareViewBinder.ViewHolder>{ @NonNull @Override protectedViewHolderonCreateViewHolder( @NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { View root = inflater.inflate(R.layout.item_square, parent, false); return new ViewHolder(root); } @Override protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull Square square){ holder.square = square; holder.squareView.setText(valueOf(square.number)); holder.squareView.setSelected(square.isSelected); } public class ViewHolderextends RecyclerView.ViewHolder{ private TextView squareView; private Square square; ViewHolder(final View itemView) { super(itemView); squareView = (TextView) itemView.findViewById(R.id.square); itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v){ itemView.setSelected(square.isSelected = !square.isSelected); } }); } } }
使用断言,比传统 Adapter 更加易于调试
众所周知,如果一个传统的 RecyclerView
Adapter
内部有异常导致崩溃,它的异常栈是不会指向到你的 Activity
,这给我们开发调试过程中带来了麻烦。如果我们的 Adapter
是复用的,就不知道是哪一个页面崩溃。而对于 MultiTypeAdapter
,我们显然要用于多个地方,而且可能出现开发者忘记注册类型等等问题。为了便于调试,开发期快速失败, MultiType 提供了很方便的断言 API: MultiTypeAsserts
,使用方式如下:
import static me.drakeet.multitype.MultiTypeAsserts.assertAllRegistered; import static me.drakeet.multitype.MultiTypeAsserts.assertHasTheSameAdapter; public class SimpleActivityextends MenuBaseActivity{ private Items items; private MultiTypeAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list); items = new Items(); adapter = new MultiTypeAdapter(items); adapter.register(TextItem.class, new TextItemViewBinder()); for (int i = 0; i < 20; i++) { items.add(new TextItem(valueOf(i))); } /* 断言所有使用的类型都已注册 */ assertAllRegistered(adapter, items); recyclerView.setAdapter(adapter); /* 断言 recyclerView 使用的是正确的 adapter */ assertHasTheSameAdapter(recyclerView, adapter); } }
assertAllRegistered
和 assertHasTheSameAdapter
都是可选择性使用, assertAllRegistered
需要在加载或更新数据之后, assertHasTheSameAdapter
必须在 recyclerView.setAdapter(adapter)
之后。
这样做以后, MultiTypeAdapter
相关的异常都会报到你的 Activity
,并且会详细注明出错的原因,而如果符合断言,断言代码不会有任何副作用或影响你的代码逻辑,这时你可以把它当作废话。关于这个类的源代码是很简单的,有兴趣可以直接看看源码: drakeet/multitype/MultiTypeAsserts.java
支持 Google AutoValue
AutoValue 是 Google 提供的一个在 Java 实体类中自动生成代码的类库,使你更专注于处理项目的其他逻辑,它可使代码更少,更干净,以及更少的 bug.
当我们使用传统方式创建一个 Java 模型类的时候,经常需要写一堆 toString()
、 hashCode()
、getter、setter 等等方法,而且对于 Android 开发,大多情况下还需要实现 Parcelable
接口。这样的结果是,我本来想要一个只有几个属性的小模型类,但出于各种原因,这个模型类方法数变得十分繁复,阅读起来很不清爽,并且难免会写错内容。AutoValue 的出现解决了这个问题,我们只需定义一些抽象类交给 AutoValue,AutoValue 会 自动 生成该抽象类的具体实现子类,并携带各种样板代码。
更详细的介绍内容和使用教程,我会在文章末尾会给出 AutoValue 的相关链接,不熟悉 AutoValue 可以借此机会看一下,在这里就不做过多介绍了。新手暂时看不懂也不必纠结,了解之后都是十分容易的。
MultiType支持了 Google AutoValue,支持自动映射某个已经注册的类型的 子类 到同一 ItemViewBinder
,规则是:如果子类 有 注册,就用注册的映射关系;如果子类 没 注册,则该子类对象使用注册过的父类映射关系。
FlatTypeAdapter(已废弃)
MultiType 3.0 之前提供了一个 FlatTypeAdapter
类,3.0 之后,这个类已经被删除了,你可以完全不必关心它。如果你使用过它,现在它已经被一对多方案替代了,请转成使用一对多功能实现。
MultiType 与下拉刷新、加载更多、HeaderView、FooterView、Diff
MultiType设计从始至终,都极力避免往复杂化方向发展,一开始我的设计宗旨就是它应该是一个非常纯粹的、专一的项目,而非各种乱七八糟的功能都要囊括进来的多合一大型库,因此它很克制,期间有许多人给我发过一些无关特性的 Pull Request,表示感谢,但全被拒绝了。
对于很多人关心的 下拉刷新、加载更多、HeaderView、FooterView、Diff 这些功能特性,其实都不应该是 MultiType 的范畴, MultiType 的分内之事是做类型、事件与 View 的分发、连接工作,其余无关的需求,都是可以在 MultiType 外部完成,或者通过继承 进行自行封装和拓展,而作为一个基础、公共类库,我想它是不应该包含这些内容。
但很多新手可能并不习惯代码分工、模块化,因此在此我有必要对这几个点简单示范下如何在 MultiType 之外去实现:
-
下拉刷新:
对于下拉刷新,
Android
官方提供了support.v4
SwipeRefreshLayout
,在Activity
层面,可以拿到SwipeRefreshLayout
调用setOnRefreshListener
设置监听器即可.或者参考我的 rebase-android 项目编写的 SwipeRefreshDelegate.java .
-
加载更多:
RecyclerView
提供了addOnScrollListener
滚动位置变化监听,要实现加载更多,只要监听并检测列表是否滚动到底部即可,有多种方式,鉴于LayoutManager
本应该只做布局相关的事务,因此我们推荐直接在OnScrollListener
层面进行判断。提供一个简单版OnScrollListener
继承类:public abstract class OnLoadMoreListenerextends RecyclerView.OnScrollListener{ private LinearLayoutManager layoutManager; private int itemCount, lastPosition, lastItemCount; public abstract void onLoadMore(); @Override public void onScrolled(RecyclerView recyclerView,intdx,intdy){ if (recyclerView.getLayoutManager() instanceof LinearLayoutManager) { layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); itemCount = layoutManager.getItemCount(); lastPosition = layoutManager.findLastCompletelyVisibleItemPosition(); } else { Log.e("OnLoadMoreListener", "The OnLoadMoreListener only support LinearLayoutManager"); return; } if (lastItemCount != itemCount && lastPosition == itemCount - 1) { lastItemCount = itemCount; this.onLoadMore(); } } }
或者参考我的 rebase-android 项目编写的 LoadMoreDelegate.java .
-
获取数据后做 Diff 更新:
MultiType支持 onBindViewHolder with payloads,详情见
ItemViewBinder
类文档。对于 Diff,可以在Activity
中进行 Diff,或者继承MultiTypeAdapter
提供接收数据方法,在方法中进行 Diff. MultiType 不提供内置 Diff 方案,不然需要依赖 v4 包,并且这也不应该属于它的范畴。 -
HeaderView、FooterView
MultiType其实本身就支持
HeaderView
、FooterView
,只要创建一个Header.class
-HeaderViewBinder
和Footer.class
-FooterViewBinder
即可,然后把new Header()
添加到items
第一个位置,把new Footer()
添加到items
最后一个位置。需要注意的是,如果使用了 Footer View,在底部插入数据的时候,需要添加到最后位置 - 1
,即倒二个位置,或者把Footer
remove 掉,再添加数据,最后再插入一个新的Footer
.
实现 RecyclerView 嵌套横向 RecyclerView
MultiType天生就适合实现类似 Google Play 或 iOS App Store 那样复杂的首页列表,这种页面通常会在垂直列表中嵌套横向列表,其实横向列表我们完全可以把它视为一种 Item
类型,这个 item 持有一个列表数据和当前横向列表滑动到的位置,类似这样:
public class PostList{ public final List<Post> posts; public int currentPosition; public PostList(@NonNull List<Post> posts){this.posts = posts;} }
对应的 HorizontalItemViewBinder
类似这样:
public class HorizontalItemViewBinder extends ItemViewBinder<PostList, HorizontalItemViewBinder.ViewHolder> { @NonNull @Override protectedViewHolderonCreateViewHolder( @NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { /* item_horizontal_list 就是一个只有 RecyclerView 的布局 */ View view = inflater.inflate(R.layout.item_horizontal_list, parent, false); return new ViewHolder(view); } @Override protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull PostList postList){ holder.setPosts(postList.posts); } static class ViewHolderextends RecyclerView.ViewHolder{ private RecyclerView recyclerView; private PostsAdapter adapter; private ViewHolder(@NonNull View itemView){ super(itemView); recyclerView = (RecyclerView) itemView.findViewById(R.id.post_list); LinearLayoutManager layoutManager = new LinearLayoutManager(itemView.getContext()); layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); recyclerView.setLayoutManager(layoutManager); /* adapter 只负责灌输、适配数据,布局交给 LayoutManager,可复用 */ adapter = new PostsAdapter(); // 或者直接使用 MultiTypeAdapter 更加方便 recyclerView.setAdapter(adapter); /* 在此设置横向滑动监听器,用于记录和恢复当前滑动到的位置,略 */ ... } private void setPosts(List<Post> posts){ adapter.setPosts(posts); adapter.notifyDataSetChanged(); } } }
实现线性布局和网格布局混排列表
这个课题其实也不属于 MultiType 的范畴, MultiType 的职责是做数据类型分发,而不是布局,但鉴于很多复杂页面都会需要线性布局和网格布局混排,我就简单讲一讲,关键在于 RecyclerView
的 LayoutManager
. 虽然是线性和网格混合,但实现起来其实只要一个网格布局 GridLayoutManager
,如果你查看 GridLayoutManager
的官方源码,你会发现它其实继承自 LinearLayoutManager
. 以下是示例和解释:
public class MultiGridActivityextends MenuBaseActivity{ private final static int SPAN_COUNT = 5; private MultiTypeAdapter adapter; private Items items; @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_multi_grid); items = new Items(); RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list); final GridLayoutManager layoutManager = new GridLayoutManager(this, SPAN_COUNT); /* 关键内容:通过 setSpanSizeLookup 来告诉布局,你的 item 占几个横向单位, 如果你横向有 5 个单位,而你返回当前 item 占用 5 个单位,那么它就会看起来单独占用一行 */ layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(intposition){ return (items.get(position) instanceof Category) ? SPAN_COUNT : 1; } }); recyclerView.setLayoutManager(layoutManager); adapter = new MultiTypeAdapter(items); adapter.applyGlobalMultiTypePool(); adapter.register(Square.class, new SquareViewBinder()); assertAllRegistered(adapter, items); recyclerView.setAdapter(adapter); loadData(); } private void loadData(){ // ... } }
数据扁平化处理
在一个 垂直 RecyclerView
中,item 们都是同级的,没有任何嵌套关系,但我们的数据结构往往存在嵌套关系,比如 Post
内部包含了 Comment
s 数据,或换句话说 Post
嵌套了 Comment
,就像微信朋友圈一样,”动态” 伴随着 “评论”。那么如何把 非扁平化 的数据排布在 扁平 的列表中呢?必然需要一个 数据扁平化处理 的过程,就像 ListView
的数据需要一个 Adapter
来适配, Adapter
就像一个油漏斗,把油引入瓶子中。我们在面对嵌套数据结构的时候,可以采用如下的扁平化处理,关于扁平化这个词,不必太纠结,简单说,就是把嵌套数据都拉出来,摊平,让 Comment
和 Post
同级,最后把它们都 add 进同一个 Items
容器,交给 MultiTypeAdapter
. 示例:
假设:你的 Post
是这样的:
public class Post{ public String content; public List<Comment> comments; }
假设:你的 Comment
是这样的:
public class Comment{ public String content; }
假设:你服务端返回的 JSON 数据是这样的:
[ { "content":"I have released the MultiType v2.2.2", "comments":[ {"content":"great"}, {"content":"I love your post!"} ] } ]
那么你的 JSON 转成 Java Bean 之后,你拿到手应该是个 List<Post> posts
对象,现在我们写一个扁平化处理的方法:
privateList<Object>flattenData(List<Post> posts){ final List<Object> items = new ArrayList<>(); for (Post post : posts) { /* 将 post 加进 items,Binder 内部拿到它的时候, * 我们无视它的 comments 内容即可 */ items.add(post); /* 紧接着将 comments 拿出来插入进 items, * 评论就能正好处于该条 post 下面 */ items.addAll(post.comments); } return items; }
最后我们所有的 posts
在加入全局 MultiType Items
之前,都需要经过扁平化处理:
items.addAll(flattenData(posts)); adapter.notifyDataSetChanged();
整个过程其实并不困难,相信大家都已经理解了。
更多示例
MultiType的开源项目提供了许多的 samples (示例) 程序,这些示例秉承了一贯的代码清晰、干净的风格,十分易于阅读:
-
这是一个类似微博数据结构的示例,数据两层结构,Item 也是两层结构:一层框架(包含头像用户名等),一层 content view(微博内容),内容嵌套于框架中。微博的每一条微博 item 都包含了这样两层嵌套关系,这样做的好处是,你不必每个 item 都去重复制造一遍外层框架。
或者换一个比喻,就像聊天消息,一条聊天消息也是两层的,一层头像、用户名、聊天气泡框,一层你的文字、图片等。另外,每一种消息都有左边和右边的样式,分别对应别人发来的消息和你发出的消息。如果左边算一种,右边又算一种,就是比较不好的设计了,会导致布局内容重复、冗余,修改操作都要做两遍。最好的方案是让他们视被为同一种类型,然后在 item 框层次进行左右边判断和框架相关数据绑定。
我提供的这个二级
ItemViewBinder
示例便是这样的两层结构。它能够让你每次新增加一个类型,只要实现内容即可,框不应该重复实现。如果再不明白,或许你可以看看我的这个示例中 微博 Item 框的布局:
从我这个
frame
布局可以看出来,它内部有一个FrameLayout
作为container
将用于容纳不同的微博内容,而这一层框架则是共同的。这个例子算高级中的高级,但实际上也是很简单,展示了 MultiType 优秀的可拓展能力。完整运行结果展示如下:
注:以上我们并没有提到服务端 JSON 数据转为我们定义的 Weibo 对象过程,实际上对于完整链路,这个过程是需要做数据转换,我们需要在
WeiboContent
层加一个type
或describe
字段用于描述微博内容类型,然后再将微博内容的 JSON 文本转为具体微博内容对象交给 Weibo. 这个内容建议直接阅读这个 sample 的WeiboContentDeserializer
源码,我利用了一种很简单又巧妙的方式,在 JSON 解析底层便进行抽象数据具体化,使得客户端和服务端都能够轻松适应这种微博和微博内容嵌套关系。 -
一个 Material Design 的关于页面,核心基于 MultiType,包含了多种 items,美观,容易使用。
-
使用
MultiType
和GridLayoutManager
实现网格和线性混合布局,实现一个选集页面。 -
TimeMachine 使用了 MultiType 来创建一个复杂的聊天页面,页面和需求虽然复杂,但使用 MultiType 显得轻松简单。
-
使用
MultiType
实现类似 Bilibili iOS 端首页复杂的多类型列表视图,包括嵌套横向RecyclerView
.
Q & A
-
Q: 觉得 MultiType 不够精简,应该怎么做?
A: 在前面 “设计思想” 中我们谈到: MultiType 或许不是使用起来最简单的,但很可能是使用起来最灵活的。 其中的缘由是它高度可定制、可拓展,而不是把一些路封死。作为一个基础类库,简单和灵活需要一个均衡点,过度精简便要以失去灵活性为代价。如果觉得 MultiType 不够精简,想将它修改得更加容易使用,我推荐的方式是去继承
MultiTypeAdapter
或ItemViewBinder
,甚至你可以重新实现一个TypePool
再设置给MultiTypeAdapter
. 我们不应该直接到底层去修改、破坏它们。总之,利用开放接口或继承的做法不管对于 MultiType 还是其它开源库,都应该是定制的首选。 -
Q: 在
ItemViewBinder
中如何拿到Context
对象?A: 有人问我说,他在
ItemViewBinder
里使用 Glide 来加载图片需要获取到 ActivityContext
对象,要怎么才能拿到Context
对象?这是一个特别简单的问题,但我想既然有人问,应该比较典型,我就详细解答下:首先,在 Android 开发中,任何View
对象都能通过view.getContext()
拿到Context
对象,这些对象本质上都是Activity
对象的引用。而在我们的ItemViewBinder
中,可以通过holder.itemView.getContext()
获取到Context
对象,也可以通过 viewHolder 的任意View
对象getContext()
方法拿到Context
对象.Context
中文释义是 “上下文对象” ,一般情况下,都是由Activity
传递给View
s,View
s 内部再进行传递。比如我们使用RecyclerView
,Activity
会将它的Context
传递给RecyclerView
,RecyclerView
再传递给Adapter
,Adapter
再传递给ViewHolder
的itemView
,itemView
再传递给它的各个子View
s,传递来传递去,其实都是同一个对象的引用。总而言之,拿到
Context
对象非常简单,只要你能拿到一个View
对象,调用view.getContext()
即可。另外,也可以参考章节,我们可以很方便地给binder
传递任何对象进去,包括Context
对象。 -
Q:如何在
ItemViewBinder
中获取到 item position?A: 从 v2.3.5 版本开始,只需要在你的
ItemViewBinder
子类里调用getPosition(holder)
方法即可。另外,ItemViewBinder
还提供了getAdapter()
或许也是很多人想要的,比如调用 adapter 进行 notify 刷新视图等。
感谢
在 MultiType 开发维护过程中,很多朋友给了我很多反馈,我也非常乐意于与大家交流,有问必答,因为这是一个难得不错的项目,它比较接近我心中对于一个完美项目的要求:设计精巧,代码干净漂亮。
我向来是不太在意项目的 star 数目的,但热衷于把我的好东西分享给更多人使用,因此在 我的 GitHub 首页我不会把我一些高 star 项目摆出来,而是放一些我觉得代码相对比较好的项目。这是我的动力,我想写一份完美的代码,就像王垠的 40 行一样,达到自觉得天衣无缝、犹如天神衣袖般的优雅,嗯,要是哪天我做到了,我就停止开源,哈哈。
话说回来,这个项目,特别感谢大家的帮忙、反馈,感谢一些朋友的 PR、贡献和推荐,是你们让我觉得开源是一件除了完善自我之外 还充满了意义的一件事情 – 能够与更多人协同,能够面向更宽广的世界,谢谢大家!以下是感谢名单:
70kg 、 zubinxiong 、 WanLiLi 、 代码家 、 CaMnter 、 android-xiaowei 、 burgessjp 、 lixi0912 、 simidaxu 、 咕咚 、 LuckyJayce 、 BelongsH 、 tmexcept 、 TellH 、 Ray Pan 、 Zack 、 Chris
引用文献
- 《Android 内存泄漏案例和解析》 https://drakeet.me/android-leaks
- 《Android 复杂的多类型列表视图新写法:MultiType》 https://drakeet.me/multitype
- 《使用 Google AutoValue 自动生成代码》 http://tedyin.me/2016/04/11/auto-value/
- 我的个人博客http://drakeet.me
- MultiType 源代码 https://github.com/drakeet/MultiType
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
深入浅出SQL(中文版)
贝里 编 / O‘Reilly Taiwan公司 / 东南大学 / 2009-6 / 98.00元
你将从《深入浅出SQL(中文版)》学到什么?在如今的世界,数据就是力量,但是成功的真正秘诀却是管理你的数据的力量。《深入浅出SQL(中文版)》带你进入SQL语言的心脏地带,从使用INSERT和SELECT这些基本的查询语法到使用子查询(subquery)、连接(join)和事务(transaction)这样的核心技术来操作数据库。到读完《深入浅出SQL(中文版)》之时,你将不仅能够理解高效数据库设......一起来看看 《深入浅出SQL(中文版)》 这本书的介绍吧!