翻譯官方文檔Guide to App Architecture

官方文檔鏈接:https://developer.android.google.cn/topic/libraries/architecture/guide.html

1.前言


由于用戶(hù)在使用某一功能時(shí)會(huì)涉及不同的應(yīng)用程序苗胀,需要不斷地切換流程和任務(wù)衙猪。舉個(gè)例子,在使用社交軟件分享照片時(shí)匈棘,會(huì)需要打開(kāi)攝像頭精置,也會(huì)需要從圖庫(kù)中選擇文件谭胚,而將數(shù)據(jù)返回社交軟件的過(guò)程中常拓,有可能會(huì)有電話打來(lái)掐禁。這一下子啟動(dòng)了許多應(yīng)用程序怜械,但是移動(dòng)設(shè)備資源有限,操作系統(tǒng)隨時(shí)會(huì)殺死一些應(yīng)用程序來(lái)騰出空間給新的應(yīng)用傅事。所以應(yīng)用程序組件的存在不由開(kāi)發(fā)者控制缕允,不應(yīng)該存儲(chǔ)數(shù)據(jù)或狀態(tài),更不應(yīng)該彼此依賴(lài)蹭越。

之前通過(guò)生命周期進(jìn)行相關(guān)操作障本,但這引入另一套邏輯,增加代碼的復(fù)雜度响鹃。

2.常見(jiàn)架構(gòu)原則


2.1.View層分離

使Activity和Fragment中的代碼盡可能的少驾霜,只處理與界面或與操作系統(tǒng)的交互。因?yàn)檫@些類(lèi)是操作系統(tǒng)和應(yīng)用程序的中間件买置,不由開(kāi)發(fā)者控制粪糙,所以應(yīng)該最小化依賴(lài)。

2.2.Model層驅(qū)動(dòng)

通過(guò)持久化模型驅(qū)動(dòng)界面忿项,具有兩點(diǎn)好處:

  • 當(dāng)操作系統(tǒng)釋放組件資源時(shí)蓉冈,用戶(hù)數(shù)據(jù)也不會(huì)丟失城舞;
  • 當(dāng)網(wǎng)絡(luò)連接狀態(tài)不好時(shí),應(yīng)用程序也能正常工作寞酿。

持久化模型獨(dú)立于組件家夺,不受系統(tǒng)的控制》サ基于它的應(yīng)用程序可以保證界面代碼簡(jiǎn)單秦踪,同時(shí)業(yè)務(wù)邏輯具有可測(cè)試性。

3.推薦的應(yīng)用框架


沒(méi)有一種架構(gòu)適用于所有場(chǎng)景掸茅。意味著椅邓,這是一個(gè)好的起點(diǎn),但若已經(jīng)有更適合的了昧狮,是不需要改變的景馁。以網(wǎng)絡(luò)獲取并顯示用戶(hù)配置信息為例子,展示架構(gòu)組件的使用方式逗鸣。

3.1.建立用戶(hù)界面

界面包含一個(gè)UserProfileFragment.java的組件類(lèi)和user_profile_layout.xml的布局文件合住。為了驅(qū)動(dòng)界面,基于ViewModel類(lèi)創(chuàng)建UserProfileViewModel.java的數(shù)據(jù)模型來(lái)保存信息撒璧。數(shù)據(jù)元素主要包括:

  • The User ID:用戶(hù)標(biāo)識(shí)符透葛。最好通過(guò)Fragment Arguments傳入Fragment,那樣操作系統(tǒng)銷(xiāo)毀應(yīng)用進(jìn)程時(shí)卿樱,id將被保存以便重啟時(shí)使用僚害。
  • The User object:一個(gè)POJO類(lèi)保存用戶(hù)數(shù)據(jù)。

一個(gè)ViewModel給一個(gè)特定的界面組件(Activity或Fragment)提供數(shù)據(jù)繁调,并處理業(yè)務(wù)邏輯中數(shù)據(jù)相關(guān)的操作萨蚕,如調(diào)用其它組件來(lái)加載數(shù)據(jù)或轉(zhuǎn)發(fā)用戶(hù)的修改。ViewModel與View隔離蹄胰,不受屏幕旋轉(zhuǎn)導(dǎo)致重建等Configuration Changes的影響岳遥。而Activity或Fragment則是UI Controller,與用戶(hù)行為和ViewModel進(jìn)行交互裕寨。

public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}
public class UserProfileFragment extends LifecycleFragment {
    private static final String UID_KEY = "uid";
    private UserProfileViewModel viewModel;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        String userId = getArguments().getString(UID_KEY);
        viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
        viewModel.init(userId);
    }

    @Override
    public View onCreateView(LayoutInflater inflater,
                @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.user_profile, container, false);
    }
}

目前使用LifecycleFragment代替Fragment浩蓉,等到lifecycles版本穩(wěn)定后,支持庫(kù)中的Fragment將實(shí)現(xiàn)LifecycleOwner接口宾袜。

ViewModel與Presenter最大的不同是捻艳,Presenter內(nèi)部封裝著一系列的行為,而ViewModel持有的是數(shù)據(jù)(狀態(tài))试和,那么數(shù)據(jù)如何傳遞呢讯泣?這里就得提LiveData了。

LiveData是一個(gè)可被觀察的數(shù)據(jù)持有者阅悍。它讓?xiě)?yīng)用中的組件觀察自己的變化好渠,卻不需要顯式的和剛性的依賴(lài)昨稼。LiveData同時(shí)會(huì)監(jiān)聽(tīng)?wèi)?yīng)用組件(Activity,F(xiàn)ragment拳锚,Services)的生命周期狀態(tài)假栓,并且做正確的事情來(lái)防止對(duì)象的內(nèi)存泄露。

如果已經(jīng)使用RxJava或Agera庫(kù)霍掺,你可以繼續(xù)使用來(lái)替代LiveData匾荆。不過(guò)當(dāng)你使用它們時(shí),確保正確處理生命周期杆烁,例如:LifecycleOwner調(diào)用了onStop()方法牙丽,需暫停相關(guān)的數(shù)據(jù)流;LifecycleOwner調(diào)用了onDestory()方法兔魂,需銷(xiāo)毀相關(guān)的數(shù)據(jù)流烤芦。你也可以添加android.arch.lifecycle:reactivestreams工件來(lái)讓LiveData和其它的響應(yīng)流庫(kù)(RxJava2)一起使用。

現(xiàn)在析校,用LiveData<User>來(lái)替代User构罗,這樣當(dāng)數(shù)據(jù)改變時(shí),F(xiàn)ragment將會(huì)收到通知智玻。更好的是遂唧,LiveData是支持生命周期的,當(dāng)自己不再被使用時(shí)吊奢,自動(dòng)清理引用盖彭。

public class UserProfileViewModel extends ViewModel {
    ...
    private LiveData<User> user;
    public LiveData<User> getUser() {
        return user;
    }
}
// UserProfileFragment.java
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    ...
    viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
    ...
    viewModel.getUser().observe(this, user -> {
        // update UI
    });
 }

與其它使用觀察回調(diào)的庫(kù)不同。當(dāng)Fragment不處于活躍狀態(tài)時(shí)事甜,是不會(huì)回調(diào)的谬泌,所以不需要手動(dòng)在Fragment的onStop()方法中停止觀察數(shù)據(jù);當(dāng)Fragment銷(xiāo)毀了逻谦,LiveData將會(huì)移除觀察者,釋放資源陪蜻。

不需要做額外的事邦马,ViewModel在配置改變時(shí),將自動(dòng)恢復(fù)相同的ViewModel實(shí)例及對(duì)當(dāng)前數(shù)據(jù)的回調(diào)宴卖。

3.2.獲取數(shù)據(jù)

將ViewModel和Fragment連接后滋将,還需要ViewModel獲取到用戶(hù)數(shù)據(jù)。這里症昏,假設(shè)使用Retrofit庫(kù)來(lái)訪問(wèn)后端接口随闽。

public interface Webservice {
    /**
     * @GET declares an HTTP GET request
     * @Path("user") annotation on the userId parameter marks it as a
     * replacement for the {user} placeholder in the @GET path
     */
    @GET("/users/{user}")
    Call<User> getUser(@Path("user") String userId);
}

雖然可以在ViewModel中直接獲取數(shù)據(jù)并復(fù)制給User對(duì)象,但隨著項(xiàng)目的變大肝谭,將越來(lái)越難以維護(hù)掘宪,而且給ViewModel太多的職責(zé)蛾扇,與單一職責(zé)的原則相違背。此外魏滚,ViewModel的活動(dòng)范圍與Activity或Fragment的生命周期相關(guān)镀首,即生命周期結(jié)束后將丟失所有數(shù)據(jù)。為此鼠次,引入了Repository這個(gè)概念更哄。

Repository是專(zhuān)門(mén)用來(lái)處理數(shù)據(jù)操作的。知道從什么地方獲取數(shù)據(jù)和調(diào)用什么接口更新數(shù)據(jù)腥寇,是不同數(shù)據(jù)源之間的中轉(zhuǎn)成翩,例如持久化模型、Web服務(wù)赦役、緩存等捕传。

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // This is not an optimal implementation, we'll fix it below
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                // error case is left out for brevity
                data.setValue(response.body());
            }
        });
        return data;
    }
}

這樣將數(shù)據(jù)源從應(yīng)用其它部分獨(dú)立出來(lái),ViewModel不知道與什么數(shù)據(jù)源交互扩劝,便于替換庸论。但是UserRepository需要Webservice實(shí)例,若通過(guò)構(gòu)造方法提供棒呛,每個(gè)使用Webservice的類(lèi)都得知道它的構(gòu)造方法聂示,使依賴(lài)變得復(fù)雜,同時(shí)都創(chuàng)建對(duì)象將占用大量資源簇秒。這里提供兩種方法參考:

  • 依賴(lài)注入:允許類(lèi)定義它們之間的依賴(lài)而不需要立馬構(gòu)建出來(lái)鱼喉,等到運(yùn)行時(shí),由其它的類(lèi)提供這些依賴(lài)趋观。
  • 服務(wù)定位:提供一個(gè)倉(cāng)庫(kù)扛禽,類(lèi)可以從中獲取它們所需的依賴(lài)而不是創(chuàng)建它們。相較于依賴(lài)注入皱坛,它更容易實(shí)現(xiàn)编曼。

這些模式可以方便地?cái)U(kuò)展代碼,因?yàn)樘峁┝饲逦姆绞焦芾硪蕾?lài)剩辟,而不需要增加其它代碼掐场。更重要的是它們都很容易測(cè)試

3.3.ViewModel和Repository
public class UserProfileViewModel extends ViewModel {
    private LiveData<User> user;
    private UserRepository userRepo;

    @Inject // UserRepository parameter is provided by Dagger 2
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(String userId) {
        if (this.user != null) {
            // ViewModel is created per Fragment so
            // we know the userId won't change
            return;
        }
        user = userRepo.getUser(userId);
    }

    public LiveData<User> getUser() {
        return this.user;
    }
}
3.4.緩存數(shù)據(jù)

若Repository只實(shí)現(xiàn)一個(gè)數(shù)據(jù)源贩猎,則會(huì)顯得不太實(shí)用亿乳。需增加數(shù)據(jù)的持有坞嘀,當(dāng)用戶(hù)再次進(jìn)入界面時(shí)不用重新加載雨让,不然會(huì)浪費(fèi)寶貴的網(wǎng)絡(luò)帶寬和迫使用戶(hù)等待新的請(qǐng)求尸变。為此,給UserRepository添加新的數(shù)據(jù)源艇棕,在內(nèi)存中緩存User對(duì)象蝌戒。

@Singleton  // informs Dagger that this class should be constructed once
public class UserRepository {
    private Webservice webservice;
    // simple in memory cache, details omitted for brevity
    private UserCache userCache;
    public LiveData<User> getUser(String userId) {
        LiveData<User> cached = userCache.get(userId);
        if (cached != null) {
            return cached;
        }

        final MutableLiveData<User> data = new MutableLiveData<>();
        userCache.put(userId, data);
        // this is still suboptimal but better than before.
        // a complete implementation must also handle the error cases.
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }
        });
        return data;
    }
}
3.5.持久化數(shù)據(jù)

內(nèi)存緩存只能針對(duì)屏幕切換等當(dāng)前應(yīng)用進(jìn)程存在的情況串塑,但若進(jìn)程被系統(tǒng)殺死,則仍需重新網(wǎng)絡(luò)請(qǐng)求瓶颠。為了不使用移動(dòng)網(wǎng)絡(luò)重新獲取相同的數(shù)據(jù)拟赊,可以通過(guò)緩存Web請(qǐng)求來(lái)解決〈饬埽可當(dāng)場(chǎng)景是通過(guò)兩個(gè)不同類(lèi)型的請(qǐng)求來(lái)獲取相同類(lèi)型的數(shù)據(jù)時(shí)吸祟,會(huì)出現(xiàn)顯示不一致的問(wèn)題,導(dǎo)致需手動(dòng)合并它們桃移。正確的做法是數(shù)據(jù)持久化屋匕,在數(shù)據(jù)庫(kù)中完成合并操作。

Room是一個(gè)對(duì)象映射庫(kù)借杰,通過(guò)最少的樣板代碼實(shí)現(xiàn)本地?cái)?shù)據(jù)持久化过吻。在編譯時(shí)會(huì)驗(yàn)證每條查詢(xún)的樣式,將中斷SQL查詢(xún)的錯(cuò)誤反映在編譯期而不是運(yùn)行時(shí)蔗衡。Room封裝了一些與原始SQL表和查詢(xún)相關(guān)的底層實(shí)現(xiàn)細(xì)節(jié)纤虽,也允許觀察數(shù)據(jù)庫(kù)中數(shù)據(jù)(包括集合和連接查詢(xún))的變化,并通過(guò)LiveData對(duì)象來(lái)反映绞惦。此外逼纸,它還顯式地定義了線程約束來(lái)規(guī)避常見(jiàn)問(wèn)題,比如主線程上訪問(wèn)存儲(chǔ)济蝉。

如果熟悉其它持久化解決方案像SQLite ORM或不同的數(shù)據(jù)庫(kù)Realm杰刽,就不需要更換,除非Room的功能與用例很契合王滤。

// 創(chuàng)建表相關(guān)的樣式
@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;
  // getters and setters for fields
}
// 創(chuàng)建數(shù)據(jù)訪問(wèn)對(duì)象
@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);
    @Query("SELECT * FROM user WHERE id = :userId")
    LiveData<User> load(String userId);
}
// 創(chuàng)建數(shù)據(jù)庫(kù)抽象類(lèi)贺嫂,編譯自動(dòng)實(shí)現(xiàn)
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

load方法能直接返回LiveData<User>,是很便利的雁乡。當(dāng)Room發(fā)現(xiàn)數(shù)據(jù)庫(kù)數(shù)據(jù)被改變了第喳,且至少有一個(gè)活躍的相關(guān)的觀察者,將自動(dòng)通知它們更新相關(guān)操作蔗怠。但是alpha1版本中墩弯,Room檢查變化是基于表的修改,有可能發(fā)些無(wú)效的修改通知寞射。

@Singleton
public class UserRepository {
    private final Webservice webservice;
    private final UserDao userDao;
    private final Executor executor;

    @Inject
    public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
        this.webservice = webservice;
        this.userDao = userDao;
        this.executor = executor;
    }

    public LiveData<User> getUser(String userId) {
        refreshUser(userId);
        // return a LiveData directly from the database.
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        executor.execute(() -> {
            // running in a background thread
            // check if user was fetched recently
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // refresh the data
                Response response = webservice.getUser(userId).execute();
                // TODO check for error etc.
                // Update the database.The LiveData will automatically refresh so
                // we don't need to do anything else here besides updating the database
                userDao.save(response.body());
            }
        });
    }
}

對(duì)UserRepository的改變不會(huì)影響UserProfileViewModel或者UserProfileFragment,這有利于測(cè)試锌钮,例如桥温,提供偽造的UserRepository來(lái)測(cè)試UserProfileViewModel。

有些情況下梁丘,例如下拉刷新侵浸,通過(guò)界面向用戶(hù)展示當(dāng)前進(jìn)行中的網(wǎng)絡(luò)操作是很重要的旺韭。好的做法是將界面操作與實(shí)際數(shù)據(jù)分離,因?yàn)閿?shù)據(jù)容易被各種原因更新(例如掏觉,獲取一個(gè)朋友列表区端,可能會(huì)再次獲取到相同的user并觸發(fā)LiveData更新)。從界面的角度來(lái)看澳腹,動(dòng)態(tài)請(qǐng)求實(shí)際上只是另一個(gè)數(shù)據(jù)點(diǎn)织盼,類(lèi)似其它任何數(shù)據(jù)片段(如,User對(duì)象)酱塔。這里有兩種常用解決方法:

  • getUser 方法返回的LiveData添加網(wǎng)絡(luò)操作的狀態(tài)沥邻。
  • 在Repository中提供另一個(gè)公開(kāi)的方法以返回刷新的狀態(tài)。尤其適合專(zhuān)門(mén)響應(yīng)用戶(hù)操作(下拉刷新)羊娃,在界面中展示請(qǐng)求的網(wǎng)絡(luò)狀態(tài)唐全。

需注意單一數(shù)據(jù)源原則。不同的后端接口返回相同的數(shù)據(jù)(粒度不同)是一種常見(jiàn)情況蕊玷,但在請(qǐng)求的間隙邮利,服務(wù)端的數(shù)據(jù)可能發(fā)生改變。若Repository直接返回網(wǎng)絡(luò)請(qǐng)求垃帅,則導(dǎo)致界面顯示沖突延届。這就是為什么在上面UserRepository實(shí)現(xiàn)時(shí),網(wǎng)絡(luò)請(qǐng)求的數(shù)據(jù)僅僅存到數(shù)據(jù)庫(kù)中挺智,觸發(fā)LiveData的刷新祷愉。推薦,DataBase是應(yīng)用唯一的數(shù)據(jù)源赦颇,而Repository是應(yīng)用其它部分的唯一數(shù)據(jù)源二鳄。

3.6.測(cè)試

很容易將代碼分成幾個(gè)模塊進(jìn)行測(cè)試。

  • 用戶(hù)界面和交互:這是唯一使用設(shè)備界面測(cè)試的地方媒怯。最好使用Espresso測(cè)試界面代碼订讼,因?yàn)镕ragment只與ViewModel交互,提供一個(gè)mock的ViewModel足夠完整地測(cè)試這個(gè)界面扇苞。
  • ViewModel:通過(guò)JUnit即可測(cè)試欺殿,僅mock所需的UserRepository。
  • UserRepository:也可以通過(guò)JUnit測(cè)試鳖敷,僅mock所需的Webservice和UserDao脖苏。主要測(cè)試是否正常調(diào)用網(wǎng)絡(luò)服務(wù)和存儲(chǔ)結(jié)果到數(shù)據(jù)庫(kù),以及數(shù)據(jù)被緩存或更新后沒(méi)有多余的請(qǐng)求定踱。由于兩者都是接口棍潘,除了mock它們,還能為更復(fù)雜的測(cè)試用例模擬實(shí)現(xiàn)它們。
  • UserDao:推薦僅使用設(shè)備測(cè)試亦歉。因?yàn)闇y(cè)試過(guò)程不涉及界面恤浪,仍能保持很快的運(yùn)行速度‰瓤可以創(chuàng)建內(nèi)存數(shù)據(jù)庫(kù)水由,以確保測(cè)試沒(méi)有任何副作用(如改變磁盤(pán)上的數(shù)據(jù)庫(kù)文件)。同時(shí)赛蔫,Room也允許指定數(shù)據(jù)庫(kù)砂客,通過(guò)提供SupportSQLiteOpenHelper的實(shí)現(xiàn)進(jìn)行單元測(cè)試。但通常不推薦濒募,因?yàn)槭謾C(jī)和電腦上的SQLite版本不一樣鞭盟。
  • Webservice:測(cè)試應(yīng)該不依賴(lài)于外部,所以測(cè)試Webservice時(shí)不能通過(guò)網(wǎng)絡(luò)訪問(wèn)后臺(tái)瑰剃。有許多庫(kù)可以做到這點(diǎn)齿诉,例如,MockWebServer庫(kù)能為你的測(cè)試創(chuàng)建本地網(wǎng)絡(luò)服務(wù)晌姚。
  • 測(cè)試工件:架構(gòu)組件提供一個(gè)android.arch.core:core-testing工件來(lái)控制后臺(tái)線程粤剧,包含兩個(gè)JUint規(guī)則:
    • InstantTaskExecutorRule:此規(guī)則可強(qiáng)制架構(gòu)組件在調(diào)用的線程上立即執(zhí)行任何后臺(tái)操作。
    • CountingTaskExecutorRule:此規(guī)則能被用于設(shè)備測(cè)試中等待架構(gòu)組件的后臺(tái)操作或者連接到Espresso作為閑置資源挥唠。
3.7.最終架構(gòu)
AAC.png

4.指導(dǎo)原則


  • 在Manifest中定義的入口點(diǎn)抵恋,如:acitivy,fragment宝磨,broadcast receiver等弧关,不是數(shù)據(jù)源。相反唤锉,它們應(yīng)該只是協(xié)調(diào)與該入口點(diǎn)相關(guān)的數(shù)據(jù)子集世囊。由于每個(gè)應(yīng)用程序組件的存活時(shí)間很短,這取決于用戶(hù)與其設(shè)備的交互以及運(yùn)行時(shí)的總體狀況窿祥,所以任何入口點(diǎn)都不應(yīng)該成為數(shù)據(jù)源株憾。
  • 嚴(yán)格的在應(yīng)用程序的各個(gè)模塊之間創(chuàng)建明確的責(zé)任界限。例如:不要讓網(wǎng)絡(luò)加載數(shù)據(jù)的代碼被多個(gè)類(lèi)或包使用晒衩。同樣嗤瞎,不要將不相關(guān)的模塊(如:數(shù)據(jù)緩存和數(shù)據(jù)綁定)放到同一個(gè)類(lèi)中。
  • 每個(gè)模塊盡可能少地暴露內(nèi)部听系。不要嘗試暴露模塊內(nèi)部哪怕一個(gè)地方的實(shí)現(xiàn)細(xì)節(jié)贝奇。你可能會(huì)在短期內(nèi)節(jié)省一些時(shí)間,但是隨著項(xiàng)目的發(fā)展靠胜,你將會(huì)花更多的時(shí)間來(lái)償還弃秆。
  • 當(dāng)定義模塊間的交互時(shí)届惋,考慮如何讓每個(gè)模塊能獨(dú)立的測(cè)試髓帽。例如菠赚,擁有一個(gè)定義良好的、從網(wǎng)絡(luò)獲取數(shù)據(jù)的接口模塊郑藏,將會(huì)使持久化數(shù)據(jù)到本地?cái)?shù)據(jù)庫(kù)的行為更易于測(cè)試衡查。相反,如果將兩個(gè)模塊的邏輯放在一個(gè)地方必盖,或者將網(wǎng)絡(luò)相關(guān)的代碼擴(kuò)散到整個(gè)項(xiàng)目拌牲,測(cè)試將會(huì)變的非常困難(并非不可能)。
  • 應(yīng)用程序的核心才是重點(diǎn)歌粥。不要花費(fèi)時(shí)間重復(fù)造輪子或一次又一次的編寫(xiě)相同的樣板代碼塌忽。相反,將精力集中在應(yīng)用程序的核心上失驶,讓AAC和其它優(yōu)秀的庫(kù)來(lái)處理重復(fù)的樣板代碼土居。
  • 持久化盡可能多的相關(guān)最新數(shù)據(jù),以便應(yīng)用程序在設(shè)備處于離線模式時(shí)還可以使用嬉探。即使你可以享用穩(wěn)定高速的網(wǎng)絡(luò)連接擦耀,但是你的用戶(hù)可能無(wú)法享用。
  • Repository應(yīng)該指定單一的數(shù)據(jù)源涩堤。每當(dāng)應(yīng)用程序需要訪問(wèn)數(shù)據(jù)時(shí)眷蜓,數(shù)據(jù)應(yīng)該始終來(lái)源于一個(gè)地方。

5.暴露網(wǎng)絡(luò)狀態(tài)


對(duì)于上面提到的網(wǎng)絡(luò)狀態(tài)問(wèn)題胎围,可以使用Resource類(lèi)封裝數(shù)據(jù)和狀態(tài)吁系。

//a generic class that describes a data with a status
public class Resource<T> {
    @NonNull public final Status status;
    @Nullable public final T data;
    @Nullable public final String message;
    private Resource(@NonNull Status status, @Nullable T data, @Nullable String message) {
        this.status = status;
        this.data = data;
        this.message = message;
    }

    public static <T> Resource<T> success(@NonNull T data) {
        return new Resource<>(SUCCESS, data, null);
    }

    public static <T> Resource<T> error(String msg, @Nullable T data) {
        return new Resource<>(ERROR, data, msg);
    }

    public static <T> Resource<T> loading(@Nullable T data) {
        return new Resource<>(LOADING, data, null);
    }
}

從網(wǎng)絡(luò)獲取數(shù)據(jù)再將磁盤(pán)存儲(chǔ)的數(shù)據(jù)展示出來(lái),是一種常見(jiàn)的現(xiàn)象白魂,對(duì)于重復(fù)的邏輯(見(jiàn)下圖)可以提取出NetworkBoundResource類(lèi)來(lái)復(fù)用汽纤。

Load.png

首先觀察數(shù)據(jù)庫(kù)的資源響應(yīng)數(shù)據(jù)的更新。當(dāng)從數(shù)據(jù)庫(kù)中獲取時(shí)碧聪,先判斷得到的結(jié)果是否符合使用要求冒版,不然就從網(wǎng)絡(luò)獲取。若想從網(wǎng)絡(luò)更新時(shí)顯示存儲(chǔ)數(shù)據(jù)逞姿,它們可以同時(shí)進(jìn)行辞嗡。若網(wǎng)絡(luò)加載成功,將結(jié)果存到數(shù)據(jù)庫(kù)中滞造,再次觸發(fā)加載流程续室;若加載失敗,直接調(diào)用失敗操作谒养。

新的數(shù)據(jù)存到磁盤(pán)上后挺狰,需從數(shù)據(jù)庫(kù)重新取數(shù)據(jù)明郭,若數(shù)據(jù)庫(kù)可以發(fā)送改變消息,通常不用這么做丰泊。但是薯定,依賴(lài)數(shù)據(jù)庫(kù)發(fā)送改變消息,也會(huì)有些問(wèn)題瞳购,因?yàn)閿?shù)據(jù)更新后可能并沒(méi)有變化话侄,數(shù)據(jù)庫(kù)將不會(huì)發(fā)送改變信息。同樣学赛,也不希望直接使用網(wǎng)絡(luò)請(qǐng)求到的結(jié)果年堆,因?yàn)檫@不符合單一數(shù)據(jù)源原則,哪怕也會(huì)觸發(fā)數(shù)據(jù)庫(kù)存儲(chǔ)更新盏浇。最后变丧,不希望沒(méi)新數(shù)據(jù)更新的情況下發(fā)送SUCCESS標(biāo)志,這會(huì)給客戶(hù)端錯(cuò)誤的信息绢掰。

// ResultType: Type for the Resource data
// RequestType: Type for the API response
public abstract class NetworkBoundResource<ResultType, RequestType> {
    // Called to save the result of the API response into the database
    @WorkerThread
    protected abstract void saveCallResult(@NonNull RequestType item);

    // Called with the data in the database to decide whether it should be
    // fetched from the network.
    @MainThread
    protected abstract boolean shouldFetch(@Nullable ResultType data);

    // Called to get the cached data from the database
    @NonNull @MainThread
    protected abstract LiveData<ResultType> loadFromDb();

    // Called to create the API call.
    @NonNull @MainThread
    protected abstract LiveData<ApiResponse<RequestType>> createCall();

    // Called when the fetch fails. The child class may want to reset components
    // like rate limiter.
    @MainThread
    protected void onFetchFailed() {
    }

    // returns a LiveData that represents the resource
    public final LiveData<Resource<ResultType>> getAsLiveData() {
        return result;
    }
}

上面的類(lèi)定義兩種類(lèi)型參數(shù)(ResultType痒蓬,RequestType),是考慮到網(wǎng)絡(luò)獲取的數(shù)據(jù)類(lèi)型與本地使用的數(shù)據(jù)類(lèi)型不匹配曼月。而使用ApiResponse作為網(wǎng)絡(luò)請(qǐng)求谊却,是因?yàn)樗鼘?duì)Retrofit2.Call進(jìn)行簡(jiǎn)單的封裝,將返回類(lèi)型轉(zhuǎn)成LiveData哑芹。

對(duì)于上面提出的要求炎辨,可以通過(guò)以下具體實(shí)現(xiàn):

public abstract class NetworkBoundResource<ResultType, RequestType> {
    private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();

    @MainThread
    NetworkBoundResource() {
        result.setValue(Resource.loading(null));
        LiveData<ResultType> dbSource = loadFromDb();
        result.addSource(dbSource, data -> {
            result.removeSource(dbSource);
            if (shouldFetch(data)) {
                fetchFromNetwork(dbSource);
            } else {
                result.addSource(dbSource,
                        newData -> result.setValue(Resource.success(newData)));
            }
        });
    }

    private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
        LiveData<ApiResponse<RequestType>> apiResponse = createCall();
        // we re-attach dbSource as a new source,
        // it will dispatch its latest value quickly
        result.addSource(dbSource,
                newData -> result.setValue(Resource.loading(newData)));
        result.addSource(apiResponse, response -> {
            result.removeSource(apiResponse);
            result.removeSource(dbSource);
            //noinspection ConstantConditions
            if (response.isSuccessful()) {
                saveResultAndReInit(response);
            } else {
                onFetchFailed();
                result.addSource(dbSource,
                        newData -> result.setValue(
                                Resource.error(response.errorMessage, newData)));
            }
        });
    }

    @MainThread
    private void saveResultAndReInit(ApiResponse<RequestType> response) {
        new AsyncTask<Void, Void, Void>() {

            @Override
            protected Void doInBackground(Void... voids) {
                saveCallResult(response.body);
                return null;
            }

            @Override
            protected void onPostExecute(Void aVoid) {
                // we specially request a new live data,
                // otherwise we will get immediately last cached value,
                // which may not be updated with latest results received from network.
                result.addSource(loadFromDb(),
                        newData -> result.setValue(Resource.success(newData)));
            }
        }.execute();
    }
}

現(xiàn)在,用NetworkBoundResource替換之前UserRepository的獲取數(shù)據(jù)的操作聪姿。由于Webservice和UserDao是由項(xiàng)目決定類(lèi)型碴萧,無(wú)法提取到NetworkBoundResource中,所以相關(guān)操作放在UserRepository中實(shí)現(xiàn)末购。

class UserRepository {
    Webservice webservice;
    UserDao userDao;

    public LiveData<Resource<User>> loadUser(final String userId) {
        return new NetworkBoundResource<User,User>() {
            @Override
            protected void saveCallResult(@NonNull User item) {
                userDao.insert(item);
            }

            @Override
            protected boolean shouldFetch(@Nullable User data) {
                return rateLimiter.canFetch(userId) && (data == null || !isFresh(data));
            }

            @NonNull @Override
            protected LiveData<User> loadFromDb() {
                return userDao.load(userId);
            }

            @NonNull @Override
            protected LiveData<ApiResponse<User>> createCall() {
                return webservice.getUser(userId);
            }
        }.getAsLiveData();
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末破喻,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子盟榴,更是在濱河造成了極大的恐慌曹质,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件擎场,死亡現(xiàn)場(chǎng)離奇詭異羽德,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)迅办,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)宅静,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人站欺,你說(shuō)我怎么就攤上這事姨夹∠舜梗” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵磷账,是天一觀的道長(zhǎng)峭沦。 經(jīng)常有香客問(wèn)我,道長(zhǎng)够颠,這世上最難降的妖魔是什么熙侍? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮履磨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘庆尘。我一直安慰自己剃诅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布驶忌。 她就那樣靜靜地躺著矛辕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪付魔。 梳的紋絲不亂的頭發(fā)上聊品,一...
    開(kāi)封第一講書(shū)人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音几苍,去河邊找鬼翻屈。 笑死,一個(gè)胖子當(dāng)著我的面吹牛妻坝,可吹牛的內(nèi)容都是我干的伸眶。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼刽宪,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼厘贼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起圣拄,我...
    開(kāi)封第一講書(shū)人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤嘴秸,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后庇谆,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體岳掐,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年族铆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了岩四。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡哥攘,死狀恐怖剖煌,靈堂內(nèi)的尸體忽然破棺而出材鹦,到底是詐尸還是另有隱情,我是刑警寧澤耕姊,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布桶唐,位于F島的核電站,受9級(jí)特大地震影響茉兰,放射性物質(zhì)發(fā)生泄漏尤泽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一规脸、第九天 我趴在偏房一處隱蔽的房頂上張望坯约。 院中可真熱鬧,春花似錦莫鸭、人聲如沸闹丐。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)卿拴。三九已至,卻和暖如春梨与,著一層夾襖步出監(jiān)牢的瞬間堕花,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工粥鞋, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缘挽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓陷虎,卻偏偏與公主長(zhǎng)得像到踏,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子尚猿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355