1. 程式人生 > >Android架構元件-App架構指南

Android架構元件-App架構指南

注意:本文中的 app元件 指代 Android應用中的四大元件( activities, services, content providers and broadcast receivers

本指南適用於那些已經擁有開發Android應用基礎知識的開發人員,現在想了解能夠開發出更加健壯、優質的應用程式架構。

首先需要說明的是:Android Architecture Components 翻譯為 Android架構元件 並不是我自己隨意翻譯的,而是Google官方部落格中明確稱其為 Android架構元件,因此我遵循了這種叫法。

在這裡我先貼上Google原文地址,以及Android架構元件官方開源示例程式碼地址。

下面這張圖是Android架構元件完整的架構圖,其中表示了的架構元件的所有模組以及它們之間如何互動:

這裡寫圖片描述

APP開發者面臨的常見問題

與傳統的桌面應用程式不同,Android應用程式的結構要複雜得多,在大多數情況下,它們只在桌面快捷啟動方式中有一個入口,並且作為單個程序執行。一個典型的Android應用程式是由多個 APP元件(Android四大元件) 構成的,包括 activities, fragments, services, content providers and broadcast receivers

這些 app元件 中的大部分都是在 應用清單(AndroidManifast.xml)

中宣告的,Android作業系統使用這些元件將應用程式整合到裝置的使用者介面中。雖然,應用程式通常上是以單個程序執行的,但是一個合理的Android應用需要更加靈活,因為使用者可以通過不同的應用程式,在他們的裝置上不斷切換流程和任務。

想象下在我們最喜愛的社交網路應用中分享照片時會發生什麼情況。首先這個應用程式觸發一個Camera(拍照或攝像) Intent,由Android作業系統啟動一個Camera應用來處理請求。此時,使用者雖然離開了這個社交網路應用,但他們的體驗是無縫的。相機應用程式又可能觸發其他 Intent,例如啟動檔案管理器,該檔案管理器可以啟動另一個應用程式,終端使用者回到社交網路應用並分享照片。此外,使用者在這個過程的任何時候都可能被電話打斷,並在打完電話後回來繼續分享照片。

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

這一切的要點在於,我們的 app元件 可以單獨和無序地啟動,並且可以在任何時候由使用者或系統銷燬。由於 app元件 是短暫的,並且它們的生命週期(建立和銷燬時)不在我們的控制之下,因此我們不應該在app元件中儲存任何 app資料或狀態,並且 app元件不應相互依賴。

通用架構原則

如果不使用 app元件儲存app資料和狀態,那該如何構造應用程式呢?

我們需要關注的最重要的事情是:如何在你的應用中分離關注點。最常見的錯誤是將所有的程式碼寫入一個 ActivityFragment任何不處理 UI 或 與作業系統互動的程式碼都不應該出現在這些類中,我們應該儘可能保持 ActivityFragment 精簡,這樣可以避免許多生命週期相關的問題。請記住,我們不擁有這些類,它們只是建立作業系統和我們的應用程式之間契約的膠水類。Android作業系統可能會隨時根據使用者互動或其他因素(如低記憶體)來銷燬它們,最好儘可能地減少依賴他們,以提供可靠的使用者體驗。

第二個重要原則是: 你應該從一個 Model 驅動你的UI,最好是一個持久化的 Model。之所以說持久化是理想的 Model,原因有兩個:如果作業系統銷燬你的應用程式以釋放資源,那麼你的使用者就不會丟失資料,即使網路連線不穩定或連線不上,你的應用程式也會繼續工作。Model 是負責處理應用程式資料的元件,它們獨立於應用程式的 Views 和 app元件,因此 Model 與這些 app元件的生命週期問題是相隔離的。保持簡潔的UI程式碼,以及不受約束的應用程式邏輯,可以使app的管理更加容易,基於具有明確定義的管理資料責任的模型類的應用程式,會更加具有可測試性,並使我們的應用程式狀態保持前後一致。

推薦的App架構

在本節中,我們將演示如何通過使用用例來構造使用了 架構元件(Architecture Components) 的應用程式。

注意:不可能有一種編寫應用程式的方法對每個場景都是最好的。對於大多數用例來說,推薦的這個架構可能是一個好的起點。如果你已經有了編寫Android應用的好方法,那就不要在更改了。

假如我沒正在搭建一個用來顯示 使用者概況的UI,該使用者概況將使用 REST API 從我們自己的伺服器端獲取。

搭建使用者介面

這個UI 將由 UserProfileFragment.javaFragment 相應的 user_profile_layout.xml 佈局檔案組成。

為了驅動使用者介面,我們的資料模型需要儲存兩個資料元素。

  • 使用者ID:使用者的識別符號。最好使用 fragment 引數(setArguments方法) 將此資訊傳遞到 fragment 中。如果Android系統銷燬了你的程序,這些資訊將被保留,便於應用在下次重新啟動時可用。

  • 使用者物件:儲存使用者資料的 POJO(簡單的Java物件)

我們將建立一個基於ViewModelUserProfileViewModel 類來儲存這些資訊。

一個 ViewModel 提供了一個特定 UI 元件中的資料,如一個 fragment 或 activity, 並且負責與資料處理業務的通訊,例如呼叫其他 app元件 來載入資料或轉發使用者資訊的修改。ViewModel不知道View,並且不受配置更改的影響,例如由於螢幕旋轉而重新建立 Activity。

現在我們有3個檔案。

  • user_profile.xml:定義螢幕上的 UI。

  • UserProfileViewModel.java:為 UI 準備資料的類。

  • UserProfileFragment.java:顯示 ViewModel 中的資料並對使用者互動作出響應的 UI 控制器。

接下來我們將開始實現(為了簡單起見,省略了佈局檔案):

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

現在,我們如何將它們聯絡在一起呢?當給 ViewModelUser 欄位設值後,我們需要一種方法來通知使用者介面,這就是 LiveData 類的作用。

LiveData 是一個可觀察的資料持有者。它允許應用程式中的元件觀察 LiveData 物件的更改,但不會在它們之間建立明確的和嚴格的依賴關係路徑。 LiveData 還會關聯 app元件(activities, fragments, services) 的生命週期狀態,並做出合適的事情來防止記憶體洩漏。

注意:如果你已經在使用類似 RxJavaAgera 的庫 ,則可以繼續使用它們而不是LiveData。但是,當你使用它們或其他方式時,請確保正確處理生命週期,以便在相關的LifecycleOwner 停止時暫停資料流,並在銷燬 LifecycleOwner 時銷燬資料流。你還可以新增 android.arch.lifecycle:reactivestreams 以將 LiveData 與其他的響應流庫(例如RxJava2)一起使用。

現在我們用 LiveData<User> 替換 UserProfileViewModel 中的 User 欄位,以便在資料更新時通知 Fragment。最主要的是:LiveData 是生命週期感知的,並且在不在需要時,它將自動清理引用。

public class UserProfileViewModel extends ViewModel {
    ...
    private User user;
    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 來說是沒有必要的,因為它是生命週期感知的,這意味著它不會呼叫回撥,除非 Fragment 處於 活動狀態(已收到 onStart() 但未收到 onStop())。當 fragment 收到 onDestroy() 時,LiveData也將自動移除觀察者 。

對於配置變化(例如,使用者旋轉螢幕)我們也沒有做任何特殊的處理。當配置改變時,ViewModel 會自動恢復,所以一旦新的 Fragment 生效,它將接收到相同的 ViewModel例項,並且 ViewModel 的回撥將立即被當前資料呼叫,這就是 ViewModels 為什麼不應該直接引用 Views 的原因,他們可以比 View的生命週期更持久。想了解更多資訊的請檢視 The lifecycle of a ViewModel

獲取資料

現在我們已經將 ViewModel 關聯到了 Fragment,但是 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 來獲取資料並將其 賦值給 user 物件,雖然這樣是可行的,但是我們的應用程式以後將很難維護。它賦予了 ViewModel 類太多的職責,違背了我們前面提到的關注點分離原則。此外,ViewModel 的作用域與一個 Activity 或一個 Fragment 生命週期相關聯,當他們的生命週期完成時將丟失所有的資料,這是非常糟糕的使用者體驗。因此,我們將 ViewModel 的這個工作委託給了一個新的模組 Repository

Repository 模組負責資料處理操作。他們為應用的其餘部分提供了一個乾淨的API,他們知道從何處獲取資料以及在更新資料時呼叫哪些API。你可以將它們視為不同資料來源 (持久化模型, web服務, 快取, etc.)之間的中介。

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

雖然 repository 模組看起來沒有必要,但是它有一個重要的目的,它從應用程式的其餘部分提取資料來源。現在我們的 ViewModel 不知道資料是從 Webservice 獲取到的,這意味著我們可以根據需要,將它(Webservice)替換為其他的實現。

注意:為了簡單起見,我們忽略了網路錯誤的情況。對於暴露錯誤和載入狀態的另一個實現,請檢視 附錄:暴露網路狀態

管理元件之間的依賴關係:

上面的 UserRepository 類需要一個 Webservice 的例項來工作,UserRepository 可以簡單地建立Webservice ,但要做到這一點,它必須需要知道 Webservice 類的依賴關係來構造它,這會使程式碼顯著和成倍的複雜化(例如,每個需要 Webservice例項的類 都需要知道如何用它的依賴來構造它)。另外,UserRepository 可能不是唯一需要 Webservice 的類。如果每個類建立一個新的 WebService,這將是非常沉重的資源。

現在我們有兩種模式可以用來解決這個問題:

  • 依賴注入:依賴注入允許類在不構造它們的情況下定義它們的依賴關係。在執行時,另一個類負責提供這些依賴關係。我們推薦 Google 的 Dagger 2 庫,在Android應用中實現依賴注入。Dagger 2 通過遍歷依賴關係樹來自動構造物件,併為依賴關係提供編譯時間保證。
  • 服務定位器:服務定位器提供了一個登錄檔,這個類可以獲得它們的依賴 而不是 構建它們。實現起來比依賴注入(DI)更容易,所以如果你不熟悉DI,可以使用 Service Locator。

這些模式允許您擴充套件程式碼,因為它們提供了用於管理依賴關係的清晰模式,無需複製程式碼或增加複雜性。這兩個模式也允許交換實現測試, 這是使用它們的主要好處之一。

在這個例子中,我們將使用 依賴注入 來管理依賴關係。

關聯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 實現 對抽象呼叫 Web服務是有好處的,但是因為它只依賴於一個數據源,所以它不是很有用。

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 是一個物件對映庫,使用最小的模板程式碼來提供本地資料持久化。在編譯時,它會根據 Schema 驗證每個查詢,因此,有問題的SQL查詢會導致編譯時出錯,而不是執行時失敗。Room 抽象了處理原始SQL表和查詢的一些底層實現細節。它還允許觀察對資料庫資料(包括集合和 join 查詢)的更改,通過 LiveData物件 公開這些更改 。另外,它明確定義瞭解決常見問題的執行緒約束,例如在主執行緒上的訪問儲存。

注意:如果你的應用程式已經使用另一個持久化解決方案(如SQLite物件關係對映(ORM)),則不需要使用 Room 替換現有的解決方案。但是,如果你正在編寫新的應用程式或重構現有的應用程式,我們建議使用 Room 來儲存應用程式的資料。這樣,你可以利用庫的抽象和查詢 驗證功能。

要使用 Room,我們需要定義我們的本地 Schema。首先,使用 註解 User 類 以將其標記為資料庫中的表。

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

然後,為我們的 app 建立一個數據庫類繼承於 RoomDatabase

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

注意 MyDatabase 是抽象的。Room 自動提供一個它的實現。有關詳細資訊,請檢視 Android架構元件- Room資料庫的使用

現在我們需要一種將使用者資料插入資料庫的方法。為此,我們將建立一個資料訪問物件(DAO: data access object)

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

然後,從我們的資料庫類中引用 DAO (Data Access Object)

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

請注意,該 load 方法返回一個 LiveData。Room 知道資料庫何時被修改,當資料改變時它會自動通知所有活躍的的察者。因為它使用的是 LiveData,所以這將是有效的,因為只有至少有一個活動的觀察者才會更新資料。

注意:Room 根據 table 的修改來檢查失效,這意味著它可能傳送誤報的通知。

現在我們可以修改 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 的資料,我們並不需要改變我們 UserProfileViewModelUserProfileFragment。這是抽象提供的靈活性。這對於測試來說有好處的,因為你可以在測試你的UserProfileViewModel 的時候提供一個假的 UserRepository

現在我們的程式碼是完整了。如果使用者以後回到相同的使用者介面,他們會立即看到使用者資訊,因為我們持久化了。同時,如果資料過期了,我們的倉庫將在後臺更新資料。當然,根據您的使用情況,如果資料太舊,您可能不希望顯示持久化資料。

在一些使用情況下,如 下拉重新整理,UI 顯示使用者是否正在進行網路操作是非常重要的。將UI 操作與實際資料分開是一個很好的做法,因為它可能因各種原因而導致更新(例如,如果我們獲取朋友列表,同一使用者可能會再次觸發 LiveData<User> 更新)。站在UI 的角度,事實上,當有一個請求執行的時候,另一個數據點,類似於任何其他的資料 (比如 User 物件)。

這個用例有兩種常見的解決方案:

  • 更改 getUser 為返回包含網路操作狀態的 LiveData 。附錄中提供了一個示例實現:公開網路狀態部分。

  • 在 repository 類中提供另一個可以返回使用者重新整理狀態的公共函式。如果只想響應顯式的使用者操作(如下拉重新整理)來顯示網路狀態,則此選項更好。

單一的真相來源:

不同的 REST API 端點通常返回相同的資料。例如,如果我們的伺服器端擁有另一個返回 朋友列表的端點,則同一個使用者物件可能來自兩個不同的API 端點,也許粒度不同。如果 UserRepositoryWebservice請求返回原本的響應,我們的UI可能會顯示不一致的資料,因為在這些請求過程中資料可能已經在伺服器端發生了改變。這就是為什麼在 UserRepository 實現中,Web服務回撥只是將資料儲存到資料庫中。然後,對資料庫的更改將觸發回撥給 活躍狀態的 LiveData 物件。

在這個模型中,資料庫充當了 單一的真相來源,應用程式的其他部分通過 Repository 訪問它。無論你是否使用磁碟快取,我們都建議將你的 Repository 指定為應用程式其餘部分唯一的真相來源。

測試

我們已經提到分離的好處之一就是可測試性,讓我們看看如何測試每個程式碼模組。

  • 使用者介面和互動:你唯一需要花費時間的是 Android UI Instrumentation 。測試UI 程式碼的最好方法是建立一個 Espresso測試。您可以建立 Fragment 併為其提供一個模擬的ViewModel。由於該 Fragment 只與 ViewModel 聯絡,所以偽造它足以完全測試這個UI。

  • ViewModel:ViewModel 可以使用 JUnit 來測試 。你只需要模擬 UserRepository 來測試它。

  • UserRepository:你同樣也可以使用 JUnit 來測試 UserRepository。你需要模擬 Webservice 和 DAO。你可以測試它是否做出了正確的Web服務呼叫,並將結果儲存到資料庫中,如果資料已快取且最新,則不會發出任何不必要的請求。因為 Webservice 和 UserDao 都是介面,你可以模擬它們,或者為更復雜的測試用例建立偽造的實現…

  • UserDao:測試 DAO 類的推薦方法是使用 instrumentation 測試。由於這些 instrumentation 測試不需要任何使用者介面,他們將會執行得很快。對於每個測試,您可以建立一個處於記憶體中的資料庫,以確保測試沒有任何副作用(如更改磁碟上的資料庫檔案)。
    Room 也允許指定資料庫的實現,所以你可以通過提供 JUnit 來測試 SupportSQLiteOpenHelper 的實現。通常不建議使用這種方法,因為裝置上執行的SQLite版本可能與主機上的SQLite版本不同。

  • Webservice:使測試獨立於外界是很重要的,所以你的 Webservice 測試也應該避免對後端進行網路呼叫。有很多庫可以幫助你,例如, MockWebServer 是一個強大的庫,可以幫助你為測試建立一個偽造的本地伺服器。

  • Testing Artifacts 架構元件提供了一個Maven artifact 來控制其後臺執行緒。在android.arch.core:core-testing artifact 內部 ,有2個 JUnit 規則:

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

最終的架構

下圖顯示了我們推薦的架構中的所有模組以及它們如何相互互動:

這裡寫圖片描述

指導原則

程式設計是一個創造性的領域,開發Android應用也不例外。解決問題的方法有很多種,可以在多個Activity 或 Fragment 之間傳遞資料,檢索遠端資料並將其儲存在本地以進行離線模式,也可以使用其他常見應用程式遇到的情況。

雖然以下建議不是強制性的,但是根據我們的經驗,從長遠來看遵循這些建議將使您的程式碼更加健壯,變得可測試和可維護。

  • 你在 AndroidManifest 中定義的入口點(activities, services, broadcast receivers, 等等)不是資料的來源。相反,他們只應該協調與該入口點相關的資料子集。由於每個 app元件的 存活相當短,這取決於使用者與裝置的互動以及當前執行時的狀況,因此你不希望這些入口點中的任何一個成為資料來源。

  • 在應用程式的各個模組之間建立明確的職責界限時要毫不留情。例如,不要將從網路中載入資料的程式碼分散到多個類或包中。同樣,不要把不相關的職責 - 比如資料快取和資料繫結 - 放到同一個類中。

  • 儘可能少地暴露每個模組。不要試圖建立“只有那一個”的快捷方式,從一個模組公開其內部實現細節。你可能會在短期內獲得一些時間,但隨著程式碼庫的不斷髮展,你將會花費更多時間付出技術代價。

  • 在定義模組之間的互動時,請考慮如何使每個模組獨立地進行測試。例如,如果有一個定義良好的 API 從網路中獲取資料,測試將資料儲存在本地資料庫中的模組會變得更容易。相反,如果將這兩個模組的邏輯混合在一起,或者在整個程式碼庫中分散網路請求程式碼,那麼要測試就會更加困難。

  • 你的APP的核心是讓它從其他APP中脫穎而出。不要花費時間重新造輪子,或者一次又一次地寫出相同的樣板程式碼。相反,將精力集中在可以讓你的應用獨特的東西上,讓Android 架構 和其他推薦的庫處理重複的樣板程式碼。

  • 持久化儘可能多的相關和最新的資料,以便當裝置處於離線模式時你的APP依然可用。雖然你可能喜歡恆定的高速連線,但你的使用者可能並不會。

  • 你的 repository 應該指定一個數據源作為單一的事實來源。無論你的應用程式何時需要訪問這些資料,都應始終從單一的事實源頭髮起。有關更多資訊,請檢視 單一的真相來源

附錄:暴露網路狀態

在上面推薦的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);
    }
}

因為從網路載入資料,而從磁碟顯示資料是一個常見的用例,我們將建立一個輔助類 NetworkBoundResource ,它可以在多個地方重複使用。以下是 NetworkBoundResource 的決策樹:

這裡寫圖片描述

它首先通過對資源的資料庫進行觀察。當第一次從資料庫載入條目時,**NetworkBoundResource**0 會檢查結果是否足夠好以便被分發,或者它應該從網路中獲取。請注意,這兩種情況可能同時發生,因為你可能希望在從網路獲取資料時顯示快取的資料。

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

注意:在將新資料儲存到磁碟之後,我們會重新初始化資料庫中的資料流,但通常我們不需要這樣做,因為資料庫將會發送更改。另一方面,依靠資料庫來發送更改,將產生依賴副作用,因為如果資料沒有變化,資料庫可以避免傳送變化,那麼它可能會中斷。我們也不希望傳送從網路返回的結果,因為這將違背單一的真相來源(也許資料庫中會有觸發機制可以改變儲存的值)。我們也不想在沒有新資料的情況下發送 SUCCESS,因為它會向客戶傳送錯誤的資訊。

下面是 公開API 是為 NetworkBoundResource 的子類提供的 :

// 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, implemented
    // in the base class.
    public final LiveData<Resource<ResultType>> getAsLiveData();
}

請注意,上面的類定義了兩個型別引數(ResultTypeRequestType),因為從 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();
    }

    public final LiveData<Resource<ResultType>> getAsLiveData() {
        return result;
    }
}

現在,我們可以使用 NetworkBoundResource 將在 repository 中繫結 User 實現 寫入我們的磁碟和網路。

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