Google官方應用程式架構指南

心跡風逝2015發表於2019-03-01

應用程式架構指南

前言-移動應用使用者體驗

在大多數情況下,App只有一個來自桌面或程式啟動器的入口點,然後作為單個整體程式執行。另一方面,Android應用程式具有更復雜的結構。典型的Android應用程式包含多個應用程式元件,包括 activities, fragments, services, content providers, and broadcast receivers等

您在app manifest中宣告瞭大部分這些應用元件。然後,Android作業系統使用此檔案來決定如何將您的應用程式整合到裝置的整體使用者體驗中。鑑於正確編寫的Android應用程式包含多個元件,並且使用者經常在短時間內與多個應用程式進行互動,因此應用程式需要適應不同型別的使用者驅動的工作流程和任務。

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

  1. 該應用程式觸發相機意圖。Android作業系統啟動相機應用程式來處理請求。 此時,使用者已離開社交網路應用程式,但他們的體驗仍然是無縫的。

  2. 相機應用程式可能會觸發其他意圖,例如啟動檔案選擇器,這可能會啟動另一個應用程式。

  3. 最終,使用者返回社交網路應用程式並共享照片。

在此過程中的任何時候,使用者都可能被電話或通知中斷。在對此中斷採取行動後,使用者希望能夠返回並恢復此照片共享過程。此應用程式跳躍行為在移動裝置上很常見,因此您的應用必須正確處理這些問題。

請記住,移動裝置也受資源限制,因此在任何時候,作業系統都可能會殺死某些應用程式程式以為新的程式騰出空間。

鑑於此環境的條件,您的應用程式元件可能會單獨啟動並無序啟動,作業系統或使用者可以隨時銷燬它們。由於這些事件不在您的控制之下,因此 您不應在應用程式元件中儲存任何應用程式資料或狀態,並且您的應用程式元件不應相互依賴。

常見的架構原則

如果您不應該使用應用程式元件來儲存應用程式資料和狀態,那麼您應該如何設計應用程式?

關注點分離

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

請記住,你沒有自己的實現Activity和Fragment; 相反,這些只是表示Android作業系統和應用程式之間合約的膠水類。作業系統可以根據使用者互動或低記憶體等系統條件隨時銷燬它們。為了提供令人滿意的使用者體驗和更易於管理的應用程式維護體驗,最好儘量減少對它們的依賴。

從模型(Model)中驅動UI

另一個重要原則是您應該從模型驅動UI,最好是持久模型。模型是負責處理應用程式資料的元件。它們獨立於View應用中的 物件和應用元件,因此它們不受應用生命週期和相關問題的影響。

永續性是理想的,原因如下:

  • 如果Android作業系統銷燬您的應用以釋放資源,您的使用者不會丟失資料。
  • 如果網路連線不穩定或無法使用,您的應用仍可繼續使用。

通過將應用程式基於具有明確定義的資料管理職責的模型類,您的應用程式更具可測性和一致性。

Google推薦的應用架構

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

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

想象一下,我們正在構建一個顯示使用者配置檔案的UI。我們使用私有後端和REST API來獲取給定配置檔案的資料。

概述

首先,請考慮下圖,該圖顯示了在設計應用程式後所有模組應如何相互互動: Google官方應用程式架構指南

請注意,每個元件僅取決於其下一級的元件。例如,活動和片段僅依賴於檢視模型。儲存庫是唯一依賴於其他多個類的類; 在此示例中,儲存庫依賴於持久資料模型和遠端後端資料來源。

這種設計創造了一種一致和愉快的使用者體驗。無論使用者在上次關閉應用程式幾分鐘後還是幾天後都回到應用程式,他們會立即看到應用程式在本地持續存在的使用者資訊。如果此資料過時,應用程式的儲存庫模組將開始從後臺更新資料。

構建使用者介面

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: Google官方應用程式架構指南

它首先觀察資源的資料庫。第一次從資料庫載入條目時,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

相關文章