【轉載】逃離adapter的地獄-針對多個View type的組合實現方案

英文原文:JOE'S GREAT ADAPTER HELL ESCAPEhtml

轉載地址:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0810/3282.htmljava

讓我來告訴你一個關於喬某人的故事,一個在MyLittleZoo Inc工做的安卓開發者。關於他是如何從爲具備多個view type的Adapter建立不一樣對象中解脫出來,最終成功實現可複用Adapter的。android

曾經有一個叫作喬某某的人,它是一個安卓開發者,爲一家名叫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大體是這樣實現的:git

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來重用以前的代碼:github

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:app

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上。maven

幾周以後,產品經理跑來跟喬說,生意的發展不及預期。爲了賺錢,gongsi決定和一家大廣告公司簽定一份協議。這家廣告公司能夠在MyLittleZoo的安卓app上展現橫幅,換句話說:喬的公司把本身的靈魂出賣給了魔鬼。喬的工做是使用別人提供的廣告sdk,把廣告橫幅包含在app中。時間緊迫,公司須要錢(從廣告上收益)。app的更新必須儘快發佈。由於廣告橫幅須要和RecyclerView中的其餘的item顯示在一塊兒,喬決定建立一個名叫calledAdvertismentAdapter的基類adapter。ide

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版的僞代碼,用於描述真實代碼的大體樣子。

相關文章
相關標籤/搜索