1. 程式人生 > >Android App 架構設計

Android App 架構設計

簡介

本文是對谷歌原生文件的翻譯,僅供學習參照。

原文連結

此文件寫給希望學習最優程式設計實踐和架構以開發健壯、高質量APP的開發者。

開發者常遇到的問題

傳統的桌面程式大多數使用場景是有一個啟動入口,作為一個獨立程序執行。Android app結構要複雜很多,一個典型的Android app由很多元件構成,包括activities,fragment,services,content providers 和broadcast receivers。

App四大元件在Androidmanifest.xml檔案裡面宣告,它們被安卓系統用來決定如何構建App在裝置上的互動體驗。之前提到,桌面App一般執行在一個獨立程序裡面,安卓App則不同。安卓裝置互動場景經常會遇到多個App之間切換任務,因此安卓App設計上需要靈活一些以滿足需求。

舉個例子:使用社交App分享一張照片。首先,社交App發intent啟動相機App,此時使用者已經離開了社交App,但是使用者可能並未感知到這個狀態。相機App又可能傳送intent啟動其他的App,比如圖片選擇器,終端使用者返回社交App完成分享照片的動作。在此過程還可能被其他事件中斷,比如來電話,使用者需要等通話結束以後才可以繼續操作分享照片的動作。

Android app元件可以被單獨啟動,也可以無序啟動,並且可能會隨時被使用者手動或系統銷燬。使用者無法掌控Android app元件的生命週期,因此不應該在元件裡面儲存app的資料和狀態,元件之間也不應相互依賴耦合。

通用的架構原則

  1. 關注點分離一個常見的錯誤是將所有程式碼都寫到Activity或者Fragment,這麼做不僅會讓程式碼看起來很臃腫,難以閱讀和維護,而且容易導致生命週期相關的問題產生。按照谷歌官方的開發推薦,任何不是處理UI和系統的程式碼都不應該寫到這兩個類裡面。Activity或者Fragment可能會因為一些原因被系統銷燬,比如低記憶體的時候,使用者無法掌控。為了使得App更加的穩定可靠,我們應該在開發中最小化對它們的依賴。
  2. Mode驅動UI更新:優選持久化模型。持久化模型有兩個好處:(1)當app被系統回收的時候使用者不用再擔心丟失資料,即使網路不通,app仍然可以繼續執行。Modes是一種元件,它用來持有app的資料,它獨立於views和app的其他元件。因此,它與app四大元件存在的生命週期問題是隔離的。保持UI程式碼和邏輯之間的隔離可以使得程式碼更加容易管理和維護。通過引入Modes類,並給予每一個mode定義好明確的資料對映關係,可以使得app更加方便測試和維護。

推薦的app架構

說明:理論上,不存在一種萬能架構使得app在所有場景下都是最優。因此,本文推薦的架構適用範圍有限,如果你已經有不錯的架構方式,可以不用更換。

下面我們開始案例,假設需要構建UI來顯示使用者的簡歷,簡歷資料需要通過REST API從後臺獲取。

構建介面

UI 對應的類UserProfileFragment.java 佈局檔案是 user_profile_layout.xml.

為了驅動UI,model需要持有兩個資料元素

  • The User ID: 使用者ID。傳遞這個數值最好的方式是在fragment的argument裡面。因為如果app程序被系統回收,這個數值會被持久化,當app重啟的時候還可以獲取到這個資料。
  • The User object: A POJO 持有使用者資料.

給予ViewModel類構建UserProfileViewModel

ViewModel 用來為UI元件(activity或者fragment)提供資料,並且負責UI與業務資料處理之間的通訊。ViewMode不關心UI的變化,例如activity旋轉或者重建。

Now we have 3 files.

  • user_profile.xml: UI定義
  • UserProfileViewModel.java: 為UI提供資料
  • UserProfileFragment.java: UI控制器,用來顯示UserProfileViewModel提供的資料

以下是程式碼實現(為簡單起見,佈局檔案被省略)

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

Note: 上面的例子使用 LifecycleFragment 代替 Fragment 。等lifecycles API穩定以後,Support Library裡面的Fragment將會更新實現 LifecycleOwner

現在已經有三個模組,如何連線他們?當ViewModel裡面使用者資料更新以後,需要通知UI來同步顯示。這時LiveData登場了。

LiveData 可觀察的資料持有者。它允許app元件在不建立與它之間顯示和剛性依賴的前提下觀察LiveData物件的改變。LiveData遵守app元件的生命週期原則,可以避免記憶體洩漏。

Note: 如果你正在使用其他的庫,例如RxJava 或者 Agera,可以不用替換成LiveData。但是如果你準備使用LiveData,你務必要正確處置生命週期,這樣當LifecycleOwner stopped的時候你的資料流也暫停,當LifecycleOwner destroyed的時候你的資料流也destroyed。如果你要在使用LiveData的時候搭配RxJava2等庫,可以通過引入 android.arch.lifecycle:reactivestreams

現在我們使用LiveData來替換 UserProfileViewModel裡面User的屬性,這樣當這個值有變化會通知fragment同步更新。LiveData遵守lifecycle原則,當它不在被需要的時候會自動清理引用。

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

Now we modify UserProfileFragment to observe the data and update the UI.

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

每當使用者資料更新,onChanged回撥方法會被執行用來重新整理UI。

如果你熟悉其它類似LiveData功能的庫,你會發現我們沒有重寫fragment的onStop()方法來停止觀察資料。使用LiveData無需做這個處理,因為它被設計自動感知Lifecycle,當fragment執行onDestroy()的時候,LiveData會自動刪除觀察。

We also didn’t do anything special to handle configuration changes (for example, user rotating the screen). The ViewModel is automatically restored when the configuration changes, so as soon as the new fragment comes to life, it will receive the same instance of ViewModel and the callback will be called instantly with the current data. This is the reason why ViewModels should not reference Views directly; they can outlive the View’s lifecycle. SeeThe lifecycle of a ViewModel.

不必針對configuration 的改變做特殊處理(例如activity旋轉)。當configuration 變化時,ViewModel 會自動恢復資料。因此,當fragment重新啟動,將獲取到與configuration 變化之前相同的ViewModels ,並且callback會被馬上呼叫,資料也和之前保持一致。因此,ViewModes不必直接引用Views,他們可以超越View的生命週期。參考The lifecycle of a ViewModel.

獲取資料

至此,ViewModel和fragment之間已經建立了聯絡,那麼ViewModel如何獲取使用者資料的?在本例中,我們假設後臺提供的是REST API,我們使用Retrofit 來封裝http請求。

下圖展示了使用retrofit與後臺互動

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

ViewMode可以直接呼叫webservice來從後臺獲取資料並傳遞給使用者物件,這是最簡單的一種實現方式。不過這不是最佳方案,因為隨著業務增加,這種架構會比較難擴充套件和維護。這種架構給予ViewMode過多的責任,因此違背了上文提到的關注分離原則。另外,ViewModel已經跟Activity或者Fragment的生命週期繫結,當UI的生命週期結束時資料丟失是非常不好的體驗,因此我們引入了Repository模組,將ViewModel獲取資料的工作交於它。

Repository 模組用來處理資料,包括:從哪兒獲取資料,當資料變化時呼叫什麼API來更新。它可以被看做是不同資料來源之間的調解人,資料來源大致有:持久化資料,webservice,快取等。

UserRepository 使用 WebService 來獲取使用者資料

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

respository看起來不是必須的,但是它有一個很重要的優點:抽象了app獲取資料的通道,比如在上例中ViewModel並不知道資料來源來自Webservice,因此當我們業務需要變更時可以方便修改資料來源。

管理元件之間的依賴:

UserRepository獲取資料的時候需要一個webservice例項,建立webservice例項並不麻煩,但是需要知道構造webservice時的依賴。這樣會稍顯複雜併產生冗餘程式碼,因為並不是只有UserrRepository需要webservice的例項,其他類在使用webservice例項的時候都需要知道構建webservice時的依賴。

有兩個模式可以用來解決上述問題:

  • 依賴注入: 依賴注入框架允許你定義一個類的依賴,而不必自己去構建這個依賴物件。程式碼執行期,有專門的類來負責提供依賴物件。我們推薦Android app使用谷歌Dagger 2 框架來實現依賴注入。Dagger 2通過遍歷依賴關係樹自動構建物件,並在依賴關係上提供編譯時保證。
  • 服務定位: 服務定位器提供了一個登錄檔,其中類可以獲取它們的依賴關係而不是構造它們. 它的實現比依賴注入簡單很多,如果你對依賴注入不熟悉,可以考慮使用服務定位。

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

在本示例中,我們繼續使用Dagger 2 來管理依賴關係。

連線 ViewModel 與 repository

我們通過修改 UserProfileViewModel 來使用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;
    }
}

快取資料

repository對於抽象webservice的請求非常奏效,但是上文示例只有一個數據源,所以可能感覺不是很明顯。

UserRepository也有自身的缺陷,如果使用者離開了UserProfileFragment,app會重新載入資料。這有兩個弊端:

  1. 浪費了網路流量
  2. 重新請求網路資料耗費時間,使用者需要等待

為此,我們在UserRepository裡面增加了快取。

@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會立即重新顯示之前的資料,這是因為我們使用了記憶體快取。但是當用戶離開app,程序被殺死,然後再次返回app,此時會出現什麼情況?

在當前架構中,遇到這種情況需要重新從後臺讀取資料。這個體驗不太好,既耽誤時間也浪費流量。為了解決這個問題,可以快取web請求。但是 如果相同的使用者資料從另一種型別的請求顯示(例如,獲取一個朋友列表)會發生什麼情況? 那麼您的應用程式可能會顯示不一致的資料,這是最令人困惑的使用者體驗。 例如,相同的使用者的資料可能會不同,因為朋友列表請求和使用者請求可以在不同的時間執行。 您的應用需要合併,以避免顯示不一致的資料。

解決上面問題最好的方法是使用持久化模型。再次谷歌推薦使用Room。

Room 是一個物件對映庫,提供本地資料永續性和最小的樣板程式碼。 在編譯時,它根據模式驗證每個查詢,損壞的SQL查詢會導致編譯時錯誤,而不是執行時失敗。 Room摘錄了使用原始SQL表和查詢的一些基本實現細節。 它還允許觀察資料庫資料(包括集合和連線查詢)的更改,通過LiveData物件公開這些更改。 另外,它明確地定義了執行緒約束,解決常見問題,如訪問主執行緒上的儲存。

Note: 如果您熟悉SQLite ORM或Realm等不同資料庫的其他永續性解決方案,則無需將其替換為Room,除非Room的功能集與您的用例更相關。

要使用Room,我們需要定義我們的本地模式。 首先,用@Entity註釋User類,將其標記為資料庫中的一個表。

@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自動提供一個它的實現類。參考文件Room

現在我們需要通過一種方式來向資料庫插入使用者資料,為此我們先新建一個data access object (DAO).

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

然後在資料庫類中引用這個DAO

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

請注意,load方法返回一個LiveData。 Room知道資料庫何時被修改,當資料發生變化時,它會自動通知所有的主動觀察者。使用LiveData,只會在至少有一個主動觀察者時更新資料。

Note: 從alpha 1版本開始,Room根據表修改檢查無效,這意味著它可能會發送錯誤的正面通知。

現在,我們可以修改我們的UserRepository來整合Room資料來源。

@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中資料來源的位置,我們也不需要更改UserProfileViewModel或UserProfileFragment。 這是抽象提供的靈活性。 這也非常適合測試,因為您可以在測試UserProfileViewModel時提供假的UserRepository。

現在我們的程式碼實現已經比較完整了。 如果使用者日後再回到同一個使用者介面,他們會立即看到使用者資訊,因為我們已經實現了持久化。 同時,如果資料過期,我們的儲存庫將在後臺更新資料。 當然,根據您的用例,如果持久資料太舊,您可能不希望顯示持久化的資料。

在一些使用情況下,例如下拉重新整理,當有網路操作的時候UI也應該照常顯示使用者資料。UI與資料分離是很好的做法,因為改變UI的原因可能有很多。

有兩種方法來解決這種情況遇到的問題:

  • 改變getUser的實現,返回一個LiveData資料,包含網路操作的狀態。這裡有一個參考示例Addendum: exposing network status
  • 在repository類中新增一個public方法,返回使用者物件最新的狀態。如果希望通過在UI上顯示網路狀態來響應使用者動作(例如下拉重新整理),那麼此方法更好。

唯一的可靠資料來源

不同REST API返回相同資料也很正常,例如:如果後臺有另外一個請求介面返回一個朋友列表,同樣的使用者物件可能會來自兩個不同的請求介面。通過webservice獲取資料,當後臺資料在在多次請求之間發生變化時,使用者得到的資料可能會出現不一致的現象。因此,在UserRepository實現中web service的回撥只是將資料儲存到資料庫,然後資料庫發生改變會觸發生成一個啟用的LiveData物件。

在這個模型中,資料庫是唯一可靠的資料來源,app其他元件通過repository訪問資料庫。無論是否使用磁碟快取,我們推薦repository來為app設計唯一一個可靠的資料來源。

測試

上面提到,關注分離帶來的一個好處是方便測試。來看下如何測試每一個模組

  • User Interface & Interactions: 這是唯一需要Android UI Instrumentation測試的。 測試UI程式碼的最好方法是建立一個Espresso測試。 您可以建立該fragment併為其提供一個模擬的ViewModel。 由於fragment只與ViewModel進行通訊,所以模擬它將足以完全測試UI。

  • ViewModel: ViewModel 可以使用 JUnit test測試.

  • UserRepository: 您也可以使用JUnit測試來測試UserRepository。 您需要模擬Webservice和DAO。 您可以測試它是否進行正確的Web服務呼叫,將結果儲存到資料庫中,如果資料被快取並且是最新的,則不會發生任何不必要的請求。 既然Webservice和UserDao都是介面,那麼你可以模擬它們,或為更復雜的測試用例偽造一個實現。

  • UserDao: 測試DAO類的推薦方法是使用儀器測試。 由於這些儀器測試不需要任何UI,因此它們可以快速執行。 對於每個測試,可以建立一個記憶體資料庫,以確保測試沒有任何副作用(如更改磁碟上的資料庫檔案)。

    Room 還允許指定資料庫實現,以便您可以通過向其提供支援SQLiteOpenHelper的JUnit實現來測試它。 通常不推薦使用此方法,因為在裝置上執行的SQLite版本可能與主機上的SQLite版本不同。

  • Webservice: 保證測試與外界的獨立性很重要,即使是webservice測試也應該避免向後臺傳送網路請求。有很多的庫可以幫助來實現這個需求,例如MockWebServer 可以偽造一個本地伺服器來用於測試。

  • Testing Artifacts架構元件提供了一個maven工件來控制其後臺執行緒。 在android.arch.core中:核心測試工件,有2個JUnit規則:

    • InstantTaskExecutorRule: 此規則可用於強制架構元件立即執行呼叫執行緒上的任何後臺操作。
    • CountingTaskExecutorRule: 該規則可用於儀器測試,以等待架構元件的後臺操作或將其連線到Espresso作為閒置資源。

最終的架構

下圖展示了谷歌推薦的架構包含的所有模組,以及模組之間如何互動。

img

指導原則

程式設計是一項創造性工作,開發Android應用程式也不例外。 無論是在多個activity或fragment之間傳遞資料,檢索遠端資料並將其在本地保持離線模式,還是任何其他場景,都有多種方法來解決問題,

谷歌推薦,遵循這些建議將使您的程式碼庫從長遠來看更加強大,可測試和可維護。

  • 安卓四大元件不應當被用作資料來源
  • 關注分離,為應用程式的各個模組之間建立明確的責任界限
  • 模組內部高內聚,儘量少的暴露每個模組的是實現細節
  • 模組之間低耦合
  • 不重複造輪子,將開發精力聚焦在自己app獨一無二的特性上
  • 持久化資料,這樣使用者離線狀態也可以使用

附錄:暴露網路狀態

在上面推薦的應用程式體系結構部分,我們故意省略網路錯誤和載入狀態,以保持樣本簡單。 在本節中,我們演示了一種使用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);
    }
}

因為在從磁碟中顯示它的時候載入資料是一個常見的用例,所以我們要建立一個幫助類,可以在多個地方重複使用NetworkBoundResourcethat。 以下是NetworkBoundResource的決策樹:

img

請求從監聽資料庫開始,當第一次從資料庫載入資料,NetworkBoundResource會檢查資料是否有效,如果有效則分發出去,否則開始從網路獲取資料。注意,這兩個動作可以同時發生,例如你在傳送網路請求的時候可能想先展示資料庫中的快取資料,等網路請求完成再用來更新資料內容。

如果網路請求成功完成,則將響應儲存到資料庫中並重新初始化流。 如果網路請求失敗,我們直接傳送失敗。

以下是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),因為從API返回的資料型別可能與本地使用的資料型別不匹配。

還要注意,上面的程式碼使用ApiResponse作為網路請求。 ApiResponse是Retrofit2.Call類的簡單包裝,用於將其響應轉換為LiveData。

以下是NetworkBoundResource類的其餘實現:

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

現在,我們可以使用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 app 架構設計01

clas -h tab size data 資源 top post 樣式 1:本文有摘抄, 1 2 3 4 5 - 開發過程中。需求、設計、編碼的一致性 - 整個程序具有統一的風格,比方對話框樣式,button風格,色調等UI元素 - 整個程序詳細統一的結

Android App 架構設計

簡介 本文是對谷歌原生文件的翻譯,僅供學習參照。 原文連結 此文件寫給希望學習最優程式設計實踐和架構以開發健壯、高質量APP的開發者。 開發者常遇到的問題 傳統的桌面程式大多數使用場景是有一個啟動入口,作為一個獨立程序執行。Android app結

Android App設計架構:MVC,MVP,MVVM與架構經驗談

用戶 自己的 req html pla 觀察 持久化 重構 his 來源: Android App的設計架構:MVC,MVP,MVVM與架構經驗談 和MVC框架模式一樣,Model模型處理數據代碼不變在Android的App開發中,很多人經常會頭疼於App的架構如何設計:

架構篇】Android移動app架構設計淺談

前言 架構,又名軟體架構,是有關軟體整體結構與元件的抽象描述,用於指導大型軟體系統各個方面的設計。 軟體架構設計目標: 1.可靠性(Reliable)。軟體架構的可靠是產品設計的前提。 2.安全性(Secure)。軟體架構的安全性是

Android App設計架構:MVC、MVP、MVVM 的分析

MVC框架模式一樣,Model模型處理資料程式碼不變在Android的App開發中,很多人經常會頭疼於App的架構如何設計: 我的App需要應用這些設計架構嗎? MVC,MVP等架構講的是什麼?區別是什麼? 本文就來帶你分析一下這幾個架構的特性,優缺點,以及App架構設計中

[轉]App架構設計經驗談:接口的設計

手動 alt obj jpg 檢查 服務器 相對 版本號 單個 原文地址:http://developer.51cto.com/art/201601/503767.htm App與服務器的通信接口如何設計得好,需要考慮的地方挺多的,在此根據我的一些經驗做一些總結分享,旨在拋

Android App架構指南

該指南針的目標人群是已經知道如何建構簡單的app,並且希望瞭解構建健壯的產品級app的最佳實踐和推薦架構。 app開發者面臨的難題 不同於大部分的傳統桌面應用只有一個入口,並且作為一個整體的程序執行,Android app有更加複雜的結構。一個典型的app由多種元件構成,包括acti

Android工程架構設計:Base Library(基層MVP框架)基於EventBus

Base Library部分把App中Application,UI(activity,fragment)公用方法重新封裝成模板方法,並開放對子類的擴充套件。同時融入mvp設計思想,封裝成基於mvp的基層架構體系。 目錄 1,IApplication(介面): 2,BaseAp

Android工程架構設計:專案群架構設計

我們寫程式碼的時候,經常會把多個類相同的功能程式碼(方法)抽出來封裝成父類,各個子類繼承父類再做擴充套件。 隨著公司開發維護的專案越來越多,你會發現各個專案中有一些通用的可複用的程式碼或者模組,考慮到資源替換,工程複用等問題,需要把公共部分剝離出來。 公司名為sky_dreaming,目前公

Android APP架構淺談(含程式碼)

寫在開頭 不多說,本文圍繞一張圖展開,請查閱,歡迎共同討論,叨擾了。如果你還有什麼需求或者什麼想法,可一起完善此demo一起進步哦!!! 讓我們從library到mouble,讓我們談談我理解的專案的Project架構。 lib-netwo

Android APP架構思考

##########################正文開始了############################### 從2011年到現在,做了幾年的Android應用與Android平臺上Opengl es應用開發,下面是關於Android APP

App架構設計經驗談:介面的設計

轉自: http://keeganlee.me/post/architecture/20160107 App與伺服器的通訊介面如何設計得好,需要考慮的地方挺多的,在此根據我的一些經驗做一些總結分享,旨在拋磚引玉。 安全機制的設計 現在,大部分App的介面都採用RES

Android基礎架構設計

最近公司因為發展,會新開幾個專案,領導讓整理下基礎框架好統一使用,也方便其他人員熟悉程式碼。因此,再次記錄一下。 1.1基本結構 基本架構我先用現在市面普及和成熟的mvp(model-view-presenter),我的理念是職責分層,高內聚低耦合。 MVP模式的核

淺談Android App架構

一、什麼是架構 什麼是架構,我最初的理解,架構就是通過降低偶合性,提高安全性和擴充套件性,達到方便對軟體進行維護的一套行之有效的分層思想。在我看來架構最主要的就是降低偶合性和提高擴充套件性,我們平常對於客戶端的修改和重構也基本上是圍繞這兩個點而進行的。當

App架構設計經驗談:資料層的設計

一個App,從根本上來說,就是對資料的處理,包括資料從哪裡來、資料如何組織、資料怎麼展示,從職責上劃分就是:資料管理、資料加工、資料展示。相對應的也就有了三層架構:資料層、業務層、展示層。本文就先講講資料層的設計。 資料層,是三層架構中的最底層,負責資料的管理。它主要的任務

App架構設計經驗談:介面的設計

安全機制的設計 現在,大部分App的介面都採用RESTful架構,RESTFul最重要的一個設計原則就是,客戶端與伺服器的互動在請求之間是無狀態的,也就是說,當涉及到使用者狀態時,每次請求都要帶上身份驗證資訊。實現上,大部分都採用token的認證方式,一般流程是:

Hybrid APP架構設計思路

轉自: https://segmentfault.com/a/1190000004263182 關於Hybrid模式開發app的好處,網路上已有很多文章闡述了,這裡不展開。 本文將從以下幾個方面闡述Hybrid app架構設計的一些經驗和思考。 通訊 作為一種

Android-IM架構設計

###1. 架構總覽 ###2. 模組介紹 ####2.1 協議封裝與任務流程 #####1) 協議與任務的封裝 a. 協議有協議頭(協議頭因為格式相同,被抽象出來)和協議體組成,協議有兩類:請求協議(request)和回覆協議(response); b. 任務(action)由請求協議、回覆協議和

關於android APP 架構的一些認知和理解

本文算是一篇漫談,談一談關於android開發中工程初始化的時候如何在初期我們就能搭建一個好的架構。本文先分析幾個當今比較流行的android軟體包,最後我們汲取其中覺得優秀的部分,搭建我們自己的通用android工程模板。 關於Android架構,因為手機的限制,目前

APP社交類專案設計之六ANDROID前臺架構及通訊介紹

       前臺安卓功能採用了MVP架構,與後臺通訊使用了當前主流的RetrofitManager網路通訊外掛,底層通訊過程封裝了Okhttp。在與後臺實際通訊過程中,RetrofitManager是單例模式,如下圖所示