Epoxy: Airbnb的安卓視圖架構
Epoxy是由Airbnb開發的,用于構建復雜RecyclerView的安卓庫。 Epoxy介紹: github.com/airbnb/epoxy
以下是 Epoxy: Airbnb’s View Architecture on Android 的譯文。
Android中的RecyclerView是一個顯示列表的強大工具,但是它的用法比較瑣碎。顯示復雜度高的列表是我們團隊的一個常用需求,比如具有多種視圖類型,分頁功能,支持平板和item動畫的列表。我們發現自己總是不斷的重復相同的設置。所以開發了 Epoxy 來減輕這個趨勢,以簡化基于列表的視圖的創建,加載靜態或者動態的內容。
Epoxy采用可組合的方式來創建列表。列表中的每個item由一個model代表,model定義了item的布局,id以及span。model還負責處理數據到視圖的綁定,在視圖被回收的時候釋放資源。如果要顯示這些model則把它們添加到Epoxy的adapter中,adapter為你處理復雜的顯示問題。
使用Epoxy顯示搜索結果
讓我們看一個如何運作的實際例子。這里是Airbnb應用中顯示一個城市街區搜索結果的視圖。
把這個視圖拆分我們得到:
-
描述城市的header
-
一個指向城市旅行指南的連接
-
數目不確定的街區視圖流
-
嵌入街區視圖流的過濾提示
除此之外有時還有一些其它的視圖,比如:
-
分頁的時候結尾部分的加載提示
-
網絡出問題時的錯誤信息
-
當滾動完所有結果之后的文字
-
在某些國家顯示的定價聲明
總共有8個不同的view類型,我們需要使用一個RecyclerView把它們聯系起來這樣整個頁面就可以滾動,呈現在一個連貫的界面中。
設置一個擁有這么多view類型的RecyclerView adapter通常會非常混亂。我們將會得到一個需要指定view type id, item counts, span counts, view holders, click listeners等設置的復雜類。
但是使用Epoxy的組合方法,我們的adapter可以專注于指定顯示哪些item,而顯示的細節則代理給了model。
代碼大致是這樣的:
public class SearchAdapter extends EpoxyAdapter {
public void bindSearchData(SearchData data) {
header.setCity(data.city);
guidebookRow.showIf(data.hasGuideBook());
for (Neighborhood neighborhood : data.neighborhoods) {
addModel(new NeighborhoodCarouselModel(neighborhood));
}
loader.showIf(data.hasMoreToLoad());
notifyModelsChanged();
}
}
我們的bindSearchData()方法接收一個包含了顯示這個view所需的所有信息的對象。當有東西變化的時候將被調用,它將重建 model state 以反應新的搜索數據。最后一行告訴Epoxy計算新model與舊model之間的差別,如有變化,則把確切的變化通知給RecyclerView。
這類似于javascript中 React 處理用戶界面的方式。代碼只需描述該顯示什么,adapter處理好如何顯示的細節。我們無需明確的定義item ids, counts, 或者 view holders這些信息。而且我們也無需負責通知發生了什么變化。
這使得它在activity從不同的數據源(比如數據庫,緩存,網絡請求)加載數據的時候可以是一個不錯的架構。它把這個狀態存儲在一個對象中,傳遞給adapter,adapter構建model從而反映出當前的狀態。每當狀態對象發生變化,不管是因為用戶輸入還是新加載的的數據導致的變化,新的狀態對象被傳遞給adapter,model被再次更新。可以在model上設置點擊事件的listeners,當有什么發生變換的時候通過回調傳遞給activity。
這種方式明確的分離了職責。隨著設計的改變或者新功能的添加,Models可以輕易的替換。組合的方式以及adapter所提供的抽象概念使得復雜度維持在低等水平。
adapter頻繁的變化item通常會影響性能。但是,Epoxy增加了diffing算法來檢測models中的變化,只更新實際發生了改變的視圖。
追蹤Adapter Item的變化
普通adapter的另一個難點是追蹤item的變化。Item可能被添加,刪除,更新或者移動,這些變化必須通知adapter。如果處理得當,這些notify調用可以讓RecyclerView只重繪發生了變化的view。但是在一個已經很復雜的adapter中手動處理這個事情還是很困難的。
Epoxy通過在models中使用一種diffing算法幫你解決了這個問題。只要你改變了model的設置,Epoxy就會找到變化然后通知RecyclerView。這簡化了你的adapter,提高了性能,還順便提供了item change動畫。
這個diffing算法依賴于每個model實現了hashCode,這樣當一個model發生變化的時候就可以被檢測到。Epoxy提供了一個注解處理器,這樣你的model就可以為那些能代表model狀態的成員添加注解。一個生成的subclass可以為你實現正確的hashCode方法,同時為每個field生成getter 和 setter 方法。
繼續我們上面的例子,header model大致是這樣的:
public class HeaderModel extends EpoxyModel<HeaderView> {
@EpoxyAttribute City city;
@Override
public void bind(HeaderView headerView){
headerView.setImage(city.getImage());
headerView.setTitle(city.getName());
headerView.setDescription(city.getDescription());
}
@LayoutRes
public int getDefaultLayout() {
return R.layout.model_header_view;
}
}
這將生成一個帶有`setCity`方法的HeaderModel_類,我們使用這個類(注意不是HeaderModel)的實例來為models列表添加header。這個header視圖只有在City對象改變的情況下才會更新。前提是假設City對象也實現了一個正確的hashCode方法來定義自己的狀態。
你還會注意到這個model實現了getDefaultLayout() 來返回一個布局資源。這個資源用于inflate傳遞給modelbind方法的view,bind方法中把數據設置到view上。另外,在adapter中layout(資源id)還被用作這個item的view type id。
Stable IDs By Default
為了讓功能正常工作,Epoxy默認啟用了RecyclerView的stable id(要了解什么是stable id,參見RecyclerView Adapter的setHasStableIds(boolean hasStableIds)方法)。
這使得diffing,item動畫以及狀態保存成為可能,每個model負責定義它的id,我們為動態生成的model手動設置id。比如每個neighborhood carousel model用網絡請求中的neighborhood對象提供的id設置。
靜態視圖比如header就要復雜點。它沒有一個固有的id與之關聯,因此我們需要制作一個。Epoxy為每個新創建的model自動生成一個id。這個id可以保證在app生命周期中不會和其他生成的model id重復,而負id被用來避免和手動設置的id重復。
保存View的狀態
Epoxy還添加了對保存視圖狀態的支持,這是默認的RecyclerView所缺乏的。比如,上面search設計中的carousels是可以橫向滑動的,為了更好的用戶體驗我們想保存這個carousel的滾動位置。如果用戶向下滾動之后再回到這里時他們應該看到carousel保持了原來的狀態。類似的,如果他們旋轉手機或者切換app之后再回來,盡管activity發生了重建,我們還是應該呈現出相同的狀態。
如果使用普通的RecyclerView adapter這就難以實現了。Epoxy支持保存任意model的view狀態,為了做到這點,它是用了 stable ids把view的狀態和model id聯系起來。
要保存view的狀態只需再model中添加如下代碼:
@Override
public boolean shouldSaveViewState {
return true;
}
Epoxy將在它離開屏幕的時候保存自己的狀態,并在返回的時候恢復。默認這個設置為false,這樣內存和滾動的性能就不會因為保存了不必要的視圖狀態而受影響。
Epoxy在靜態內容中的應用
RecyclerView通常用于顯示從遠程數據(比如網絡或者數據庫)加載的動態內容,否則使用scrollview要簡單些。但是Epoxy可以讓RecyclerView的使用和ScrollView一樣簡單,我們的詳情界面就是這樣做的。
這種效果使用ScrollView來實現可能是最簡單的。但是我們使用Epoxy配合RecyclerView可以得到更快的加載速度,也更容易實現動畫。
性能對我們來說至關重要,這個頁面通常在用戶搜索的時候展示,用戶點擊一個搜索結果的圖片,然后使用共享元素動畫切換到詳情頁面,為了讓搜索體驗良好,動畫必須流暢,因此details view的加載必須非常快。
讓我們仔細看看這個view了解為什么它們會影響性能。首先,最頂上的圖片實際是一個橫向的RecyclerView,這樣用戶就能滑動查看房間的圖片。在中間我們有一張靜態的地圖顯示房源的位置,而在底部我們還有另一個RecyclerView,顯示該地區的類似房源。而在這三個比較大的視圖中間還穿插著一些文字信息和小圖片。
這些加在一起就得到了一個帶有很多位圖的非常復雜的結構。這使得測量和布局的過程要花更長的時間,同時還需要更多的內存來加載圖片。
另外,我們還從不同的渠道加載數據-databases, in-memory caches, 以及多個網絡請求。這對為用戶顯示即時數據有好處,但是如果處理不好也會增加更多的時間開銷。
龐大的視圖結構,多個bitmap,多個view刷新,這些要求使得我們有足夠的理由去關注性能問題。多虧了Epoxy我們可以在兼顧這些考慮的情況下也能提供很棒的用戶體驗。這是因為:
-
因為我們使用的是RecyclerView,當用戶首次打開這個屏幕的時候只有一小部分視圖被加載。避免了過早的加載map圖片,底部的畫廊以及它們之間的所有視圖。這就使得布局更快,內存使用更小,過度動畫更流暢。
-
當新數據被加載的時候我們無需反復的刷新view,減小了丟幀的概率。如果遇到類似的列表請求,而那個carousel不在屏幕上,我們什么夜不用做。如果價格發生了變換,Epoxy只是更新價格標簽。這增加了進入動畫的流暢度,同時防止用戶滾動的時候丟幀。
-
自帶Item change動畫。當數據發生變化的時候我們可以以相應的動畫顯示,隱藏或者更新view。比如,點擊翻譯按鈕可以插入一個加載器,當加載完成再過渡到翻譯后的text,這避免了突兀的變化。
Epoxy的未來
我們非常高興將Epoxy作為 開源庫 分享出來,歡迎感興趣的開發者貢獻代碼幫助我們改進。我們積極的開發Epoxy以改進注解處理器,diffing算法以及通用工具。希望其它開發者能找到這個庫的新用法,幫助我們把它做成一個更好的工具。
可以在 airbnb.io 上查看我們所有的開源項目。推ter: @AirbnbEng + @AirbnbData 。
來自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2016/1209/6838.html