【譯】Google官方推出的Android架構組件系列文章(一)App架構指南

PS: 2018.06.24按照官網最新文檔更新本文翻譯

系列文章導航

  1. 【譯】Google官方推出的Android架構組件系列文章(一)App架構指南
  2. 【譯】Google官方推出的Android架構組件系列文章(二)將Architecture Components引入工程
  3. 【譯】Google官方推出的Android架構組件系列文章(三)處理生命周期
  4. 【譯】Google官方推出的Android架構組件系列文章(四)LiveData
  5. 【譯】Google官方推出的Android架構組件系列文章(五)ViewModel
  6. 【譯】Google官方推出的Android架構組件系列文章(六)Room持久化庫

原文地址:https://developer.android.com/jetpack/docs/guide

這篇指南適用于熟悉構建app基礎,并且想要了解構建強大的生產級應用的最佳實踐和推薦架構的開發人員。

App開發人員面臨的常見問題

不像傳統的桌面應用,大部分情況下從一個單一的快捷啟動器啟動,之后作為一個單一進程運行,Android應用程序結構更復雜。一個典型的Android應用由多個應用組件構建而成,包含activityfragmentservicecontent provider以及broadcast receiver

這些應用組件大多數聲明于app manifest文件中,Android操作系統通過這個文件來決定如何將你的應用程序集成到設備的整體用戶體驗中。如前所述,桌面應用一般是運行在一個單獨的進程中,而一個編寫正確的Android應用則需要更加靈活。因為,用戶可以不斷切換流程和任務而任意使用設備上的不同應用程序。

舉個例子,思考一下當你在喜歡的社交應用中分享照片時會發生什么。該應用觸發一個camera intent,Android系統根據這個Intent啟動相機應用來處理這個請求。在這個時間點,用戶離開了這個社交應用,但是他們的體驗卻是無縫的。接下來,相機應用可能會觸發別的Intent,比如啟動文件選擇框,這可能會啟動另一個應用。最后用戶返回到社交應用,然后分享這張照片。在這個過程的任意時間點,用戶也可能會被一個電話中斷,在打完電話后才會回來分享照片。

在Android中,這種應用間跳躍行為是很常見的,因此你的應用必須能夠正確得處理這些流程。牢記一點,移動設備的資源是有限的,因此在任何時候,操作系統可能需要殺掉某些應用來為新的應用騰空間。

所有這些歸納為一點:你的應用組件可以被單獨、無序地啟動,并且在任意時間可以被用戶或者系統銷毀。因為應用組件生存時間是短暫的,并且他們的生命周期(創建和銷毀)不受你的控制,所以你不應該將任何應用程序數據或狀態存儲在應用程序組件中,并且應用組件不應該相互依賴。

常用架構準則

如果不能用應用組件來存儲應用數據和狀態,那么應用應該如何架構呢?

你應該聚焦的最重要的事情,是你應用中的關注點分離(separation of concerns)。一個常犯的錯誤是把所有代碼都寫在Activity或者Fragment中。任何不是用來處理UI或者操作系統交互相關的代碼都不應該放到這些類中。盡可能讓這些類保持簡潔能夠讓你避免很多生命周期相關的問題。請記住,你并不擁有這些類,它們僅僅是將你的應用和操作系統黏貼在一起的合約類。任何時候,Android系統可能會根據用戶交互或者其他因素(如低內存)而銷毀它們。最好盡量減少對它們的依賴,從而提供一個堅實的用戶體驗。

第二條重要原則是,你應該根據模型來驅動UI,最好是持久化模型。有兩個原因來說明持久化是理想的:如果操作系統銷毀了你的應用來釋放資源,你的用戶將不會丟掉數據,并且當網絡抖動或者沒有連接時,你的應用仍然可以繼續工作。模型(Model)是那些負責處理應用數據的組件。它們獨立于View(視圖)和應用組件,因此它們與這些組件的生命周期問題隔離。保持UI代碼簡單,遠離應用邏輯將會更容易管理。將你的應用程序構建在那些數據管理責任定義良好的模型類之上,將會使它們可測試,并具備應用一致性。

推薦的應用架構

在這一節,我們通過一個用例來演示如何使用Architecture Components架構應用程序。

注意:沒有哪一種編寫app的方式能夠最佳滿足所有場景。話雖如此,這個推薦的架構對于大多數場景都是一個好的開端。如果你已經擁有一種好的編寫Android應用的方式,你可以不需要改變。

假設我們在構建一個展示用戶信息的UI。用戶信息可以使用REST API從我們的私有后端獲取到。

構建界面

UI包括一個fragment UserProfileFragment.java,以及相應的布局文件user_profile_layout.xml

為了驅動UI,我們的數據模型需要持有兩個數據元素。

  • User ID: 用戶ID。最好通過fragment參數來傳遞這個數據。如果系統銷毀了進程,該信息會被保留,當app重啟后再次可用
  • User Object:持有用戶數據的一個POJO對象。

我們創建一個繼承自ViewModelUserProfileViewModel類,該類將持有上面的信息。

ViewModel向具體的UI組件(比如fragmentactivity)提供數據,并且處理與數據處理業務部分的通信,例如調用別的組件加載數據或轉發用戶修改。ViewModel并不知道View,并且不受配置改變的影響,比如因為旋轉而重新創建Activity。

現在,我們有3個文件:

  • user_profile.xml: 定義UI界面
  • UserProfileViewModel.java: 為UI提供數據的類
  • UserProfileFragment.java: UI控制器。展示ViewModel的數據,響應用戶交互。

下面是我們的初始實現(為了簡化省略布局文件):

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字段設置以后,我們需要一種方式來通知UI。所以,該LiveData類登場了。

LiveData是一個可觀察的數據持有者(data holder)。它允許應用組件觀察LiveData對象的改變,而不需要在它們之間創建顯式和剛性的依賴路徑。LiveData還尊重應用程序組件(ActivityFragmentService)的生命周期狀態,并且做正確的事情以防止對象泄漏, 從而使你的應用程序不消耗更多的內存。

注意:如果你已經在使用像RxJavaAgrea這樣的庫,你可以繼續使用而不必替換為LiveData。但是當你在使用這些庫或其他方法的時候,請確保能正確處理生命周期,比如說當相關的LifecycleOwner停止時候你的數據流應該暫停,而LifecycleOwner銷毀的時候,你的數據流也應該被銷毀。你也可以添加android.arch.lifecycle:reactivestreams庫,將LiveData和另一個響應式流庫配合使用(比如RxJava2)。

現在我們將UserProfileViewModel中的User域替換成LiveData<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將會刷新。

如果你熟悉別的使用可觀察回調的庫,則可能已經意識到我們不必重寫FragmentonStop()方法來停止觀察數據。使用LiveData是不必這么做的,因為它是生命周期感知的,也就是說只有Fragment在激活狀態(收到onStart(),但是沒有收到onStop())的時候,LiveData才會調用回調。當Fragment收到onDestroy()時,LiveData會自動移除觀察者。

我們也不需要做任何特殊的事情來處理配置改變(比如,用戶旋轉屏幕)。當配置改變時,一旦新的Fragment創建,ViewModel會自動還原,它將收到同一個ViewModel實例,并且將立即使用當前的數據調用回調。這也是為啥 ViewModels不應該直接引用Views。它們可以超越View的生命周期。參見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的范圍被綁到了ActivityFragment的生命周期上,因此在生命周期結束時將會丟失所有的數據,這是一個很糟糕的用戶體驗。相反,我們的ViewModel將把這項工作委托給一個新的Repository模塊。

Repository模塊負責處理數據操作。他們向app的其他部分提供一個干凈整潔的API。他們知道從哪里去獲取數據,知道當數據更新時候需要調用哪些API。你可以把他們看作不同數據源(持久化數據,Web服務,緩存等)之間的中間人。

下面的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拉取的,這也就意味著如果有需要的話我們可以將它替換成其他的實現。

注意:為了簡單起見,我們去掉了網絡異常情況。有關暴露錯誤和加載狀態的替代實現版本,請參閱附錄:暴露網絡狀態

管理組件間依賴關系

上面的UserRepository類需要一個Webservice實例來完成它的工作。它可以簡單創建一個實例,但是這樣做它需要知道構建Webservice的依賴關系。這將會使代碼復雜化并且重復(比如,每個需要一個Webservice實例的類都需要知道如何使用它的依賴來構造它)。此外,UserRepository可能不是唯一一個需要Webservice的類。如果每個類都創建一個新的WebService,這將會是資源浪費。

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

  • 依賴注入:依賴注入允許類定義他們的依賴關系而不創建他們。在運行時,另外的類負責提供這些依賴。我們推薦Google的Dagger2庫來實現Android應用的依賴注入。
  • 服務定位器(Service Locator):服務定位器提供了一個注冊表,其中類可以獲取它們的依賴關系,而不是構造它們。與依賴注入相比,實施起來相對容易,因此如果你不熟悉依賴注入,請改用Service Locator

這些模式允許你擴展代碼,因為它們為管理依賴關系提供了明確的模式,而會重復代碼或增加復雜性。這兩種方式都允許交換實現方式進行測試,這也是采用它們的主要好處之一。

在這個例子里,我們將使用Dagger2來管理依賴。

連接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 并返回,app將重新拉取數據。這個很糟糕,因為兩個原因:它浪費了寶貴的網絡帶寬,并迫使用戶等待新的查詢完成。為了解決這個問題,我們將向我們的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對象暴露這些更改。另外,它明確定義了解決常見問題的線程約束,例如在主線程上訪問存儲。

注意:如果你熟悉其他持久化方案像SQLite ORM或其他的數據庫比如Realm,則無需將其替換為Room,除非Room的功能集與你的用例更相關。如果你在編寫新的app或者重構老的app,我們建議使用Room來做數據持久化。

要使用Room,我們需要定義我們的本地協議(schema)。首先,在User類上增加@Entity注解,將其標識為數據庫的一個表。

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

然后,通過繼承RoomDatabase來創建你的應用數據庫類。

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

注意到MyDatabase是抽象的。Room將自動提供它的實現。詳細內容請參閱Room文檔。

現在,我們需要一種方式把user數據插入到數據庫中。為了滿足這個,我們創建一個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<User>Room知道數據庫什么時候被修改,并且當數據變化的時候,它會自動通知所有處于激活狀態的觀察者。因為它使用的是LiveData,這將是很高效的,因為它只會在至少有一個激活觀察者時更新數據。

注意:Room基于數據庫表修改來檢查無效修改,這意味著它可能會發送假的正面通知。( Room checks invalidations based on table modifications which means it may dispatch false positive notifications.)

現在我們可以修改我們的 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

現在我們的代碼是完整的。如果用戶幾天后返回到同一個頁面,他們可以立即看到用戶信息因為我們將這部分數據持久化保存了。同時,如果數據過時,我們的reposity將在后臺更新數據。當然,這取決于你的使用場景,如果持久化數據太舊,你可能不希望對其展示。

在某些使用場景,比如下拉刷新,當進行網絡操作時候,對于UI來說向用戶展示操作進度是非常重要的。把UI操作與實際數據分離是很好的做法,因為數據可能可能由于各種原因被更新(例如,如果我們獲取一個好友列表,則可能再次獲取相同的用戶,觸發LiveData<User>更新)。從UI的角度來說,一個正在進行中的請求只是一個數據點這樣一個事實,和別的數據片(比如User對象)沒啥區別。

對于這種場景,有兩種常見的解決辦法:

  • 更改getUser返回一個包含網絡操作異常的LiveData。附錄:暴露網絡狀態一節提供了一個實現例子。
  • reposity類中提供一個可以返回User刷新狀態的接口。如果要僅在UI中顯示網絡狀態(為了響應用戶操作,比如下拉刷新),這個選項會更好。

真相的唯一來源

不同的REST API端返回相同的數據,這種情況是很常見的。舉個栗子,如果我們的后臺有一個端點返回一個朋友列表,那么同一個user對象可能來自不同的API端點,也可能是不同的粒度。如果UserRepository按原樣從WebService請求然后返回響應,那么我們的UI可能會顯示不一致的數據,因為數據可能會在這些請求之間的服務端發生更改。這也是為什么在UserRepository的實現里,Web服務回調僅僅將數據保存到數據庫中。然后,對于數據庫的改變會觸發激活的LiveData的回調。

在這個模型里面,數據庫扮演著真相的唯一來源角色,app的其他部分通過reposity來訪問它。不管你是否使用磁盤緩存,我們推薦你的reposity指定某個數據源作為應用程序的其余部分的唯一真實來源。

測試

我們已經提到分離的好處之一是可測試性。讓我們看看如何測試每個代碼模塊。

  • UI和交互:這將是你唯一需要Android UI Instrumentation 測試的時間。測試UI代碼的最佳途徑是創建一個Espresso測試。你可以創建一個fragment,然后給它提供一個Mock的ViewModel。因為這個fragment僅僅與ViewModel通信,對其進行Mock將足以完全測試這個UI。

  • ViewModel:ViewModel可以通過JUnit來測試。你只需要Mock UserRepository就可以測試它。

  • UserRepository: 你也可以用JUnit來測試UserRepository。你需要mock Webservice 和DAO。你可以測試它是否執行了正確的Web服務調用,將結果保存到數據庫中,如果數據已經被緩存和更新,則不會發生任何不必要的請求。

  • UserDao:測試DAO類的推薦方法是使用instrumentation 測試。因為這些instrumentation 測試不需要任何UI,他們將運行很快。對于每個測試,你可以創建一個內存數據庫來保證測試沒有任何副作用(如更改磁盤上的數據庫文件)

    Room還允許指定數據庫實現,因此你可以通過提供SupportSQLiteOpenHelper的JUnit實現來測試它。通常不推薦使用此方法,因為運行在設備上的SQLite版本可能與你主機上的SQLite版本不同。

  • WebService:使測試獨立于外部世界是很重要的,甚至你的WebService測試應該避免執行對后臺的網絡請求。有很多庫可以解決這個問題。例如,MockWebServer是一個很好的庫,它可以幫助你創建一個假的本地服務器用于測試。

  • 測試ArtifactArchitecture Components提供一個maven artifact 來控制它的后臺線程。在android.arch.core:core-testing artifact里面,有兩個JUnit規則:

    • InstantTaskExecutorRule:該規則可用于強制Architecture Components在調用線程上立即執行任何后臺操作。
    • CountingTaskExecutorRule:該規則可用于instrumentation 測試,以等待Architecture Components的后臺操作或連接到Espresso作為閑置資源。

最終架構

下面的圖顯示了我們推薦架構的所有模塊,以及它們之間如何交互。

final-architecture.png

指導原則

編程是一個創意領域,構建Android應用程序也不例外。有很多辦法去解決一個問題,無論是在多個Activity或Fragment之間傳遞數據,檢索遠程數據并將其保存到本地用于離線模式,還是任何其他常用應用遇到的常見場景。

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

  • 你定義在manifest文件中的入口點——activity,service,broadcast recevier等等,不是數據源。相反,它們應該只是與該入口點相關的數據自己的協調者。因為每個應用組件的壽命相當短,取決于用戶與設備的交互以及運行時的整體狀況,你不希望任何的這些入口點變成數據源。
  • 堅決在你的應用程序各個模塊之間創建明確定義的責任邊界。比如說,不要將從網絡中加載數據的代碼散布到各個類或者包中。同樣,不要將無關責任比如數據緩存和數據綁定雜糅到同一個類中。
  • 每個模塊盡可能少的向外暴露。不要試圖創建一個從一個模塊暴露內部實現細節的萬能快捷方式。你可能會在短期內節約一點時間,但隨著代碼庫的發展,你將多付出很多技術債務。
  • 當你定義模塊之間的交互時,請考慮如何讓每個模塊分離成可測試的。例如,擁有一個定義良好的從網絡獲取數據的API將使得更容易測試在本地數據庫中持久化該數據的模塊。相反,如果將這兩個模塊的邏輯雜糅到一起,或者將網絡代碼散布在你整個代碼庫中,將會非常難以測試。
  • 你的應用程序的核心是能夠讓它脫穎而出的那部分東西。不要花時間重復造輪子,或者一遍遍寫模板代碼。相反,你應該將精力集中到讓你的應用獨一無二,處理重復模板代碼的事情就交給Android Architecture Component和其他推薦的庫吧。
  • 相對多一點持久化數據,盡可能更新數據,以便當設備處于離線狀態時,你的應用仍然可用。雖然你可能享受穩定和高速的連接,但是你的用戶可能不會。
  • 你的repository 應該指定一個數據源作為唯一真相數據源。每當你的應用程序需要訪問數據片時,它應該始終源自這個唯一的真相數據源。有關更多信息,請參考唯一真相源。

附錄:暴露網絡狀態

在上面推薦的應用架構這一節,我們有意省略了網絡錯誤和加載狀態來讓樣例代碼簡單。在這一節,我們演示一種使用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的決策樹:

network-bound-resource.png

它從觀察資源的數據庫開始。當數據條目第一次從數據庫加載的時候,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 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做網絡請求。ApiResponseRetrofit2.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將我們的磁盤和網絡綁定User實現寫入到repository。

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();
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內容