1. 程式人生 > >通用的recyclerview adapter 適配

通用的recyclerview adapter 適配

統一管理適配Adapter多型別處理,更加方便的管理和操作。

joe 偉大的地獄逃脫者

讓我來和你講個故事,關於joe 的MyLittleZoo公司 。關於他是怎麼創造可複用的不同型別的Recyclerview adapter來終結他的噩夢。和他最終是怎麼用最小的代價管理和實現可複用adapters。
從前,有一個Android developer的小夥叫joe ,他在一個初創公司叫MyLittleZoo 的地方上班。這家公司是在網上銷售寵物物品的。joe 的工作是負責維護一個和pc網頁上相同功能的android 客戶端。所以他90%的工作是需要早recycler view上展示一系列的item。在版本1.0 需要展示 accessories 型別的list。joe通過實現AccessoiresAdapter來展示accessories型別。但是列表的特殊展示需用使用item_accessory_offer.xml,而正常的展示是使用item_accessory.xml。所以這個adapter是有兩種型別。adapter的一種型別是為不同的item填充不同的佈局。本質上來說,一個view type 只有一個唯一的integer id。所以joe的 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)): } } }

到目前為止,MyLittleZoo 1.0版本在市場上進展的還不錯。隨著MyLittleZoo的使用者量不斷的增長,app也要進行不斷的迭代。joe的新需求是在一個新的Activity去展示不同型別的item。NewsTeaser型別要和Accessories型別展示在一起。由於HomeAdapter需要展示Accessories,因此他決定通過繼承的方式複用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需要實現一些關於寵物食物的提示。因此joe需要實現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))
  }

  ...

}

他能夠及時提交專案上線產品經理很開心。MyLittleZoo 2.0版本在市場上成功釋出。
幾個星期後,產品經理找到joe告訴他專案發展的沒有預期的好。為了賺錢,公司決定和廣告公司簽訂一份合同。廣告公司可以在MyLittleZoo的app上展示一個banner。換句話說:他們出賣了他們的靈魂。joe 的工作是將廣告sdk嵌入,並展示banner 在app裡。隨著時間的流逝,公司需要錢(通過廣告運營商獲取)。APP 版本的更新越快越好。因為廣告banner需要和其他的item一起展示在recyclerview中,joe決定建立一個base adapter 基類的adapter.稱之為AdvertismentAdapter:
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 extends AdvertisementAdapter
HomeAdapter extends AccessoiresAdapter extends AdvertisementAdapter
PetFoodTipAdapter extends AdvertisementAdapter

版本3.0 的在漫天的廣告中終於在市場上釋出。當然再一次產品經理對於joe的工作表示非常的讚賞。
半年後,產品經理再次來找joe的茬。世事萬變啊,joe。我們驚奇的發現,MyLittleZoo 的Android使用者不喜歡blinking blinking的廣告,幾乎亮瞎了使用者的雙眼。導致app在市場上獲得大面積的負面回覆。活躍使用者戲劇性的掉了一大截,公司不能好好地賺錢了。但是MyLittleZoo 不能單純的把廣告從應用上移除,他們和魔鬼,額。。。就是他們的廣告商簽訂了長期有效的廣告合同。
然後銷售部一個小夥有個很讚的主意,我們制定第二個app在recyclerview只展示 NewsTeaser 型別和 PetFoodTipl型別,不要亮瞎眼的廣告,這個計劃會重新獲取使用者的信任。此外,產品經理告訴joe這個版本必須要在兩天時間內釋出版本。因為這個週末是即將到來的這個是寵物博覽會。到時候這個app一定要及時出現然後震驚全場。joe認為這個是可行的。他已經有了NewsTeaser 型別and PetFoodTip型別這些xml佈局,而且adapter已經實現了。所以joe需要做的就是把這些共有的放置到公有的library裡。實現MyLittleZoo和新的app能夠共享使用
joe開始準備把方法移動到library,然而他意識到了他有一堆亂七八糟的事情要面對。還記得繼承adapter的那一些類嗎。
1.每個adapter都是繼承 至AdvertisementAdapter,但是沒有廣告需要展示在新的app裡。此外,提供廣告展示的sdk是十分的冗餘的,經常因為記憶體洩露導致crash。即使sdk沒有廣告展示也會經常在後臺做一系列的操作。最重要的是,需要匯入廣告sdk 在新的app是沒辦法接受的
2.沒有adapter可以複用展示NewsTeaser型別(HomeAdapter的部分)和PetFoodTip型別(PetFoodTipAdapter的部分)。joe這個時候要怎麼做。他可以建立一個新的NewsTipAdapter繼承自HomeAdapter 然後他需要新增PetFoodTip為新的type.但是這意味著他講有兩個adapter持有一樣型別的petFoodTip的型別

歡迎來再次回到joe的adapter地獄

我的天啊,joe陷入異常的絕望,失落。絕望之外緊接而至的是恐慌。他應該怎麼去修復這個bug,他需要怎麼去修復才不會重蹈覆轍,可能該死的產品汪在一個月後又有新的特性新的型別需要展示。
joe緊忙開始把需求都寫在白板上。但是腦海裡沒有一絲的思路。傷心難過的時候,他想起了小時候,當他還是一個孩子。童年的時候是過得多麼的輕鬆。每天需要唯一需要做的事情就是在玩完樂高積木的時候清理房間。就這麼簡單。額。。。樂高。。等等,樂高。。。突然有一個絕妙的思路在腦海裡閃現。我真正需要構建一個adapter就想搭樂高積木一樣。先做一個空白的基礎架構然後根據圖片再把積木拼湊好。如果你需要一個視窗,那就拿窗戶的模組拼湊上。如果需要一個屋頂,就拿符合屋頂的模組。如果屋子需要一個後花園,則拿一個樂高的花朵。我勒個去,腦海裡滿滿的都是畫面感。

組合優於繼承
多少次他和其他開發者討論的時候也是說“組合優於繼承”。直到現在這也只是一個很好的slogan,他從來都沒有根據這個準則構建。因此一個空白adapter是一個基礎架構。viewtype是可複用的部件。
因此joe開始定義可複用的樂高模組,像NewsTeaserAdapterDelegate和PetFoodTipAdapterDelegate

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 = (PetFoodTipViewHolder) vh;

      vh.image.setImageRes(tip.getImage());
      vh.text.setText(tip.getText());
  }
}

然後他拿著這個基礎架構,一個空的adapter.然後將樂高的模組放到新的app的NewsTipAdapter

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);
    }
  }
}

我猜你已經get到了。代替繼承,joe已經定義了delegate給一系列的view型別。每個delegate的職責是建立和繫結view holder。正如上面你所看到的程式碼片段。有一系列的樣板程式碼需要填寫。joe發現了一個更好的方法解決問題。

/**
 * @param <T> the type of adapters data source i.e. List<Accessory>
 */
public interface AdapterDelegate<T> {

  /**
   * 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) {
    ...
  }
}

這個想法是註冊一個AdapterDelegates給AdapterDelegatesManager管理。AdapterDelegatesManager通過一系列的邏輯處理給出符合view type 的正確AdapterDelegate。所以綜上所述,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()); // Assigns internally ViewType integer
    delegates.add(new PetFoodTipAdapterDelegate());
  }

  @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);
  }
}

我猜你已經能夠想象出MyLittle app其他的adapter是怎麼展現。這裡有AdvertisementAdapterDelegate,NewsTeaserAdapterDelegate,PetFoodTipAdapterDelegate和AccessoryAdapterDelegate。從現在開始,多個adapter可以通過非常重要的view type (AdapterDelegates) 型別組合起來。其他的優點是,可以從一個龐大的adapter的邏輯裡抽取出,填充佈局,構造view holder 和繫結view holder等邏輯讓程式碼分離開模組話,可複用的adapterDelegate。你有意識到,adapter程式碼裡非常的清淨,還有你特別關心的,可以讓程式碼可擴充套件性更強和更高的解耦。另外一個更好的是可以很多team成員同時在同一個adapter上進行程式碼操作不用考慮複雜的merge.因為沒有人會觸碰到adapter複雜的程式碼。team成員可以更加專注的寫每個型別的AdapterDelegate。
joe很開心,產品經理和開心,使用者很開心,大家都很開心。joe 太開心了,然後決定把AdapterDelegates放到自己的額庫裡準備開源。結局總算是美好的。

ps. 這個庫也提供ListDelegationAdapter 的基類,已經把RecyclerView.Adapter的方法和AdapterDelegatesManager的方法放到一起,這樣你可以減少大量重讀程式碼的工作

public class NewsTipAdapter extends ListDelegationAdapter {

  public NewsTipAdapter(){
    // delegatesManager is a field defined in super class
    // ViewType integer is assigned internally by delegatesManager
    delegatesManager.add(new NewsTeaserAdapterDelegate());
    delegatesManager.add(new PetFoodTipAdapterDelegate());
  }

}

最後:joe 和MyLittlezoo 都不是真實的。都是作者的想想。但是文章的程式碼片段可以不編寫。這個是java裡虛擬碼,是幫助你理解這個思想的。