1. 程式人生 > >Android官方架構元件指南

Android官方架構元件指南

此指南適用於那些曾經或現在進行Android應用的基礎開發,並希望瞭解和學習編寫Android程式的最佳實踐和架構。通過學習來構建強大的生產級別的應用。

注意:此指南預設你對Android開發有比較深的理解,熟知Android Framework。如果你還只是個Android開發新手,那麼建議先學習下Android的基礎知識。

Android程式設計師面臨的問題

傳統的桌面應用程式開發在大多數情況下,啟動器快捷方式都有一個入口點,並作為一個單一的過程執行,但Android應用程式的結構更為複雜。典型的Android應用程式由多個應用程式元件構成,包括Activity,Fragment,Service,ContentProvider和Broadcast Receiver。

大多數這些應用程式元件在Android作業系統使用的AndroidManifest中宣告,以決定如何將應用程式整合到裝置上來為使用者提供完整的體驗。儘管如前所述,桌面應用程式傳統上是作為一個單一的程序執行的,但正確編寫的Android應用程式則需要更靈活,因為使用者通過裝置上的不同應用程式編織方式,不斷切換流程和任務。

舉個例子,當用戶在社交App上打算分享一張照片,那麼Android系統就會為此啟動相機來完成此次請求。此時使用者離開了社交App,但是這個使用者體驗是無縫連線的。相機可能又會觸發並啟動檔案管理器來選擇照片。最終回到社交App並分享照片。此外,在此過程中的任何時候,使用者可能會被打電話中斷,並在完成電話後再回來分享照片。

在Android中,這種應用間跳轉行為很常見,因此你的應用必須正確處理這些流程。請記住,移動裝置是資源有限的,所以在任何時候,作業系統可能需要殺死一些應用來為新的應用騰出空間。

你的應用程式的所有元件都可以被單獨啟動或無序啟動,並且在任何時候由使用者或系統銷燬。因為應用程式元件是短暫的,它們的生命週期(建立和銷燬時)不受你的控制,因此你不應該將任何應用程式資料或狀態儲存在應用程式元件中,並且應用程式元件不應相互依賴。

常見的架構原理

如果你無法使用應用程式元件來儲存應用程式資料和狀態,應如何構建應用程式?

在你的App開發中你應該將重心放在分層上,如果將所有的程式碼都寫在Activity或者Fragment中,那問題就大了。任何不是處理UI或跟作業系統互動的操作不應該放在這兩個類中。儘量保持它們程式碼的精簡,這樣你可以避免很多與生命週期相關的問題。記住你並不能掌控Activity和Fragment,他們只是在你的App和Android系統間起了橋樑的作用。任何時候,Android系統可能會根據使用者操作或其他因素(如低記憶體)來回收它們。最好儘量減少對他們的依賴,以提供堅實的使用者體驗。

還有一點比較重要的就是持久模型驅動UI。使用持久模型主要是因為當你的UI被回收或者在沒有網路的情況下還能正常給使用者展示資料。模型是用來處理應用資料的元件,它們獨立於應用中的檢視和四大元件。因此模型的生命週期必然和UI是分離的。保持UI程式碼的整潔,會讓你能更容易的管理和調整UI。讓你的應用基於模型開發可以很好的管理你應用的資料並是你的應用更具測試性和持續性。

應用架構推薦

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

注意:不可能存在某一種架構方式可以完美適合任何場景。話雖如此,這種架構應該是大多數用例的良好起點。如果你已經有了很好的Android應用程式架構方式,請繼續保持。

假設我們需要一個現實使用者資料的UI,該使用者的資料檔案將使用REST API從服務端獲取。

構建使用者介面

我們的這個使用者介面由一個UserProfileFragment.java檔案和它的佈局檔案user_profile_layout.xml

為了驅動UI,資料模型需要持有下面兩個資料:

  • User ID:使用者的識別符號。最好使用Fragment的引數將此資訊傳遞到Fragment中。如果Android作業系統回收了Fragment,則會保留此資訊,以便下次重新啟動應用時,該ID可用。
  • User Object:傳統的Java物件,代表使用者的資料。

為此,我們新建一個繼承自ViewModel的名為UserProfileViewModel的模型來持有這個資料。

ViewModel提供特定UI元件的資料,例如Activity和Fragment,並處理與資料處理業務部分的通訊,例如呼叫其他元件來載入資料或轉發使用者修改。ViewModel不瞭解View,並且不受UI的重建(如重由於旋轉而導致的Activity的重建)的影響。

現在我們有一下三個檔案:

  • user_profile.xml: 檢視的佈局檔案。
  • UserProfileViewModel.java: 持有UI資料的模型。
  • UserProfileFragment.java: 用於顯示資料模型中的資料並和使用者進行互動。

一下是具體程式碼(為了簡化,佈局檔案省略)。

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穩定後,Fragment會預設實現LifeCycleOwner。

現在,我們有三個檔案,我們如何連線它們?畢竟,當ViewModel的使用者欄位被設定時,我們需要一種通知UI的方法。這裡就要提到LiveData了。

LiveData是一個可觀察的資料持有者。它允許應用程式中的元件觀察LiveData物件持有的資料,而不會在它們之間建立顯式和剛性的依賴路徑。LiveData還尊重你的應用程式元件(Activity,Fragment,Service)的生命週期狀態,並做正確的事情以防止記憶體洩漏,從而你的應用程式不會消耗更多的記憶體。

如果你已經使用了想Rxjava活著Agrea這類第三方庫,那麼你可以使用它們代替LiveData,不過你需要處理好它們與元件生命週期之間的關係。

現在我們使用LiveData<User>來代替UserProfileViewModel中的User欄位。所以Fragment可以通過觀察它來更新資料。LiveData值得稱道的地方就在於它是生命週期感知的,當生命週期結束是,其上的觀察者會被即使清理。

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

然後將UserProfileFragment修改如下,觀察資料並更新UI:

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

一旦使用者資料更新,onChanged回撥將被呼叫然後UI會被重新整理。

如果你熟悉一些使用觀察者模式第三方庫,你會覺得奇怪,為什麼沒有在Fragment的onStop()方法中將觀察者移除。對於LiveData來說這是沒有必要的,因為它是生命週期感知的,這意味著如果UI處於不活動狀態,它就不會呼叫觀察者的回撥來更新資料。並且在onDestroy後會自動移除。

我們也不需要處理任何檢視重建(如螢幕旋轉)。ViewModel會自動恢復重建前的資料。當新的檢視被創建出來後,它會接收到與之前相同的ViewModel例項,並且觀察者的回撥會被立刻呼叫,更新最新的資料。這也是ViewModel為什麼不能直接引用檢視物件,因為它的生命週期長於檢視物件。

獲取資料

現在我們將檢視和模型連線起來,但是模型該怎麼獲取資料呢?在這個例子中,我們假設使用REST API從後臺獲取。我們將使用Retrofit來向後臺請求資料。

我們的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);
}

如果只是簡單的實現,ViewModel可以直接操作Webservice來獲取使用者資料。雖然這樣可以正常工作,但你的應用無法保證它的後續迭代。因為這樣做將太多的責任讓ViewModel來承擔,這樣就違反類之前講到的分層原則。又因為ViewModel的生命週期是繫結在Activity和Fragment上的,所以當UI被銷燬後如果丟失所有資料將是很差的使用者體驗。所以我們的ViewModel將和一個新的模組進行互動,這個模組叫Repository。

Repository模組負責處理資料。它為應用程式的其餘部分提供了一個乾淨的API。他知道在資料更新時從哪裡獲取資料和呼叫哪些API呼叫。你可以將它們視為不同資料來源(永續性模型,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的其他部分抽象出了資料來源。現在我們的ViewModel並不知道資料是通過WebService來獲取的,這意味著我們可以隨意替換掉獲取資料的實現。

管理元件間的依賴關係

上面這種寫法可以看出來UserRepository需要初始化Webservice例項,這雖然說起來簡單,但要實現的話還需要知道Webservice的具體構造方法該如何寫。這將加大程式碼的複雜度,另外UserRepository可能並不是唯一使用Webservice的物件,所以這種在內部構建Webservice例項顯然是不推薦的,下面有兩種模式來解決這個問題:

  • 依賴注入:依賴注入允許類定義它們的依賴關係而不構造它們。在執行時,另一個類負責提供這些依賴關係。我們建議在Android應用程式中使用Google的Dagger 2庫實現依賴注入。Dagger 2通過遍歷依賴關係樹自動構建物件,並在依賴關係上提供編譯時保證。
  • 服務定位器:服務定位器提供了一個登錄檔,其中類可以獲取它們的依賴關係而不是構造它們。與依賴注入(DI)相比,實現起來相對容易,因此如果您不熟悉DI,請改用Service Locator。

這些模式允許你擴充套件程式碼,因為它們提供明確的模式來管理依賴關係,而不會重複程式碼或增加複雜性。兩者都允許交換實現進行測試;這是使用它們的主要好處之一。在這個例子中,我們將使用Dagger 2來管理依賴關係。

連線ViewModel和Repository

現在,我們的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;
    }
}

快取資料

上面的Repository雖然網路請求做了封裝,但是它依賴後臺資料來源,所以存在不足。

上面的UserRepository實現的問題是,在獲取資料之後,它不會保留在任何地方。如果使用者離開UserProfileFragment並重新進來,則應用程式將重新獲取資料。這是不好的,有兩個原因:它浪費了寶貴的網路頻寬和迫使使用者等待新的查詢完成。為了解決這個問題,我們將向我們的UserRepository新增一個新的資料來源,它將把User物件快取在記憶體中。如下:

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

持久化資料

在當前的實現中,如果使用者旋轉螢幕或離開並返回到應用程式,現有UI將立即可見,因為Repository會從記憶體中檢索資料。但是,如果使用者離開應用程式,並在Android作業系統殺死程序後幾小時後又會怎麼樣?

在目前的實現中,我們將需要從網路中再次獲取資料。這不僅是一個糟糕的使用者體驗,也是浪費,因為它將使用移動資料來重新獲取相同的資料。你以通過快取Web請求來簡單地解決這個問題,但它會產生新的問題。如果請求一個朋友列表而不是單個使用者,會發生什麼情況?那麼你的應用程式可能會顯示不一致的資料,這是最令人困惑的使用者體驗。例如,相同的使用者的資料可能會不同,因為朋友列表請求和使用者請求可以在不同的時間執行。你的應用需要合併他們,以避免顯示不一致的資料。

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

Room是一個物件對映庫,它提供本地資料永續性和最少的樣板程式碼。在編譯時,它根據模式驗證每個查詢,從而錯誤的SQL查詢會導致編譯時錯誤,而不是執行時失敗。Room抽象了使用原始SQL表和查詢的一些基本實現細節。它還允許觀察資料庫資料(包括集合和連線查詢)的更改,通過LiveData物件公開這些更改。

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

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

接著建立資料庫類:

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

值得注意的是MyDatabase是一個抽象了,Room會在編譯期間提供它的一個實現類。

接下來需要定義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

現在我們可以修改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的直接資料來源從Webservice改為本地資料庫,但我們卻不需要修改UserProfileViewModel或者UserProfileFragment。這就是抽象層帶來的好處。這也給測試帶來了方便,因為你可以提供一個虛假的UserRepository來測試你的UserProfileViewModel。

現在,如果使用者重新回到這個介面,他們會立刻看到資料,因為我們已經將資料做了持久化的儲存。當然如果有用例需要,我們也可不展示太老舊的持久化資料。

在一些用例中,比如下拉重新整理,如果正處於網路請求中,那UI需要告訴使用者正處於網路請求中。一個好的實踐方式就是將UI與資料分離,因為UI可能因為各種原因被更新。從UI的角度來說,請求中的資料和本地資料類似,只是它還沒有被持久化到資料庫中。

以下有兩種解決方法:

  • 將getUser的返回值中加入網路狀態。
  • 在Repository中提供一個可以返回重新整理狀態的方法。如果你只是想在使用者通過下拉重新整理來告訴使用者目前的網路狀態的話,那這個方法是比較適合的。

資料唯一來源

在以上例項中,資料唯一來源是資料庫,這樣做的好處是使用者可以基於穩定的資料庫資料來更新頁面,而不需要處理大量的網路請求狀態。資料庫有資料則使用,沒有資料則等待其更新。

測試

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

  • 使用者介面與互動:這是唯一一個需要使用到Android UI Instrumentation test的測試模組。測試UI的最好方法就是使用Espresso框架。你可以建立Fragment然後提供一個虛假的ViewModel。因為Fragment只跟ViewModel互動,所以虛擬一個ViewModel就足夠了。
  • ViewModel:ViewModel可以用JUnit test進行測試。因為其不涉及介面與互動。而且你只需要虛擬UserRepository即可。
  • UserRepository:測試UserRepository同樣使用JUnit test。你可以虛擬出Webservice和DAO。你可以通過使用正確的網路請求來請求資料,讓後將資料通過DAO寫入資料庫。如果資料庫中有相關資料則無需進行網路請求。
  • UserDao:對於DAO的測試,推薦使用instrumentation進行測試。因為此處無需UI,並且可以使用in-memory資料庫來保證測試的封閉性,不會影響到磁碟上的資料庫。
  • Webservice:保持測試的封閉性是相當重要的,因此即使是你的Webservice測試也應避免對後端進行網路呼叫。有很多第三方庫提供這方面的支援。例如,MockWebServer是一個很棒的庫,可以幫助你為你的測試建立一個假的本地伺服器。

架構圖

架構圖

指導原則

程式設計是一個創意領域,構建Android應用程式也不例外。有多種方法來解決問題,無論是在多個Activity或Fragment之間傳遞資料,還是檢索遠端資料並將其在本地保持離線模式,或者是任何其他常見的場景。

雖然以下建議不是強制性的,但經驗告訴我們,遵循這些建議將使你的程式碼庫從長遠來看更加強大,可測試和可維護。

  • 在AndroidManifest中定義的Activity,Service,Broadcast Receiver等,它們不是資料來源。相反,他們只是用於協調和展示資料。由於每個應用程式元件的壽命相當短,執行狀態取決於使用者與其裝置的互動以及執行時的整體當前執行狀況,所以不要將這些元件作為資料來源。
  • 你需要在應用程式的各個模組之間建立明確界定的責任範圍。例如,不要在不同的類或包之間傳遞用於載入網路資料的程式碼。同樣,不要將資料快取和資料繫結這兩個責任完全不同的放在同一個類中。
  • 每個模組之間要竟可能少的相互暴露。不要抱有僥倖心理去公開一個關於模組的內部實現細節的介面。你可能會在短期內獲得到便捷,但是隨著程式碼庫的發展,你將多付多次技術性債務。
  • 當你定義模組之間的互動時,請考慮如何使每個模組隔離。例如,擁有用於從網路中提取資料的定義良好的API將使得更容易測試在本地資料庫中持久存在該資料的模組。相反,如果將這兩個模組的邏輯組合在一起,或者將整個程式碼庫中的網路程式碼放在一起,那麼測試就更難(如果不是不可能)。
  • 你的應用程式的核心是什麼讓它獨立出來。不要花時間重複輪子或一次又一次地編寫相同的樣板程式碼。相反,將精力集中在使你的應用程式獨一無二的同時,讓Android架構元件和其他推薦的庫來處理重複的樣板程式碼。
  • 保持儘可能多的相關聯的新鮮資料,以便你的應用程式在裝置處於離線模式時可用。雖然你可以享受恆定和高速連線,但你的使用者可能不會。
  • 你的Repository應指定一個數據源作為真實的單一來源。每當你的應用程式需要訪問這些資料時,它應該始終源於真實的單一來源。

擴充套件: 公開網路狀態

在上面的小結我們故意省略了網路錯誤和載入狀態來保證例子的簡潔性。在這一小結我們演示一種使用Resource類來封裝資料及其狀態。以此來公開網路狀態。

下面是簡單的Resource實現:

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

以為從網路上抓取視訊的同時在UI上顯示資料庫的舊資料是很常見的用例,所以我們要建立一個可以在多個地方重複使用的幫助類NetworkBoundResource。以下是NetworkBoundResource的決策樹:

NetworkBoundResource的決策樹

NetworkBoundResource從觀察資料庫開始,當第一次從資料庫載入完實體後,NetworkBoundResource會檢查這個結果是否滿足用來展示的需求,如不滿足則需要從網上重新獲取。當然以上兩種情況可能同時發生,你希望先將資料顯示在UI上的同時去網路上請求新資料。

如果網路請求成果,則將結果儲存到資料庫,然後重新從資料庫載入資料,如果網路請求失敗,則直接傳遞錯誤資訊。

注意:在上面的過程中可以看到當將新資料儲存到資料庫後,我們重新從資料庫載入資料。雖然大部分情況我們不必如此,因為資料庫會為我們傳遞此次更新。但另一方面,依賴資料庫內部的更新機制並不是我們想要的如果更新的資料與舊資料一致,則資料谷不會做出更新提示。我們也不希望直接從網路請求中獲取資料直接用於UI,因為這樣違背了單一資料來源的原則。

下面是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,因為從網路請求返回的資料型別可能會和資料庫返回的不一致。

另外注意到上面程式碼中的ApiResponse這個類,他是將Retroft2.Call轉換成LiveData的一個簡單封裝。

下面是NetworkBoundResource餘下部分的實現:

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

    @MainThread
    NetworkBoundResource() {
        //1初始化NetworkBoundResource
        result.setValue(Resource.loading(null));
        //2從資料庫載入本地資料
        LiveData<ResultType> dbSource = loadFromDb();

        result.addSource(dbSource, data -> {
            //3載入完成後判斷是否需要從網上更新資料
            result.removeSource(dbSource);
            if (shouldFetch(data)) {
                //4從網上更新資料
                fetchFromNetwork(dbSource);
            } else {
                //直接用本地資料更新
                result.addSource(dbSource,
                        newData -> result.setValue(Resource.success(newData)));
            }
        });
    }

    private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
        //5進行網路請求
        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請求資料成功,儲存資料
                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儲存請求到的資料
                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再次載入資料庫,使用資料庫中的最新資料
                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();
    }
}

相關推薦

Android官方架構元件指南

此指南適用於那些曾經或現在進行Android應用的基礎開發,並希望瞭解和學習編寫Android程式的最佳實踐和架構。通過學習來構建強大的生產級別的應用。 注意:此指南預設你對Android開發有比較深的理解,熟知Android Framework。如果你還只是個Android開發新手,那麼建議先學習下And

Android 官方架構元件 ViewModel:從前世今生到追本溯源

爭取打造 Android Jetpack 講解的最好的部落格系列: Android官方架構元件Lifecycle:生命週期元件詳解&原理分析 Android官方架構元件ViewModel:從前世今生到追本溯源 Android官方架構元件Paging:分頁庫的設計美學

Android 官方架構元件--ROOM 資料庫操作簡單介紹

本文只是簡單的介紹一下怎麼在我們的專案中使用ROOM和一個簡單的小例子。                ROOM是Google官方推出的一個永續性資料庫,Room永續性庫提供了SQLite的抽象層,以便在充分利用SQLite的同時允許流暢的資料庫訪問。        RO

Android 官方架構元件(二)——LiveData

參考連結: https://developer.android.google.cn/topic/libraries/architecture/livedata https://mp.weixin.qq.com/s/ir3DBkGt5mna3RDjTpRFOQ LiveData是googl

Android 官方架構元件(一)——Lifecycle

參考文章:  https://mp.weixin.qq.com/s/VJif0D5PlrmyA1_emV-k0g  https://mp.weixin.qq.com/s/jU-UHkRbiruBq6BcNOjr5w 下面大量原始碼,請耐心點看~ 什麼是Lifecycle? Li

Android官方架構元件LiveData: 觀察者模式領域二三事

本文是 《Android Jetpack 官方架構元件》 系列的最後一篇文章,和一些朋友的觀點不同的是,我認為它是 最重要 的核心元件,因為 LiveData本身很簡單,但其代表卻正是 MVVM 模式最重要的思想,即 資料驅動檢視(也有叫觀察者模式、響應式等)——這也是擺脫 順序性程式設

Android 官方架構元件(三)——ViewModel

初到掘金,人生地不熟,喜歡的朋友,點個贊鼓勵下新手唄~ 參考文章: https://developer.android.google.cn/topic/libraries/architecture/viewmodel https://mp.weixin.qq.com/s/thoXHuXHC3sV90

Android官方架構元件Paging:分頁庫的設計美學

前言 本文已授權 微信公眾號 玉剛說 (@任玉剛)獨家釋出。 我是一個崇尚 開源 的Android開發者,在過去的一段時間裡,我研究了Github上的一些優秀的開源庫,這些庫原始碼中那些 天馬行空 的 設計 和 思想 令我沉醉其中。 在我職

Android官方架構元件介紹之Lifecycle的使用詳解

Lifecycle 是用來管理和響應activity和Fragment生命週期的變化。我們通常在Activity和Fragment中生命週期方法中進行一些繁重操作,幫我們可以將這些生命週期的方法使用Lifecycle進行管理。它可以自動整合Activity和Fragment生

Android官方架構元件:Lifecycle(控制生命週期)

應該是 Android Jetpack 講解的最好的部落格系列: 概述 同時,如何利用 android.arch.lifecycle 包提供的類來控制資料、監聽器等的 lifecycle。同時,LiveData 與 ViewModel 的 lifecy

Android 官方架構元件之 Lifecycle的學習

Lifecycle:官方介紹 Lifecycle is a class that holds the information about the lifecycle state of a component (like an activity or a fragment) and all

Android官方架構元件介紹之ViewModel

ViewModel 像Activity,Fragment這類應用元件都有自己的生命週期並且是被Android的Framework所管理的。Framework可能會根據使用者的一些操作和裝置的狀態對Activity或者Fragment進行銷燬和重構。作為開發者,這些行為我們是無法干預的。 所以Activity或

Android官方架構元件介紹之LifeCycle

Google 2017 I/O開發者大會於近日召開,在開發者大會上谷歌除了釋出了Android O等一些新產品之外,也對Android程式碼的架構做出了一個官方的迴應。 下面是官方提供的Android App開發的架構圖: 從上圖可以看到一些關鍵字:ViewModel,LiveData,Room等。其

Android官方架構元件介紹之LiveData

LiveData LiveData是一個用於持有資料並支援資料可被監聽(觀察)。和傳統的觀察者模式中的被觀察者不一樣,LiveData是一個生命週期感知元件,因此觀察者可以指定某一個LifeCycle給LiveData,並對資料進行監聽。 如果觀察者指定LifeCycle處於Started或者RESUMED狀

Android官方架構組件介紹之LifeCycle(一)

mave 工程 視圖 平時 清理 star new 內部 serve Android官方架構組件介紹之LifeCycle 下面是官方提供的Android App開發的架構圖: 從上圖可以看到一些關鍵字:ViewModel,LiveData,Room等。其實看了上面視頻的

Android官方架構組件介紹之ViewModel(三)

gets use 時間管理 src context per and 發生 方法 ViewModel 像Activity,Fragment這類應用組件都有自己的生命周期並且是被Android的Framework所管理的。Framework可能會根據用戶的一些操作和設備的狀態對

Android官方架構組件介紹之應用(四)

怎麽 nbsp 註冊 bool 其他 info get inf prot 講一個項目常見的功能,友盟統計功能 例如一個項目有很多多modlue,每個裏面modlue都有Activity,Activity需要友盟統一,Fragment也需要友盟統計。一般做法就是繼承一個Bas

Android官方架構組件:Lifecycle詳解&迪士尼彩樂園網站架設原理分析

ner 觀察者 and 順序 觸發 組件 oncreate mcr save 我們先將重要的這些類挑選出來: LifecycleObserver接口( Lifecycle觀察者):實現該接口的類,通過註解的方式,可以通過被LifecycleOwner類的addObserve

Android官方架構組件:Lifecycle詳解&迪士尼彩樂園定制開發原理分析

npr save this end ons 關於 直接 能夠 封裝 Lifecycle 是一個類,它持有關於組件(如 Activity 或 Fragment)生命周期狀態的信息,並且允許其他對象觀察此狀態。 我們只需要2步: 1、Prestener繼承LifecycleOb

Android官方架構組件:Lifecycle詳解&迪士尼彩樂園平臺搭建原理分析

基類 客服 androi lifecycle 利用 思想 pub 遇到 原理 在過去的谷歌IO大會上,Google官方向我們推出了 Android Architecture Components,其中談到Android組件處理生命周期的問題,向我們介紹了 Handling