Android官方架構(gòu)組件指南

此指南適用于那些曾經(jīng)或現(xiàn)在進行Android應用的基礎(chǔ)開發(fā)矾睦,并希望了解和學習編寫Android程序的最佳實踐和架構(gòu)。通過學習來構(gòu)建強大的生產(chǎn)級別的應用。

注意:此指南默認你對Android開發(fā)有比較深的理解固翰,熟知Android Framework。如果你還只是個Android開發(fā)新手羹呵,那么建議先學習下Android的基礎(chǔ)知識骂际。

Android程序員面臨的問題

傳統(tǒng)的桌面應用程序開發(fā)在大多數(shù)情況下,啟動器快捷方式都有一個入口點冈欢,并作為一個單一的過程運行歉铝,但Android應用程序的結(jié)構(gòu)更為復雜。典型的Android應用程序由多個應用程序組件構(gòu)成涛癌,包括Activity犯戏,F(xiàn)ragment送火,Service拳话,ContentProvider和Broadcast Receiver。

大多數(shù)這些應用程序組件在Android操作系統(tǒng)使用的AndroidManifest中聲明种吸,以決定如何將應用程序集成到設(shè)備上來為用戶提供完整的體驗弃衍。盡管如前所述瘩蚪,桌面應用程序傳統(tǒng)上是作為一個單一的進程運行的证鸥,但正確編寫的Android應用程序則需要更靈活,因為用戶通過設(shè)備上的不同應用程序編織方式揩页,不斷切換流程和任務猖败。

舉個例子速缆,當用戶在社交App上打算分享一張照片,那么Android系統(tǒng)就會為此啟動相機來完成此次請求恩闻。此時用戶離開了社交App艺糜,但是這個用戶體驗是無縫連接的。相機可能又會觸發(fā)并啟動文件管理器來選擇照片。最終回到社交App并分享照片破停。此外翅楼,在此過程中的任何時候,用戶可能會被打電話中斷真慢,并在完成電話后再回來分享照片毅臊。

在Android中,這種應用間跳轉(zhuǎn)行為很常見黑界,因此你的應用必須正確處理這些流程管嬉。請記住,移動設(shè)備是資源有限的朗鸠,所以在任何時候宠蚂,操作系統(tǒng)可能需要殺死一些應用來為新的應用騰出空間。

你的應用程序的所有組件都可以被單獨啟動或無序啟動童社,并且在任何時候由用戶或系統(tǒng)銷毀求厕。因為應用程序組件是短暫的,它們的生命周期(創(chuàng)建和銷毀時)不受你的控制扰楼,因此你不應該將任何應用程序數(shù)據(jù)或狀態(tài)存儲在應用程序組件中呀癣,并且應用程序組件不應相互依賴。

常見的架構(gòu)原理

如果你無法使用應用程序組件來存儲應用程序數(shù)據(jù)和狀態(tài)弦赖,應如何構(gòu)建應用程序项栏?

在你的App開發(fā)中你應該將重心放在分層上,如果將所有的代碼都寫在Activity或者Fragment中蹬竖,那問題就大了沼沈。任何不是處理UI或跟操作系統(tǒng)交互的操作不應該放在這兩個類中。盡量保持它們代碼的精簡币厕,這樣你可以避免很多與生命周期相關(guān)的問題列另。記住你并不能掌控Activity和Fragment,他們只是在你的App和Android系統(tǒng)間起了橋梁的作用旦装。任何時候页衙,Android系統(tǒng)可能會根據(jù)用戶操作或其他因素(如低內(nèi)存)來回收它們。最好盡量減少對他們的依賴阴绢,以提供堅實的用戶體驗店乐。

還有一點比較重要的就是持久模型驅(qū)動UI。使用持久模型主要是因為當你的UI被回收或者在沒有網(wǎng)絡(luò)的情況下還能正常給用戶展示數(shù)據(jù)呻袭。模型是用來處理應用數(shù)據(jù)的組件眨八,它們獨立于應用中的視圖和四大組件。因此模型的生命周期必然和UI是分離的左电。保持UI代碼的整潔廉侧,會讓你能更容易的管理和調(diào)整UI含长。讓你的應用基于模型開發(fā)可以很好的管理你應用的數(shù)據(jù)并是你的應用更具測試性和持續(xù)性。

應用架構(gòu)推薦

回到這篇文章的主題伏穆,來說說Android官方架構(gòu)組件(一下簡稱架構(gòu))拘泞。一下會介紹如何在你的應用中實踐這一架構(gòu)模式。

注意:不可能存在某一種架構(gòu)方式可以完美適合任何場景枕扫。話雖如此陪腌,這種架構(gòu)應該是大多數(shù)用例的良好起點。如果你已經(jīng)有了很好的Android應用程序架構(gòu)方式烟瞧,請繼續(xù)保持诗鸭。

假設(shè)我們需要一個現(xiàn)實用戶資料的UI,該用戶的資料文件將使用REST API從服務端獲取参滴。

構(gòu)建用戶界面

我們的這個用戶界面由一個UserProfileFragment.java文件和它的布局文件user_profile_layout.xml强岸。

為了驅(qū)動UI,數(shù)據(jù)模型需要持有下面兩個數(shù)據(jù):

  • User ID:用戶的標識符砾赔。最好使用Fragment的參數(shù)將此信息傳遞到Fragment中蝌箍。如果Android操作系統(tǒng)回收了Fragment,則會保留此信息暴心,以便下次重新啟動應用時妓盲,該ID可用。
  • User Object:傳統(tǒng)的Java對象专普,代表用戶的數(shù)據(jù)悯衬。

為此,我們新建一個繼承自ViewModel的名為UserProfileViewModel的模型來持有這個數(shù)據(jù)檀夹。

ViewModel提供特定UI組件的數(shù)據(jù)筋粗,例如Activity和Fragment,并處理與數(shù)據(jù)處理業(yè)務部分的通信炸渡,例如調(diào)用其他組件來加載數(shù)據(jù)或轉(zhuǎn)發(fā)用戶修改娜亿。ViewModel不了解View,并且不受UI的重建(如重由于旋轉(zhuǎn)而導致的Activity的重建)的影響偶摔。

現(xiàn)在我們有一下三個文件:

  • user_profile.xml: 視圖的布局文件暇唾。
  • UserProfileViewModel.java: 持有UI數(shù)據(jù)的模型。
  • UserProfileFragment.java: 用于顯示數(shù)據(jù)模型中的數(shù)據(jù)并和用戶進行交互辰斋。

一下是具體代碼(為了簡化,布局文件省略)瘸味。

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

注意:上面的UserProfileFragment繼承自LifeCycleFragment而不是Fragment宫仗。當Lifecycle的Api穩(wěn)定后,F(xiàn)ragment會默認實現(xiàn)LifeCycleOwner旁仿。

現(xiàn)在藕夫,我們有三個文件孽糖,我們?nèi)绾芜B接它們?畢竟毅贮,當ViewModel的用戶字段被設(shè)置時办悟,我們需要一種通知UI的方法。這里就要提到LiveData了滩褥。

LiveData是一個可觀察的數(shù)據(jù)持有者病蛉。它允許應用程序中的組件觀察LiveData對象持有的數(shù)據(jù),而不會在它們之間創(chuàng)建顯式和剛性的依賴路徑瑰煎。LiveData還尊重你的應用程序組件(Activity铺然,F(xiàn)ragment,Service)的生命周期狀態(tài)酒甸,并做正確的事情以防止內(nèi)存泄漏魄健,從而你的應用程序不會消耗更多的內(nèi)存。

如果你已經(jīng)使用了想Rxjava活著Agrea這類第三方庫插勤,那么你可以使用它們代替LiveData沽瘦,不過你需要處理好它們與組件生命周期之間的關(guān)系。

現(xiàn)在我們使用LiveData<User>來代替UserProfileViewModel中的User字段农尖。所以Fragment可以通過觀察它來更新數(shù)據(jù)其垄。LiveData值得稱道的地方就在于它是生命周期感知的,當生命周期結(jié)束是卤橄,其上的觀察者會被即使清理绿满。

public class UserProfileViewModel extends ViewModel {
    ...
    private LiveData<User> user;
    public LiveData<User> getUser() {
        return user;
    }
}

然后將UserProfileFragment修改如下,觀察數(shù)據(jù)并更新UI:

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    viewModel.getUser().observe(this, user -> {
      // update UI
    });
}

一旦用戶數(shù)據(jù)更新窟扑,onChanged回調(diào)將被調(diào)用然后UI會被刷新喇颁。

如果你熟悉一些使用觀察者模式第三方庫,你會覺得奇怪嚎货,為什么沒有在Fragment的onStop()方法中將觀察者移除橘霎。對于LiveData來說這是沒有必要的,因為它是生命周期感知的殖属,這意味著如果UI處于不活動狀態(tài)姐叁,它就不會調(diào)用觀察者的回調(diào)來更新數(shù)據(jù)。并且在onDestroy后會自動移除洗显。

我們也不需要處理任何視圖重建(如屏幕旋轉(zhuǎn))外潜。ViewModel會自動恢復重建前的數(shù)據(jù)。當新的視圖被創(chuàng)建出來后挠唆,它會接收到與之前相同的ViewModel實例处窥,并且觀察者的回調(diào)會被立刻調(diào)用,更新最新的數(shù)據(jù)玄组。這也是ViewModel為什么不能直接引用視圖對象滔驾,因為它的生命周期長于視圖對象谒麦。

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

現(xiàn)在我們將視圖和模型連接起來,但是模型該怎么獲取數(shù)據(jù)呢哆致?在這個例子中绕德,我們假設(shè)使用REST API從后臺獲取。我們將使用Retrofit來向后臺請求數(shù)據(jù)摊阀。

我們的retrofit類Webservice如下:

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

如果只是簡單的實現(xiàn)耻蛇,ViewModel可以直接操作Webservice來獲取用戶數(shù)據(jù)。雖然這樣可以正常工作驹溃,但你的應用無法保證它的后續(xù)迭代城丧。因為這樣做將太多的責任讓ViewModel來承擔,這樣就違反類之前講到的分層原則豌鹤。又因為ViewModel的生命周期是綁定在Activity和Fragment上的亡哄,所以當UI被銷毀后如果丟失所有數(shù)據(jù)將是很差的用戶體驗。所以我們的ViewModel將和一個新的模塊進行交互布疙,這個模塊叫Repository蚊惯。

Repository模塊負責處理數(shù)據(jù)。它為應用程序的其余部分提供了一個干凈的API灵临。他知道在數(shù)據(jù)更新時從哪里獲取數(shù)據(jù)和調(diào)用哪些API調(diào)用截型。你可以將它們視為不同數(shù)據(jù)源(持久性模型,Web服務儒溉,緩存等)之間的中介者宦焦。

UserRepository類如下:

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

雖然repository模塊看上去沒有必要,但他起著重要的作用顿涣。它為App的其他部分抽象出了數(shù)據(jù)源〔郑現(xiàn)在我們的ViewModel并不知道數(shù)據(jù)是通過WebService來獲取的,這意味著我們可以隨意替換掉獲取數(shù)據(jù)的實現(xiàn)涛碑。

管理組件間的依賴關(guān)系

上面這種寫法可以看出來UserRepository需要初始化Webservice實例精堕,這雖然說起來簡單,但要實現(xiàn)的話還需要知道Webservice的具體構(gòu)造方法該如何寫蒲障。這將加大代碼的復雜度歹篓,另外UserRepository可能并不是唯一使用Webservice的對象,所以這種在內(nèi)部構(gòu)建Webservice實例顯然是不推薦的揉阎,下面有兩種模式來解決這個問題:

  • 依賴注入:依賴注入允許類定義它們的依賴關(guān)系而不構(gòu)造它們庄撮。在運行時,另一個類負責提供這些依賴關(guān)系余黎。我們建議在Android應用程序中使用Google的Dagger 2庫實現(xiàn)依賴注入重窟。Dagger 2通過遍歷依賴關(guān)系樹自動構(gòu)建對象,并在依賴關(guān)系上提供編譯時保證惧财。
  • 服務定位器:服務定位器提供了一個注冊表巡扇,其中類可以獲取它們的依賴關(guān)系而不是構(gòu)造它們。與依賴注入(DI)相比垮衷,實現(xiàn)起來相對容易厅翔,因此如果您不熟悉DI,請改用Service Locator搀突。

這些模式允許你擴展代碼刀闷,因為它們提供明確的模式來管理依賴關(guān)系,而不會重復代碼或增加復雜性仰迁。兩者都允許交換實現(xiàn)進行測試;這是使用它們的主要好處之一甸昏。在這個例子中,我們將使用Dagger 2來管理依賴關(guān)系徐许。

連接ViewModel和Repository

現(xiàn)在施蜜,我們的UserProfileViewModel可以改寫成這樣:

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

緩存數(shù)據(jù)

上面的Repository雖然網(wǎng)絡(luò)請求做了封裝,但是它依賴后臺數(shù)據(jù)源雌隅,所以存在不足翻默。

上面的UserRepository實現(xiàn)的問題是,在獲取數(shù)據(jù)之后恰起,它不會保留在任何地方修械。如果用戶離開UserProfileFragment并重新進來,則應用程序?qū)⒅匦芦@取數(shù)據(jù)检盼。這是不好的肯污,有兩個原因:它浪費了寶貴的網(wǎng)絡(luò)帶寬和迫使用戶等待新的查詢完成。為了解決這個問題吨枉,我們將向我們的UserRepository添加一個新的數(shù)據(jù)源蹦渣,它將把User對象緩存在內(nèi)存中。如下:

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

持久化數(shù)據(jù)

在當前的實現(xiàn)中东羹,如果用戶旋轉(zhuǎn)屏幕或離開并返回到應用程序剂桥,現(xiàn)有UI將立即可見,因為Repository會從內(nèi)存中檢索數(shù)據(jù)属提。但是权逗,如果用戶離開應用程序,并在Android操作系統(tǒng)殺死進程后幾小時后又會怎么樣冤议?

在目前的實現(xiàn)中斟薇,我們將需要從網(wǎng)絡(luò)中再次獲取數(shù)據(jù)。這不僅是一個糟糕的用戶體驗恕酸,也是浪費堪滨,因為它將使用移動數(shù)據(jù)來重新獲取相同的數(shù)據(jù)。你以通過緩存Web請求來簡單地解決這個問題蕊温,但它會產(chǎn)生新的問題袱箱。如果請求一個朋友列表而不是單個用戶遏乔,會發(fā)生什么情況?那么你的應用程序可能會顯示不一致的數(shù)據(jù)发笔,這是最令人困惑的用戶體驗盟萨。例如,相同的用戶的數(shù)據(jù)可能會不同了讨,因為朋友列表請求和用戶請求可以在不同的時間執(zhí)行捻激。你的應用需要合并他們,以避免顯示不一致的數(shù)據(jù)前计。

正確的處理方法是使用持久模型胞谭。這時候Room就派上用場了。

Room是一個對象映射庫男杈,它提供本地數(shù)據(jù)持久性和最少的樣板代碼丈屹。在編譯時,它根據(jù)模式驗證每個查詢势就,從而錯誤的SQL查詢會導致編譯時錯誤泉瞻,而不是運行時失敗。Room抽象了使用原始SQL表和查詢的一些基本實現(xiàn)細節(jié)苞冯。它還允許觀察數(shù)據(jù)庫數(shù)據(jù)(包括集合和連接查詢)的更改袖牙,通過LiveData對象公開這些更改。

要使用Room我們首先需要使用@Entity來定義實體:

@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;
  // getters and setters for fields
}

接著創(chuàng)建數(shù)據(jù)庫類:

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}

值得注意的是MyDatabase是一個抽象了舅锄,Room會在編譯期間提供它的一個實現(xiàn)類鞭达。

接下來需要定義DAO:

@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);
    @Query("SELECT * FROM user WHERE id = :userId")
    LiveData<User> load(String userId);
}

接著在MyDatabase中添加獲取上面這個DAO的方法:

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

這里的load方法返回的是LiveData<User>,所以當相關(guān)數(shù)據(jù)庫中的數(shù)據(jù)有任何變化時皇忿,Room都會通知LiveData上的處于活動狀態(tài)的觀察者畴蹭。

現(xiàn)在我們可以修改UserRepository了:

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

這里雖然我們將UserRepository的直接數(shù)據(jù)來源從Webservice改為本地數(shù)據(jù)庫,但我們卻不需要修改UserProfileViewModel或者UserProfileFragment鳍烁。這就是抽象層帶來的好處叨襟。這也給測試帶來了方便,因為你可以提供一個虛假的UserRepository來測試你的UserProfileViewModel幔荒。

現(xiàn)在糊闽,如果用戶重新回到這個界面,他們會立刻看到數(shù)據(jù)爹梁,因為我們已經(jīng)將數(shù)據(jù)做了持久化的保存右犹。當然如果有用例需要,我們也可不展示太老舊的持久化數(shù)據(jù)姚垃。

在一些用例中念链,比如下拉刷新,如果正處于網(wǎng)絡(luò)請求中,那UI需要告訴用戶正處于網(wǎng)絡(luò)請求中掂墓。一個好的實踐方式就是將UI與數(shù)據(jù)分離谦纱,因為UI可能因為各種原因被更新。從UI的角度來說梆暮,請求中的數(shù)據(jù)和本地數(shù)據(jù)類似服协,只是它還沒有被持久化到數(shù)據(jù)庫中绍昂。

以下有兩種解決方法:

  • 將getUser的返回值中加入網(wǎng)絡(luò)狀態(tài)啦粹。
  • 在Repository中提供一個可以返回刷新狀態(tài)的方法。如果你只是想在用戶通過下拉刷新來告訴用戶目前的網(wǎng)絡(luò)狀態(tài)的話窘游,那這個方法是比較適合的唠椭。

數(shù)據(jù)唯一來源

在以上實例中,數(shù)據(jù)唯一來源是數(shù)據(jù)庫忍饰,這樣做的好處是用戶可以基于穩(wěn)定的數(shù)據(jù)庫數(shù)據(jù)來更新頁面贪嫂,而不需要處理大量的網(wǎng)絡(luò)請求狀態(tài)。數(shù)據(jù)庫有數(shù)據(jù)則使用艾蓝,沒有數(shù)據(jù)則等待其更新力崇。

測試

我們之前提到分層可以個應用提供良好的測試能力,接下來就看看我們怎么測試不同的模塊赢织。

  • 用戶界面與交互:這是唯一一個需要使用到Android UI Instrumentation test的測試模塊亮靴。測試UI的最好方法就是使用Espresso框架。你可以創(chuàng)建Fragment然后提供一個虛假的ViewModel于置。因為Fragment只跟ViewModel交互茧吊,所以虛擬一個ViewModel就足夠了。
  • ViewModel:ViewModel可以用JUnit test進行測試八毯。因為其不涉及界面與交互搓侄。而且你只需要虛擬UserRepository即可。
  • UserRepository:測試UserRepository同樣使用JUnit test话速。你可以虛擬出Webservice和DAO讶踪。你可以通過使用正確的網(wǎng)絡(luò)請求來請求數(shù)據(jù),讓后將數(shù)據(jù)通過DAO寫入數(shù)據(jù)庫泊交。如果數(shù)據(jù)庫中有相關(guān)數(shù)據(jù)則無需進行網(wǎng)絡(luò)請求乳讥。
  • UserDao:對于DAO的測試,推薦使用instrumentation進行測試活合。因為此處無需UI雏婶,并且可以使用in-memory數(shù)據(jù)庫來保證測試的封閉性,不會影響到磁盤上的數(shù)據(jù)庫白指。
  • Webservice:保持測試的封閉性是相當重要的留晚,因此即使是你的Webservice測試也應避免對后端進行網(wǎng)絡(luò)呼叫。有很多第三方庫提供這方面的支持。例如错维,MockWebServer是一個很棒的庫奖地,可以幫助你為你的測試創(chuàng)建一個假的本地服務器。

架構(gòu)圖

架構(gòu)圖

指導原則

編程是一個創(chuàng)意領(lǐng)域赋焕,構(gòu)建Android應用程序也不例外参歹。有多種方法來解決問題,無論是在多個Activity或Fragment之間傳遞數(shù)據(jù)隆判,還是檢索遠程數(shù)據(jù)并將其在本地保持離線模式犬庇,或者是任何其他常見的場景。

雖然以下建議不是強制性的侨嘀,但經(jīng)驗告訴我們臭挽,遵循這些建議將使你的代碼庫從長遠來看更加強大,可測試和可維護咬腕。

  • 在AndroidManifest中定義的Activity欢峰,Service,Broadcast Receiver等涨共,它們不是數(shù)據(jù)源纽帖。相反,他們只是用于協(xié)調(diào)和展示數(shù)據(jù)举反。由于每個應用程序組件的壽命相當短懊直,運行狀態(tài)取決于用戶與其設(shè)備的交互以及運行時的整體當前運行狀況,所以不要將這些組件作為數(shù)據(jù)源照筑。
  • 你需要在應用程序的各個模塊之間創(chuàng)建明確界定的責任范圍吹截。例如,不要在不同的類或包之間傳遞用于加載網(wǎng)絡(luò)數(shù)據(jù)的代碼凝危。同樣波俄,不要將數(shù)據(jù)緩存和數(shù)據(jù)綁定這兩個責任完全不同的放在同一個類中。
  • 每個模塊之間要竟可能少的相互暴露蛾默。不要抱有僥幸心理去公開一個關(guān)于模塊的內(nèi)部實現(xiàn)細節(jié)的接口懦铺。你可能會在短期內(nèi)獲得到便捷,但是隨著代碼庫的發(fā)展支鸡,你將多付多次技術(shù)性債務冬念。
  • 當你定義模塊之間的交互時,請考慮如何使每個模塊隔離牧挣。例如急前,擁有用于從網(wǎng)絡(luò)中提取數(shù)據(jù)的定義良好的API將使得更容易測試在本地數(shù)據(jù)庫中持久存在該數(shù)據(jù)的模塊。相反瀑构,如果將這兩個模塊的邏輯組合在一起裆针,或者將整個代碼庫中的網(wǎng)絡(luò)代碼放在一起,那么測試就更難(如果不是不可能)。
  • 你的應用程序的核心是什么讓它獨立出來世吨。不要花時間重復輪子或一次又一次地編寫相同的樣板代碼澡刹。相反,將精力集中在使你的應用程序獨一無二的同時耘婚,讓Android架構(gòu)組件和其他推薦的庫來處理重復的樣板代碼罢浇。
  • 保持盡可能多的相關(guān)聯(lián)的新鮮數(shù)據(jù),以便你的應用程序在設(shè)備處于脫機模式時可用沐祷。雖然你可以享受恒定和高速連接嚷闭,但你的用戶可能不會。
  • 你的Repository應指定一個數(shù)據(jù)源作為真實的單一來源戈轿。每當你的應用程序需要訪問這些數(shù)據(jù)時凌受,它應該始終源于真實的單一來源。

擴展: 公開網(wǎng)絡(luò)狀態(tài)

在上面的小結(jié)我們故意省略了網(wǎng)絡(luò)錯誤和加載狀態(tài)來保證例子的簡潔性思杯。在這一小結(jié)我們演示一種使用Resource類來封裝數(shù)據(jù)及其狀態(tài)。以此來公開網(wǎng)絡(luò)狀態(tài)挠进。

下面是簡單的Resource實現(xiàn):

//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ò)上抓取視頻的同時在UI上顯示數(shù)據(jù)庫的舊數(shù)據(jù)是很常見的用例色乾,所以我們要創(chuàng)建一個可以在多個地方重復使用的幫助類NetworkBoundResource。以下是NetworkBoundResource的決策樹:

NetworkBoundResource的決策樹

NetworkBoundResource從觀察數(shù)據(jù)庫開始领突,當?shù)谝淮螐臄?shù)據(jù)庫加載完實體后暖璧,NetworkBoundResource會檢查這個結(jié)果是否滿足用來展示的需求,如不滿足則需要從網(wǎng)上重新獲取君旦。當然以上兩種情況可能同時發(fā)生澎办,你希望先將數(shù)據(jù)顯示在UI上的同時去網(wǎng)絡(luò)上請求新數(shù)據(jù)。

如果網(wǎng)絡(luò)請求成果金砍,則將結(jié)果保存到數(shù)據(jù)庫局蚀,然后重新從數(shù)據(jù)庫加載數(shù)據(jù),如果網(wǎng)絡(luò)請求失敗恕稠,則直接傳遞錯誤信息琅绅。

注意:在上面的過程中可以看到當將新數(shù)據(jù)保存到數(shù)據(jù)庫后,我們重新從數(shù)據(jù)庫加載數(shù)據(jù)鹅巍。雖然大部分情況我們不必如此千扶,因為數(shù)據(jù)庫會為我們傳遞此次更新。但另一方面骆捧,依賴數(shù)據(jù)庫內(nèi)部的更新機制并不是我們想要的如果更新的數(shù)據(jù)與舊數(shù)據(jù)一致澎羞,則數(shù)據(jù)谷不會做出更新提示。我們也不希望直接從網(wǎng)絡(luò)請求中獲取數(shù)據(jù)直接用于UI敛苇,因為這樣違背了單一數(shù)據(jù)源的原則妆绞。

下面是NetworkBoundResource類的公共api:

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

注意到上面定義了兩種泛型,ResultType和RequestType,因為從網(wǎng)絡(luò)請求返回的數(shù)據(jù)類型可能會和數(shù)據(jù)庫返回的不一致摆碉。

另外注意到上面代碼中的ApiResponse這個類塘匣,他是將Retroft2.Call轉(zhuǎn)換成LiveData的一個簡單封裝。

下面是NetworkBoundResource余下部分的實現(xiàn):

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

    @MainThread
    NetworkBoundResource() {
        //1初始化NetworkBoundResource
        result.setValue(Resource.loading(null));
        //2從數(shù)據(jù)庫加載本地數(shù)據(jù)
        LiveData<ResultType> dbSource = loadFromDb();

        result.addSource(dbSource, data -> {
            //3加載完成后判斷是否需要從網(wǎng)上更新數(shù)據(jù)
            result.removeSource(dbSource);
            if (shouldFetch(data)) {
                //4從網(wǎng)上更新數(shù)據(jù)
                fetchFromNetwork(dbSource);
            } else {
                //直接用本地數(shù)據(jù)更新
                result.addSource(dbSource,
                        newData -> result.setValue(Resource.success(newData)));
            }
        });
    }

    private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
        //5進行網(wǎng)絡(luò)請求
        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()) {
                //6請求數(shù)據(jù)成功巷帝,保存數(shù)據(jù)
                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) {
                //7保存請求到的數(shù)據(jù)
                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.
                //8再次加載數(shù)據(jù)庫,使用數(shù)據(jù)庫中的最新數(shù)據(jù)
                result.addSource(loadFromDb(),
                        newData -> result.setValue(Resource.success(newData)));
            }
        }.execute();
    }
}

接著我們就可以在UserRepository中使用NetworkBoundResource了楞泼。

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)容合作請聯(lián)系作者
  • 序言:七十年代末驰徊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子堕阔,更是在濱河造成了極大的恐慌棍厂,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件超陆,死亡現(xiàn)場離奇詭異牺弹,居然都是意外死亡,警方通過查閱死者的電腦和手機时呀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門张漂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人谨娜,你說我怎么就攤上這事航攒。” “怎么了趴梢?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵漠畜,是天一觀的道長。 經(jīng)常有香客問我坞靶,道長憔狞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任滩愁,我火速辦了婚禮躯喇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘硝枉。我一直安慰自己廉丽,他們只是感情好,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布妻味。 她就那樣靜靜地躺著正压,像睡著了一般。 火紅的嫁衣襯著肌膚如雪责球。 梳的紋絲不亂的頭發(fā)上焦履,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天拓劝,我揣著相機與錄音,去河邊找鬼嘉裤。 笑死郑临,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的屑宠。 我是一名探鬼主播厢洞,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼典奉!你這毒婦竟也來了躺翻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤卫玖,失蹤者是張志新(化名)和其女友劉穎公你,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體假瞬,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡陕靠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了笨触。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片懦傍。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖芦劣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情说榆,我是刑警寧澤虚吟,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站签财,受9級特大地震影響串慰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜唱蒸,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一邦鲫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧神汹,春花似錦庆捺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至氓拼,卻和暖如春你画,著一層夾襖步出監(jiān)牢的瞬間抵碟,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工坏匪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拟逮,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓适滓,卻偏偏與公主長得像敦迄,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容