【转载】逃离adapter的地狱-针对多个View type的组合实现方案
英文原文:JOE'S GREAT ADAPTER HELL ESCAPE
转载地址:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0810/3282.html
让我来告诉你一个关于乔某人的故事,一个在MyLittleZoo Inc工作的安卓开发者。关于他是如何从为具有多个view type的Adapter创建不同对象中解脱出来,最终成功实现可复用Adapter的。
曾经有一个叫做乔某某的人,它是一个安卓开发者,为一家名叫MyLittleZoo Inc的初创公司工作。这是一家在网上销售宠物相关东西的公司。乔的工作是创建和维护一个与在线商店功能相同的安卓原生app。因此90%的开发工作都只是用RecyclerView显示一个列表。第一个版本1.0只需要显示一个配料列表,为此乔实现了一个AccessoiresAdapter,但是特别推荐的配料用item_accessory_offer.xml 显示,而普通配料则用item_accessory.xml显示。因此这个Adapter有两种View type。在adapter中,view type可以让你为不容的item渲染不同的xml布局。在内部view type其实只是一个唯一的id,一个整型。因此乔的AccessoiresAdapter大致是这样实现的:
public class AccessoiresAdapter extends RecyclerView.Adapter {
final int VIEW_TYPE_ACCESSORY = 0;
final int VIEW_TYPE_ACCESSORY_SPECIAL_OFFER = 1;
List<Accessory> items;
@Override public int getItemViewType(int position) {
Accessory accessory = items.get(postion);
if (accessory.hasSpecialOffer()){
return VIEW_TYPE_ACCESSORY_SPECIAL_OFFER;
} else {
return VIEW_TYPE_ACCESSORY;
}
}
@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (VIEW_TYPE_ACCESSORY_SPECIAL_OFFER == viewType){
return new SpecialOfferAccessoryViewHolder(inflater.inflate(R.layout.item_accessory_offer, parent));
} else {
return new AccessoryViewHolder (inflater.inflate(R.layout.item_accessory)):
}
}
...
}
MyLittelZoo安卓app1.0发布在了play store上。一切看上去都还好,一直相安无事。
后来,MyLittelZoo变大了,app也是如此。乔需要实现一个新的启动Activity,这里需要显示不同item:NewsTeaser需要和配料显示在一起,所以他建立了HomeAdapter,因为HomeAdapter需要同时显示配料,所以他决定通过继承AccessoriesAdapter来重用之前的代码:
public class HomeAdapter extends AccessoriesAdapter {
final int VIEW_TYP_NEWS_TEASER = 2;
@Override public int getItemViewType(int position) {
if (items.get(position) instanceof NewsTeaser){
return VIEW_TYP_NEWS_TEASER;
} else {
// accessories and special offers
return super.getItemViewType(position);
}
}
@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (VIEW_TYP_NEWS_TEASER == viewType){
return new NewsTeaserItem( inflater.inflate(R.layout.item_news_teaser, parent));
} else {
// accessories and special offers
return super.onCreateViewHolder(parent, viewType);
}
}
...
}
同时还有一个新的只显示关于宠物食物的小建议的Activity需要实现,因此乔实现了PetFoodTipAdapter:
public class PetFoodTipAdapter extends RecyclerView.Adapter {
final int VIEW_TYP_FOOD_TIP = 0;
@Override public int getItemViewType(int position) {
return VIEW_TYP_FOOD_TIP;
}
@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new PetFoodViewHolder(inflater.inflate(R.layout.item_pet_food, parent))
}
...
}
因为他能按时交付,他的项目经理非常高兴。MyLittelZoo 2.0 成功发布在 Play Store上。
几周之后,产品经理跑来跟乔说,生意的发展不及预期。为了赚钱,gongsi决定和一家大广告公司签订一份协议。这家广告公司可以在MyLittleZoo的安卓app上展示横幅,换句话说:乔的公司把自己的灵魂出卖给了魔鬼。乔的工作是使用别人提供的广告sdk,把广告横幅包含在app中。时间紧迫,公司需要钱(从广告上收益)。app的更新必须尽快发布。因为广告横幅需要和RecyclerView中的其他的item显示在一起,乔决定创建一个名叫calledAdvertismentAdapter的基类adapter。
public class AdvertismentAdapter extends RecyclerView.Adapter {
final int VIEW_TYP_ADVERTISEMENT = 0;
@Override public int getItemViewType(int position) {
return VIEW_TYP_ADVERTISEMENT;
}
@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new AdvertismentViewHolder(inflater.inflate(R.layout.item_advertisment, parent))
}
...
}
从此,其他的所有adapter都继承自AdvertisementAdapter:
AccessoiresAdapter 继承 AdvertisementAdapter
HomeAdapter 继承 AccessoiresAdapter 继承 AdvertisementAdapter
PetFoodTipAdapter 继承 AdvertisementAdapter
到处充满广告的3.0 版本发布在了Play Store上。产品经理再次对乔的工作感到满意。
半年之后,产品经理再次敲响了乔的门,告诉他事情有了变化。MyLittleZoo 安卓app的用户不喜欢3.0版本中那些亮瞎人狗眼的广告横幅,app在play store上受到了大量的差评。访问量大减,公司不再盈利了。但是MyLittleZoo不能简单的从app中撤掉广告,因为他们和“魔鬼”签订了一个相当长的合同,魔鬼这里指的当然是广告公司。
然后MyLittleZoo市场部有个聪明的家伙,他想到了重启一个app项目,只在一个RecyclerView中显示NewsTeaser和PetFoodTip。没有广告,没有推荐。按计划是为了赢回用户的信任。再一次,产品经理告诉乔app需要在两天之内发布,因为在接下来的周末又一个很大的宠物节,app需要在届时呈现才行。乔认为这是可行的。他已经有了NewsTeaser和PetFoodTip的xml布局,adapter也已经是实现了的。因此乔只需把它们搬到一个library中,在原始的MyLittleZoo app和新的无广告版的advertisement app中共享。
乔准备开始把东西搬到这个library,这时他意识到了此时面对的混乱境地:你还记得adapter的继承关系吗?
每个adapter都继承自AdvertisementAdapter。但是新的app不需要显示广告。此外,显示横幅的广告sdk实在是太bug了,导致了太多的内存泄漏,经常崩溃。即使没有显示广告,sdk也还是在背后干很多事情。因此在新的app中包含广告sdk时不能接受的。
对于同时显示NewsTeaser(HomeAdapter的一部分)和PetFoodTip(PetFoodTipAdapter的一部分),没有可以重用的adapter。乔该怎么办呢?他可以创建一个新的NewsTipAdapter继承自HomeAdapter,然后把PetFoodTip作为一个心的view type添加进去。但是那就意味着对于同一个view type有两个adapter需要维护。
欢迎来到adapter的地狱,乔!
可怜的娃儿,乔非常沮丧。沮丧过后是担忧。他该如何修复这个问题呢?他该如何修复才能避免一个月之后因为实现新功能(一个心的view type)而再次修复呢?
因此乔开始把自己的需求写在了白板上。但是没有想出什么办法。他很悲伤,他想到了自己的孩提时代,那个时候生活是多么轻松。那时候需要担心的唯一事情就是在耍完乐高以后清理自己的房间。乐高?等等!乔有了个聪明的主意:他真正需要的是像堆积乐高房子一样的构建自己的adapter:做好一个地基,然后把真正需要的乐高部件粘在一起。如果你的房子需要窗户,去取一个窗户部件;如果需要屋顶,取一个屋顶部件;如果需要后花园,取一朵花的部件。
靠,接着他就有了大致的图景:
组合优于继承
在和其他程序员讨论的时候,他不止一次同意“组合优于继承”的观点。在这之前,这对于他来说不过是一句口号而已,而现在他才真正根据这个准则构建出一点东西来。好了,一个空的adapter是地基。ViewType则是可重用的组件(乐高部件)。
因此乔开始定义可复用的乐高部件比如NewsTeaserAdapterDelegate和andPetFoodTipAdapterDelegate:
public class NewsTeaserAdapterDelegate {
private int viewType;
public NewsTeaserAdapterDelegate(int viewType){
this.viewType = viewType;
}
public int getViewType(){
return viewType;
}
public boolean isForViewType(List items, int position) {
return items.get(position) instanceof NewsTeaser;
}
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new NewsTeaserViewHolder(inflater.inflate(R.layout.item_news_teaser, parent, false));
}
public void onBindViewHolder(List items, int position, RecyclerView.ViewHolder holder) {
NewsTeaser teaser = (NewsTeaser) items.get(position);
NewsTeaserViewHolder vh = (NewsTeaserViewHolder) vh;
vh.title.setText(teaser.getTitle());
vh.text.setText(teaser.getText());
}
}
public class PetFoodTipAdapterDelegate {
private int viewType;
public PetFoodTipAdapterDelegate(int viewType){
this.viewType = viewType;
}
public int getViewType(){
return viewType;
}
public boolean isForViewType(List items, int position) {
return items.get(position) instanceof PetFoodTip;
}
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new PetFoodTipViewHolder(inflater.inflate(R.layout.item_pet_food, parent, false));
}
public void onBindViewHolder(List items, int position, RecyclerView.ViewHolder holder) {
PetFoodTip tip = (PetFoodTip) items.get(position);
PetFoodTipViewHolder vh = (NewsTeaserViewHolder) vh;
vh.image.setImageRes(tip.getImage());
vh.text.setText(tip.getText());
}
}
然后是地基,一个空的adapter,然后把乐高部件放在上面,创建NewsTipAdapter,在新app中使用:
public class NewsTipAdapter extends RecyclerView.Adapter{
final int VIEW_TYP_NEWS_TEASER = 0;
final int VIEW_TYP_FOOD_TIP = 1;
NewsTeaserAdapterDelegate newsTeaserDelegate;
PetFoodTipAdapterDelegate foodTipDelegate;
List items;
public NewsTipAdapter(){
newsTeaserDelegate = new NewsTeaserAdapterDelegate(VIEW_TYP_NEWS_TEASER);
foodTipDelegate = new PetFoodTipAdapterDelegate(VIEW_TYP_FOOD_TIP);
}
@Override public int getItemViewType(int position) {
if (newsTeaserDelegate.isForViewType(items, position)){
return newsTeaserDelegate.getViewType();
}
else if (foodTipDelegate.isForViewType(items, position)){
return foodTipDelegate.getViewType();
}
throw new IllegalArgumentException("No delegate found");
}
@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (newsTeaserDelegate.getViewType() == viewType){
return newsTeaserDelegate.onCreateViewHolder(parent);
}
else if (foodTipDelegate.getViewType() == viewType){
return foodTipDelegate.onCreateViewHolder(parent);
}
throw new IllegalArgumentException("No delegate found");
}
@Override public void onBindViewHolder(VH holder, int position){
int viewType = holder.getViewType();
if (newsTeaserDelegate.getViewType() == viewType){
newsTeaserDelegate.onBindViewHolder(items, position, holder);
}
else if (foodTipDelegate.getViewType == viewType){
foodTipDelegate.onBindViewHolder(items, position, holder);
}
}
}
我猜你应该看明白了。与使用继承不同,乔定为每个view type义了一个delegate。每个delegate负责创建和绑定ViewHolder。就如你看到的,上面的代码片段有许多散乱的代码。乔发现了一个插件式的解决办法:
/**
* @param <T> the type of adapters data source i.e. List<Accessory>
*/
public interface AdapterDelegate<T> {
/**
* Get the view type integer. Must be unique within every Adapter
*
* @return the integer representing the view type
*/
public int getItemViewType();
/**
* Called to determine whether this AdapterDelegate is the responsible for the given data
* element.
*
* @param items The data source of the Adapter
* @param position The position in the datasource
* @return true, if this item is responsible, otherwise false
*/
public boolean isForViewType(@NonNull T items, int position);
/**
* Creates the {@link RecyclerView.ViewHolder} for the given data source item
*
* @param parent The ViewGroup parent of the given datasource
* @return The new instantiated {@link RecyclerView.ViewHolder}
*/
@NonNull public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent);
/**
* Called to bind the {@link RecyclerView.ViewHolder} to the item of the datas source set
*
* @param items The data source
* @param position The position in the datasource
* @param holder The {@link RecyclerView.ViewHolder} to bind
*/
public void onBindViewHolder(@NonNull T items, int position, @NonNull RecyclerView.ViewHolder holder);
}
public class AdapterDelegatesManager<T> {
public AdapterDelegatesManager<T> addDelegate(@NonNull AdapterDelegate<T> delegate) {
...
}
public int getItemViewType(@NonNull T items, int position) {
...
}
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
...
}
public void onBindViewHolder(@NonNull T items, int position, @NonNull RecyclerView.ViewHolder viewHolder) {
...
}
}
这个办法就是把AdapterDelegate注册到一个AdapterDelegatesManager。AdapterDelegatesManager内部有决定不同view type采取何种AdapterDelegate的逻辑与调用相应的delegate方法。应用到NewsTipAdapter中的代码大致如下:
public class NewsTipAdapter extends RecyclerView.Adapter{
final int VIEW_TYP_NEWS_TEASER = 0;
final int VIEW_TYP_FOOD_TIP = 1;
List items;
AdapterDelegatesManager delegates = new AdapterDelegatesManager();
public NewsTipAdapter(){
delegates.add(new NewsTeaserAdapterDelegate(VIEW_TYP_NEWS_TEASER));
delegates.add(new PetFoodTipAdapterDelegate(VIEW_TYP_FOOD_TIP));
}
@Override public int getItemViewType(int position) {
return delegates.getItemViewType(items, position);
}
@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return delegates.onCreateViewHolder(parent, viewType);
}
@Override public void onBindViewHolder(VH holder, int position){
delegates.onBindViewHolder(items, position, holder);
}
}
我猜你应该能想象MyLittleZoo app其它adapter的样子了。总共有AdvertisementAdapterDelegate, NewsTeaserAdapterDelegate, PetFoodTipAdapterDelegate 和AccessoryAdapterDelegate。从此adapter可以通过那些真正需要的view type(AdapterDelegate)来组合。另外一个好处是你将inflating布局,创建view holder,绑定view holder的过程从adapter中分离出来,成了单独的,模块化的,可复用的AdapterDelegate。你有注意到现在adapter看起来是多了小巧吗?你可分散注意力到扩展性和降低耦合上面来了。另一个附加效果就是更多的团队成员可以在adapter上协同工作,而不用担心复杂的融合问题,因为不是每个人都接触庞大的adapter文件,而是团队成员同时专注于不同的AdapterDelegate 文件。
乔非常高兴,产品经理也非常高兴,用户也非常高兴,大家都非常高兴。(老外就是哆嗦!)。乔决定把这些AdapterDelegate放在自己的library中,并且开源,真是皆大欢喜。
你可以在github上找到这些AdapterDelegate ,同时也可以在maven central找到。
ps:AdapterDelegate library还提供了一个基类ListDelegationAdapter,它已经把RecyclerView.Adapter 的方法和AdapterDelegatesManager的方法放在了一起,因此你可以进一步减少代码:
public class NewsTipAdapter extends ListDelegationAdapter {
final int VIEW_TYP_NEWS_TEASER = 0;
final int VIEW_TYP_FOOD_TIP = 1;
public NewsTipAdapter(){
// delegatesManager is a field defined in super class
delegatesManager.add(new NewsTeaserAdapterDelegate(VIEW_TYP_NEWS_TEASER));
delegatesManager.add(new PetFoodTipAdapterDelegate(VIEW_TYP_FOOD_TIP));
}
}
更详细的内容情查看Github 上的library。
声明:乔和MyLittleZoo Inc都不是真实的。请注意本文涉及到的代码片段可能无法编译。它们是java版的伪代码,用于描述真实代码的大致样子。
【转载】逃离adapter的地狱-针对多个View type的组合实现方案的更多相关文章
- 【转载】Adapter用法总结大全
下面的是看到的比较好的地址: Android各种Adapter的用法: http://my.oschina.net/u/658933/blog/372151 Andro ...
- 转载 SharePoint【Site Definition 系列】– 创建Content Type
转载原地址: http://www.cnblogs.com/wsdj-ITtech/archive/2012/09/01/2470274.html Sharepoint本身就是一个丰富的大容器,里面 ...
- Android 利用RecyclerView.Adapter刷新列表中的单个view问题
首先使用RecyclerView的adapter继承:RecyclerView.Adapter public class OrderListAdapter extends RecyclerView.A ...
- Grunt针对静态文件的压缩,版本控制打包方案
在讲之前先谈谈大致步骤:安装nodejs -> 全局安装grunt -> 项目创建package.json --> 项目安装grunt以及grunt插件 -> 配置Gruntf ...
- 【转载】Java 升级到jdk7后DbVisualizer 6 启动空指针的处理方案
将JDK从6升级到了7(或从其他电脑移植DBV文件夹后),每当启动DbVisualizer 6的时候都会报空指针异常 在官网上找到了相关的方案,如下: In the DbVisualizer inst ...
- 【转载】#457 Converting Between enums and their Underlying Type
When you declare an enum, by default each enumerated value is represented internally with an int. (S ...
- [Node] 逃离回调地狱
逃离Node回调地狱 Background : 在Node中,函数的返回结果大多利用回调的方式处理.如简单的判断文件是否存在并读取内容: var fs = require('fs'); fs.exis ...
- Android高手进阶——Adapter深入理解与优化
Android高手进阶--Adapter深入理解与优化 通常是针对包括多个元素的View,如ListView,GridView.ExpandableListview,的时候我们是给其设置一个Adapt ...
- 业余草通告CSDN博客用户zhang__ao非法转载文章的公告
今天早上有粉丝给我反馈,CSDN的一位用户大量非法的转载了我的个人网站:业余草(www.xttblog.com)上的大量文章.现一对该用户转载业余草上网站上的所有文章进行了举报! 从上图中可以看出,该 ...
随机推荐
- GNU MAKE指南
GNU make 指南 翻译: 哈少 译者按: 本文是一篇介绍 GNU Make 的文章,读完后读者应该基本掌握了 make 的用法.而 make 是所有想在 Unix (当然也包括 Linux )系 ...
- 我的VSTO之路(五):Outlook初步开发之联系人扩展
原文:我的VSTO之路(五):Outlook初步开发之联系人扩展 上一讲我们完成对Word的介绍,文本开始,我将着重介绍Outlook.Outlook是微软Office中一个非常实用的工具,尤其在一个 ...
- (转载)eclipse 快捷键大全,eclipse查找类,文件,添加注释
(转载)http://hi.baidu.com/fegro/item/8224c8c28b174627ee466598 /* ----------------------------------- ...
- 数据结构(树套树):ZJOI 2013 K大数查询
有几个点卡常数…… 发现若第一维为位置,第二维为大小,那么修改时第一维修改区间,查询时第一维查询区间,必须挂标记.而这种情况下标记很抽象,而且Push_down不是O(1)的,并不可行. 那要怎么做呢 ...
- tomcat中debug启动和start启动的区别
debug启动tomcat:修改代码不加方法,不加参数,只是单纯的修改方法,不用重启tomcat(热部署). start启动tamcat:修改代码需要重启tomcat.
- 333. Largest BST Subtree
nlgn就不说了..说n的方法. 这个题做了好久. 一开始想到的是post-order traversal. 左右都是BST,然后自己也是BST,返还长度是左+右+自己(1). 左右其中一个不是,或者 ...
- linux网络编程echo多进程服务器
echo_server 多进程版本 #include <unistd.h> #include <stdlib.h> #include <stdio.h> #incl ...
- php字符编码转utf-8格式
<? function is_utf8($other) { if (preg_match("/^([".chr(228)."-".chr(233).&qu ...
- JSON字符串与JSON对象的区别及转换
JSON对象是直接可以使用JQuery操作的格式,和js中的对象一样,可以用对象(类名)点出属性(方法). JSON字符串仅仅只是一个字符串,一个整体,不截取的话没办法取出其中存储的数据,不能直接使用 ...
- 基本RC积分电路及原理分析
电阻R和电容C串联接入输入信号VI,由电容C输出信号V0,当RC (τ)数值与输入方波宽度tW之间满足:τ>>tW (一般至少为10倍以上),这样的电路称为积分电路 在电容C两端(输出端) ...