Google官方應用程序架構指南

應用程序架構指南

前言-移動應用用戶體驗

在大多數狀況下,App只有一個來自桌面或程序啓動器的入口點,而後做爲單個總體進程運行。另外一方面,Android應用程序具備更復雜的結構。典型的Android應用程序包含多個應用程序組件,包括 activities, fragments, services, content providers, and broadcast receivers等html

您在app manifest中聲明瞭大部分這些應用組件。而後,Android操做系統使用此文件來決定如何將您的應用程序集成到設備的總體用戶體驗中。鑑於正確編寫的Android應用程序包含多個組件,而且用戶常常在短期內與多個應用程序進行交互,所以應用程序須要適應不一樣類型的用戶驅動的工做流程和任務。java

例如,當您考慮在本身喜歡的社交網絡應用中分享照片時會發生什麼:react

  1. 該應用程序觸發相機意圖。Android操做系統啓動相機應用程序來處理請求。 此時,用戶已離開社交網絡應用程序,但他們的體驗仍然是無縫的。android

  2. 相機應用程序可能會觸發其餘意圖,例如啓動文件選擇器,這可能會啓動另外一個應用程序。git

  3. 最終,用戶返回社交網絡應用程序並共享照片。github

在此過程當中的任什麼時候候,用戶均可能被電話或通知中斷。在對此中斷採起行動後,用戶但願可以返回並恢復此照片共享過程。此應用程序跳躍行爲在移動設備上很常見,所以您的應用必須正確處理這些問題。web

請記住,移動設備也受資源限制,所以在任什麼時候候,操做系統均可能會殺死某些應用程序進程覺得新的進程騰出空間。數據庫

鑑於此環境的條件,您的應用程序組件可能會單獨啓動並沒有序啓動,操做系統或用戶能夠隨時銷燬它們。因爲這些事件不在您的控制之下,所以 您不該在應用程序組件中存儲任何應用程序數據或狀態,而且您的應用程序組件不該相互依賴。編程

常見的架構原則

若是您不該該使用應用程序組件來存儲應用程序數據和狀態,那麼您應該如何設計應用程序?後端

關注點分離

最重要的原則是 分離關注點,在一個 Activity 或一個Fragment 中編寫全部代碼是一個常見的錯誤。這些基於UI的類應該只包含處理UI和app交互的邏輯。經過保持這些類的精簡,您能夠避免許多與生命週期相關的問題發生。

請記住,你沒有本身的實現Activity和Fragment; 相反,這些只是表示Android操做系統和應用程序之間合約的膠水類。操做系統能夠根據用戶交互或低內存等系統條件隨時銷燬它們。爲了提供使人滿意的用戶體驗和更易於管理的應用程序維護體驗,最好儘可能減小對它們的依賴。

從模型(Model)中驅動UI

另外一個重要原則是您應該從模型驅動UI,最好是持久模型。模型是負責處理應用程序數據的組件。它們獨立於View應用中的 對象和應用組件,所以它們不受應用生命週期和相關問題的影響。

持久性是理想的,緣由以下:

  • 若是Android操做系統銷燬您的應用以釋放資源,您的用戶不會丟失數據。
  • 若是網絡鏈接不穩定或沒法使用,您的應用仍可繼續使用。

經過將應用程序基於具備明肯定義的數據管理職責的模型類,您的應用程序更具可測性和一致性。

Google推薦的應用架構

在本文中,咱們將演示如何使用 Android Jetpack Components 構建應用程序,方法是使用端到端的用例。

注意:編寫最適合每種狀況的應用程序是不可能的。話雖這麼說,這個推薦的架構是大多數狀況和工做流程的良好起點。若是您已經有一種編寫遵循通用架構原則的 Android 應用程序的好方法,則無需更改它。
複製代碼

想象一下,咱們正在構建一個顯示用戶配置文件的UI。咱們使用私有後端和REST API來獲取給定配置文件的數據。

概述

首先,請考慮下圖,該圖顯示了在設計應用程序後全部模塊應如何相互交互:

請注意,每一個組件僅取決於其下一級的組件。例如,活動和片斷僅依賴於視圖模型。存儲庫是惟一依賴於其餘多個類的類; 在此示例中,存儲庫依賴於持久數據模型和遠程後端數據源。

這種設計創造了一種一致和愉快的用戶體驗。不管用戶在上次關閉應用程序幾分鐘後仍是幾天後都回到應用程序,他們會當即看到應用程序在本地持續存在的用戶信息。若是此數據過期,應用程序的存儲庫模塊將開始從後臺更新數據。

構建用戶界面

UI由片斷 UserProfileFragment 和相應的佈局文件組成 user_profile_layout.xml 。

要驅動UI,咱們的數據模型須要包含如下數據元素:

  • User ID:用戶的標識符。最好使用Fragment參數將此信息傳遞到片斷中。若是Android操做系統破壞了咱們的流程,則會保留此信息,所以下次從新啓動應用時ID就可用。
  • User object:包含用戶詳細信息的數據類。

咱們使用 UserProfileViewModel 基於 ViewModel 的架構組件來保存此信息。

一個 ViewModel 對象提供針對特定 UI 組件中的數據,如一個 fragment 或 activity,幷包含數據處理的業務邏輯與 model 進行通訊。例如,ViewModel 能夠調用其餘組件來加載數據,它能夠轉發用戶請求來修改數據。ViewModel 不知道UI組件,所以它不會受到配置更改的影響,例如旋轉設備時,從新建立的 activity 。
複製代碼

咱們如今定義瞭如下文件:

  • user_profile.xml:屏幕的UI佈局定義。
  • UserProfileFragment:顯示數據的UI控制器。
  • UserProfileViewModel:準備數據以供查看 UserProfileFragment 並對用戶交互做出反應的類。

如下代碼段顯示了這些文件的起始內容。(爲簡單起見,省略了佈局文件。)

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;
    }
}
複製代碼

UserProfileFragment

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_layout, container, false);
    }
}
複製代碼

如今咱們有了這些代碼模塊,咱們如何鏈接它們?畢竟,當user在UserProfileViewModel類中設置字段時,咱們須要一種方法來通知UI。這就是LiveData架構組件的用武之地。

LiveData 是一個可觀察的數據持有者。應用程序中的其餘組件可使用此> holder監視對象的更改,而無需在它們之間建立明確且嚴格的依賴關係路徑。LiveData組件還尊重應用程序組件的生命週期狀態(如activities, fragments, and services),幷包括清除邏輯以防止對象泄漏和過多的內存消耗。

注意:若是您已經使用了像 RxJava 或 Agera 這樣的庫 ,則能夠繼續使用它們而不是 LiveData。可是,當您使用這些庫和方法時,請確保正確處理應用程序的生命週期。特別是,確保在相關 LifecycleOwner 內容中止時暫停數據流,並在相關內容 LifecycleOwner 被銷燬時銷燬這些流。您還能夠添加 android.arch.lifecycle:reactivestreams 組件以將 LiveData 與另外一個反應流庫(如RxJava2)一塊兒使用。
複製代碼

要將LiveData組件合併到咱們的應用程序中,咱們更改 UserProfileViewModel 中的字段類型變成 LiveData。如今,在UserProfileFragment 更新數據時通知。此外,因爲此 LiveData字段可識別生命週期,所以在再也不須要引用後會自動清除引用。

UserProfileViewModel

public class UserProfileViewModel extends ViewModel {
    ...
    //private User user;
    private LiveData<User> user;
    public LiveData<User> getUser() {
        return user;
    }
}
複製代碼

如今咱們修改UserProfileFragment觀察數據並更新UI:

UserProfileFragment

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

每次更新用戶配置文件數據時, onChanged() 都會調用回調,並刷新UI。

若是您熟悉使用可觀察回調的其餘庫,您可能已經意識到咱們沒有覆蓋片斷的onStop()方法來中止觀察數據。LiveData不須要此步驟,由於它可識別生命週期,這意味着onChanged()除非片斷處於活動狀態,不然它不會調用回調。也就是說,它已收到onStart()但還沒有收到onStop())。調用 fragment's 的onDestroy()方法時,LiveData也會自動刪除觀察者。

咱們也沒有添加任何邏輯來處理配置更改,好比用戶旋轉設備的屏幕。當配置發生變化時,UserProfileViewModel會自動恢復,所以一旦建立新的片斷,它就會接收到相同的ViewModel實例,而且使用當前數據當即調用回調。鑑於ViewModel對象的目的是超越它們更新的相應視圖對象,您不該該在ViewModel的實現中包含對視圖對象的直接引用。有關ViewModel生命週期的更多信息對應於UI組件的生命週期,請參閱 ViewModel的生命週期

請求數據

如今咱們已經使用 LiveData 鏈接 UserProfileViewModel 到了 UserProfileFragment,咱們如何獲取用戶配置文件數據?

對於此示例,咱們假設咱們的後端提供REST API。咱們使用 Retrofit 庫來訪問咱們的後端,儘管您能夠自由地使用不一樣的庫來實現相同的目的。

如下是咱們 Webservice 與後端通訊的定義: 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獲取數據並將此數據分配給咱們的LiveData對象。這種設計有效,但經過使用它,咱們的應用程序隨着它的發展變得愈來愈難以維護。它給 UserProfileViewModel 類帶來了太多的責任 ,這違反了 關注點分離 原則。此外,ViewModel的範圍與 Activity or Fragment 生命週期聯繫在一塊兒,這意味着當關聯的UI對象的生命週期結束時,來自Webservice的數據就會丟失。這種行爲會產生不良的用戶體驗。

相反,咱們的ViewModel將數據抓取過程委託給一個新的模塊,即一個存儲庫。

存儲庫模塊處理數據操做。它們提供了一個乾淨的API,以便應用程序的其他部分能夠輕鬆地檢索這些數據。他們知道從何處獲取數據以及在更新數據時要進行的API調用。您能夠將存儲庫視爲不一樣數據源之間的調解器,例如持久性models,web services 和 caches。
複製代碼

咱們的UserRepository類(如如下代碼段所示)使用一個實例WebService來獲取用戶的數據:

UserRepository

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // 這不是一個最優的實現。咱們稍後再解決。
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }

            // 爲了簡便起見,省略了錯誤案例。
        });
        return data;
    }
}
複製代碼

即便存儲庫模塊看起來沒必要要,它也有一個重要的目的:它從應用程序的其他部分抽象出數據源。如今,咱們 UserProfileViewModel不知道如何獲取數據,所以咱們能夠爲視圖模型提供從幾個不一樣的數據獲取實現得到的數據。

注意:爲簡單起見,咱們省略了網絡錯誤狀況。有關公開錯誤和加載狀態的替代實現,請參閱 附錄:公開網絡狀態。
複製代碼

管理組件之間的依賴關係

UserRepository 上面的類須要一個 Webservice 獲取用戶數據的實例。它能夠簡單地建立實例,但要作到這一點,它還須要知道 Webservice 類的依賴關係。另外, UserRepository 可能不是惟一須要的Webservice 的類。這種狀況要求咱們複製代碼,由於須要引用的每一個類都須要 Webservice 知道如何構造它及其依賴項。若是每一個類建立一個新的WebService,咱們的應用程序可能會變得很是消耗資源。

您可使用如下設計模式來解決此問題:

  • 依賴注入(DI):依賴注入容許類在不構造它們的狀況下定義它們的依賴關係。在運行時,另外一個類負責提供這些依賴項。咱們建議使用 Dagger 2 庫在Android應用程序中實現依賴注入。Dagger 2經過遍歷依賴樹自動構造對象,併爲依賴關係提供編譯時保證。
  • 服務定位器:服務定位器模式提供了一個註冊表,其中類能夠獲取它們的依賴關係而不是構造它們。

實現服務註冊表比使用 依賴注入 更容易,所以若是您不熟悉DI,請改用服務定位器模式。

這些模式容許您擴展代碼,由於它們提供了清晰的模式來管理依賴項,而無需複製代碼或增長複雜性。此外,這些模式容許您在測試和生產數據獲取實現之間快速切換。

咱們的示例應用程序使用Dagger 2來管理 Webservice對象的依賴項。

鏈接ViewModel和存儲庫

如今,咱們修改咱們 UserProfileViewModel 使用 UserRepository 對象:

UserProfileViewModel

public class UserProfileViewModel extends ViewModel {
    private LiveData<User> user;
    private UserRepository userRepo;

    // 命令 Dagger 2 提供 UserRepository 參數。
    @Inject
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(int userId) {
        if (this.user != null) {
            // ViewModel is created on a per-Fragment basis, so the userId
            // doesn't change.
            return;
        }
        user = userRepo.getUser(userId);
    }

    public LiveData<User> getUser() {
        return this.user;
    }
}
複製代碼

緩存數據

UserRepository 實現將對 Webservice 對象的調用抽象出來,但由於它只依賴於一個數據源,它不是很靈活。

UserRepository實現的關鍵問題是,在它從咱們的後端獲取數據以後,它不會在任何地方存儲這些數據。所以,若是用戶離開 UserProfileFragment,而後返回到它,咱們的應用程序必須從新取回數據,即便它沒有改變。

因爲如下緣由,此設計不是最理想的:

  • 它浪費了寶貴的網絡帶寬。
  • 它強制用戶等待新查詢完成。

爲了解決這些缺點,咱們向 UserRepository 添加了一個新的數據源,它在內存中緩存 User 對象:

UserRepository

// 告訴Dagger2 這個類只應該構造一次。
@Singleton
public class UserRepository {
    private Webservice webservice;

    // 簡單的內存緩存。爲了簡潔起見,省略了細節。
    private UserCache userCache;

    public LiveData<User> getUser(int userId) {
        LiveData<User> cached = userCache.get(userId);
        if (cached != null) {
            return cached;
        }

        final MutableLiveData<User> data = new MutableLiveData<>();
        userCache.put(userId, data);

        // 這個實現仍然是次優的,可是比之前更好了。
        // 一個完整的實現也須要對應的處理錯誤案例,此處忽略
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }
        });
        return data;
    }
}
複製代碼

持續存儲數據

使用咱們當前的實現,若是用戶旋轉設備或離開並當即返回應用程序,現有的UI將當即可見,由於存儲庫從內存緩存中檢索數據。

可是,若是用戶離開應用程序並在Android操做系統殺死進程後幾小時後回來會發生什麼?在這種狀況下依靠咱們當前的實現,咱們須要從網絡再次獲取數據。這種從新獲取過程不只僅是糟糕的用戶體驗; 這也是浪費,由於它消耗了寶貴的移動數據。

您能夠經過緩存Web請求來解決此問題,但這會產生一個關鍵的新問題:若是相同的用戶數據顯示來自其餘類型的請求,例如獲取朋友列表,會發生什麼?該應用程序將顯示不一致的數據,這充其量使人困惑。例如,若是用戶在不一樣時間發出好友列表請求和單用戶請求,咱們的應用可能會顯示同一用戶數據的兩個不一樣版本。咱們的應用程序須要弄清楚如何合併這些不一致的數據。

處理這種狀況的正確方法是使用持久模型。這是Room persistence library來救援的地方。

Room 是一個對象映射庫,它經過最少的樣板代碼提供本地數據持久性。在編譯時,它會根據您的數據模式驗證每一個查詢,所以損壞的SQL查詢會致使編譯時錯誤而不是運行時故障。房間抽象出了使用原始SQL表和查詢的一些底層實現細節。它還容許您觀察對數據庫數據的更改,包括收藏品和鏈接查詢,使用LiveData對象公開這些更改。它甚至顯式地定義了處理常見線程問題的執行約束,好比訪問主線程上的存儲器。

注意:若是您的應用已經使用了其餘持久性解決方案,例如SQLite對象關係映射(ORM),則無需使用Room替換現有解決方案。可是,若是您正在編寫新應用或重構現有應用,咱們建議您使用Room來保留應用的數據。這樣,您就能夠利用庫的抽象和查詢驗證功能。
複製代碼

要使用Room,咱們須要定義本地模式。首先,咱們將@Entity註釋添加 到User數據模型類中,並將 @PrimaryKey註釋添加到類的id字段中。這些註釋標記User爲數據庫id中的表和表的主鍵:

User

@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;

  // Getters and setters for fields.
}
複製代碼

而後,咱們經過實現 RoomDatabase 咱們的應用程序來建立數據庫類 :

UserDatabase

@Database(entities = {User.class}, version = 1)
public abstract class UserDatabase extends RoomDatabase {
}
複製代碼

注意這UserDatabase是抽象的。Room自動提供它的實現。有關詳細信息,請參閱Room 文檔。

咱們如今須要一種將用戶數據插入數據庫的方法。對於此任務,咱們建立了一個數據訪問對象(DAO)

UserDao

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

請注意,該load方法返回一個類型的對象LiveData。Room 知道數據庫什麼時候被修改,並在數據發生變化時自動通知全部活動觀察者。因爲Room使用LiveData,所以該操做很是有效; 它僅在至少有一個活動觀察者時才更新數據。

注意:Room根據表格修改檢查失效,這意味着它可能會發送誤報通知。
複製代碼

在UserDao定義了咱們的類以後,咱們從數據庫類中引用DAO: UserDatabase

@Database(entities = {User.class}, version = 1)
public abstract class UserDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}
複製代碼

如今咱們能夠修改咱們 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);
        // 直接從數據庫返回LiveData對象。
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        // 在後臺線程中運行。
        executor.execute(() -> {
            // 檢查最近是否獲取用戶數據。
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // 刷新數據。
                Response<User> response = webservice.getUser(userId).execute();

                // 在這裏檢查錯誤。

                // 更新數據庫。LiveData對象是自動的
                // 刷新,因此咱們不須要作任何其餘事情。
                userDao.save(response.body());
            }
        });
    }
}
複製代碼

請注意,即便咱們更改了數據的來源 UserRepository,咱們也不須要更改咱們的 UserProfileViewModel 或 UserProfileFragment。這個小範圍的更新展現了咱們的應用程序架構提供的靈活性。它也很是適合測試,由於咱們能夠提供假數據 UserRepository 並同時測試咱們的產品 UserProfileViewModel。

若是用戶在返回使用此體系結構的應用程序以前等待幾天,那麼在存儲庫能夠獲取更新信息以前,他們可能會看到過期的信息。根據您的使用狀況,您可能不但願顯示此過期信息。相反,您能夠顯示佔位符數據,該數據顯示虛擬值並指示您的應用當前正在獲取並加載最新信息。

單一的真實來源

不一樣的REST API端點返回相同的數據是很常見的。例如,若是咱們的後端有另外一個端點,它返回一個朋友列表,同一個用戶對象可能來自兩個不一樣的API端點,甚至可能使用不一樣的粒度級別。若是UserRepository原樣返回來自Webservice請求的響應,而不檢查一致性,咱們的ui可能會顯示使人困惑的信息,由於存儲庫中數據的版本和格式將取決於最近調用的端點。

所以,咱們的UserRepository實現將web服務響應保存到數據庫中。對數據庫的更改會觸發對活動LiveData對象的回調。使用這個模型,數據庫可做爲單一真實來源,應用程序的其餘部分可以使用UserRepository訪問它。不管您是否使用磁盤緩存,咱們建議您的存儲庫指定一個數據源做爲應用程序其他部分的惟一真實來源。

顯示正在進行的操做

在某些用例中,例如pull-to-refresh,UI向用戶顯示當前正在進行網絡操做很是重要。將UI操做與實際數據分開是一種很好的作法,由於數據可能會因各類緣由而更新。例如,若是咱們獲取了一個好友列表,可能會再次以編程方式獲取相同的用戶,從而觸發 LiveData更新。從UI的角度來看,有請求在運行這一事實只是另外一個數據點,相似於User對象自己的任何其餘數據。

咱們可使用如下策略之一在UI中顯示一致的數據更新狀態,不管更新數據的請求來自何處:

  • 更改getUser()以返回類型的對象LiveData。該對象將包括網絡操做的狀態。 有關示例,請參閱 NetworkBoundResource 中 android-architecture-components GitHub項目中的實現。
  • 在UserRepository類中提供另外一個能夠返回刷新狀態的公共函數User。若是要僅在數據獲取過程源自顯式用戶操做(例如,下拉刷新pull-to-refresh)時纔在UI中顯示網絡狀態,則此選項會更好。

測試每一個組件

在關注點分離部分,咱們提到遵循這一原則的一個關鍵好處是可測試性。

如下列表顯示瞭如何從擴展現例中測試每一個代碼模塊:

  • 用戶界面和交互:使用Android UI工具測試。建立此測試的最佳方法是使用 Espresso庫。您能夠建立片斷併爲其提供模擬 UserProfileViewModel。由於片斷只與片斷進行通訊,因此 UserProfileViewModel 模擬這個類就足以徹底測試應用的UI。
  • ViewModel:您能夠 UserProfileViewModel使用JUnit測試來測試該類。你只須要模擬一個類,UserRepository。
  • UserRepository:您也可使用JUnit test來測試 UserRepository。您須要模擬 Webservice 和 UserDao。在這些測試中,驗證如下行爲:
    • 存儲庫進行正確的Web服務調用。
    • 存儲庫將結果保存到數據庫中。
    • 若是數據被緩存而且是最新的,則存儲庫不會發出沒必要要的請求。

由於Webservice和UserDao都是接口,因此您能夠對它們進行模擬,或者爲更復雜的測試用例建立假數據的實現。

  • UserDao:使用檢測測試來測試DAO類。因爲這些檢測測試不須要任何UI組件,所以它們能夠快速運行。 對於每一個測試,建立一個內存數據庫以確保測試沒有任何反作用,例如更改磁盤上的數據庫文件。

注意:Room容許指定數據庫實現,所以能夠經過提供JUnit實現來測試DAO 。可是,不建議使用此方法,由於設備上運行的SQLite版本可能與開發計算機上的SQLite版本不一樣。 SupportSQLiteOpenHelper

  • Web服務:在這些測試中,避免對後端進行網絡調用。對於全部測試,尤爲是基於Web的測試,獨立於外部世界很是重要。 包括 MockWebServer 在內的幾個庫 能夠幫助您爲這些測試建立虛假的本地服務器。
  • 測試工件:Architecture Components提供了一個maven工件來控制其後臺線程。該 android.arch.core:core-testing 工件包含如下JUnit的規則:
    • InstantTaskExecutorRule:使用此規則當即執行調用線程上的任何後臺操做。
    • CountingTaskExecutorRule:使用此規則等待架構組件的後臺操做。您還能夠將此規則與 Espresso 關聯爲空閒資源。

最佳作法

編程是一個創造性的領域,構建Android應用程序也不例外。有許多方法能夠解決問題,不管是在多個活動或片斷之間傳遞數據,檢索遠程數據並在本地持久保存以用於脫機模式,仍是任何其餘很是重要的應用程序遇到的常見場景。

雖然如下建議不是強制性的,但咱們的經驗是,遵循它們可使您的代碼庫在長期運行中更加健壯,可測試和可維護:

  • 避免將應用的入口點(如活動,服務和廣播接收器)指定爲數據源。 相反,它們應該只與其餘組件進行協調,以檢索與該入口點相關的數據子集。每一個應用程序組件都是至關短暫的,這取決於用戶與設備的交互以及系統的總體當前健康情況。
  • 在應用的各個模塊之間建立明肯定義的責任範圍。 例如,不要將代碼庫中的數據加載到代碼庫中的多個類或包中。一樣,不要將多個不相關的職責(例如數據緩存和數據綁定)定義到同一個類中。
  • 從每一個模塊儘量少地暴露。 不要試圖建立「只是那個」的快捷方式,從一個模塊公開內部實現細節。您可能會在短時間內得到一些時間,但隨着代碼庫的發展,您會屢次承擔技術債務。
  • 考慮如何使每一個模塊獨立可測試。 例如,具備用於從網絡獲取數據的定義良好的API使得更容易測試將該數據保存在本地數據庫中的模塊。相反,若是您將這兩個模塊的邏輯混合在一個地方,或者在整個代碼庫中分發網絡代碼,那麼測試就變得更加困難 - 若是不是不可能的話。
  • 專一於您應用的獨特核心,以便從其餘應用中脫穎而出。 不要一次又一次地編寫相同的樣板代碼來從新發明輪子。相反,請將時間和精力集中在使應用程序獨一無二的地方,並讓Android架構組件和其餘推薦的庫處理重複的樣板。
  • 保持儘量多的相關和新鮮數據。 這樣,即便設備處於離線模式,用戶也能夠享受應用的功能。請記住,並不是全部用戶都享受恆定的高速鏈接。
  • 將一個數據源指定爲單一事實來源。 每當您的應用須要訪問此數據時,它應始終源於此單一事實來源。

附錄:暴露網絡狀態

在上面 推薦的應用程序架構 部分中,咱們省略了網絡錯誤和加載狀態以保持代碼片斷的簡單性。

本節演示如何使用Resource封裝數據及其狀態的類來公開網絡狀態。

如下代碼段提供瞭如下示例實現Resource:

// A generic class that contains data and status about loading this data.
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<>(Status.SUCCESS, data, null);
    }

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

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

    public enum Status { SUCCESS, ERROR, LOADING }
}
複製代碼

由於在顯示該數據的磁盤副本時從網絡加載數據是很常見的,因此建立一個能夠在多個位置重用的幫助程序類是很好的。在本例中,咱們建立了一個名爲的類NetworkBoundResource。

下圖顯示瞭如下決策樹NetworkBoundResource:

它首先觀察資源的數據庫。第一次從數據庫加載條目時,NetworkBoundResource檢查結果是否足以分派或是否應從網絡從新獲取。請注意,這兩種狀況均可能同時發生,由於您可能但願在從網絡更新緩存數據時顯示緩存數據。

若是網絡調用成功完成,它會將響應保存到數據庫中並從新初始化流。若是網絡請求失敗,則 NetworkBoundResource直接發送失敗。

注意:將新數據保存到磁盤後,咱們從數據庫從新初始化流。可是,咱們一般不須要這樣作,由於數據庫自己剛好發送了更改。

請記住,依賴數據庫來分派更改涉及依賴於相關的反作用,這是很差的,由於若是數據庫由於數據沒有更改而最終沒有調度更改,則可能會發生這些反作用的未定義行爲。

此外,不要發送從網絡到達的結果,由於這會違反單一的真實原則。畢竟,數據庫可能包含在「保存」操做期間更改數據值的觸發器。一樣,不要在沒有SUCCESS新數據的狀況下進行調度,由於那時客戶端會收到錯誤版本的數據。
複製代碼

如下代碼段顯示了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 to fetch
    // potentially updated data 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 object that represents the resource that's implemented
    // in the base class.
    public final LiveData<Resource<ResultType>> getAsLiveData();
}
複製代碼

請注意有關類定義的這些重要細節:

  • 它定義了兩個類型的參數,ResultType而且RequestType,由於從API返回的數據類型可能不符合當地使用的數據類型。
  • 它使用一個ApiResponse爲網絡請求調用的類。ApiResponse是一個簡單的包裝Retrofit2.Call類,它將響應轉換爲實例LiveData。

NetworkBoundResource該類的完整實現做爲android-architecture-components GitHub項目的一部分出現 。

建立後NetworkBoundResource,咱們能夠用它來寫咱們的磁盤和網絡結合實現User的UserRepository類:

UserRepository

class UserRepository {
    Webservice webservice;
    UserDao userDao;

    public LiveData<Resource<User>> loadUser(final int 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();
    }
}
複製代碼

Content and code samples on this page are subject to the licenses described in the Content License. Java is a registered trademark of Oracle and/or its affiliates.

上次更新日期:九月 25, 2018

相關文章
相關標籤/搜索