1、Android官方架構(gòu)App體系結(jié)構(gòu)指南

App體系結(jié)構(gòu)指南

本指南適用于那些過(guò)去構(gòu)建應(yīng)用程序的基礎(chǔ)知識(shí)的開(kāi)發(fā)人員,現(xiàn)在想知道構(gòu)建強(qiáng)大的生產(chǎn)質(zhì)量應(yīng)用程序的最佳實(shí)踐和建議的體系結(jié)構(gòu)。

注意:本指南假定讀者熟悉Android框架。如果您不熟悉應(yīng)用程序開(kāi)發(fā),請(qǐng)查看 入門(mén) 培訓(xùn)系列,其中包含本指南的必備主題

應(yīng)用開(kāi)發(fā)者面臨的常見(jiàn)問(wèn)題

與傳統(tǒng)的桌面應(yīng)用程序不同,Android應(yīng)用程序的結(jié)構(gòu)要復(fù)雜得多,在大多數(shù)情況下,它們只有一個(gè)啟動(dòng)快捷方式的入口點(diǎn),并且可以作為一個(gè)整體進(jìn)程運(yùn)行。一個(gè)典型的Android應(yīng)用程序是由多個(gè)應(yīng)用程序組件構(gòu)成的,包括活動(dòng),片段,服務(wù),內(nèi)容提供者和廣播接收器。

大多數(shù)這些應(yīng)用程序組件都是在Android操作系統(tǒng)使用的應(yīng)用程序清單中聲明的,以決定如何將您的應(yīng)用程序與其設(shè)備的整體用戶(hù)體驗(yàn)相集成。雖然如前所述,桌面應(yīng)用程序傳統(tǒng)上是以整體的方式運(yùn)行的,但正確編寫(xiě)的Android應(yīng)用程序需要更加靈活,因?yàn)橛脩?hù)可以通過(guò)設(shè)備上的不同應(yīng)用程序進(jìn)行編程,不斷切換流程和任務(wù)。

例如,請(qǐng)考慮在您最喜愛(ài)的社交網(wǎng)絡(luò)應(yīng)用程序中分享照片時(shí)會(huì)發(fā)生什么情況。該應(yīng)用程序觸發(fā)Android操作系統(tǒng)啟動(dòng)相機(jī)應(yīng)用程序來(lái)處理請(qǐng)求的相機(jī)意圖。此時(shí),用戶(hù)離開(kāi)了社交網(wǎng)絡(luò)應(yīng)用,但他們的體驗(yàn)是無(wú)縫的。相機(jī)應(yīng)用程序又可能觸發(fā)其他意圖,例如啟動(dòng)文件選擇器,該文件選擇器可以啟動(dòng)另一個(gè)應(yīng)用程序。最終用戶(hù)回到社交網(wǎng)絡(luò)應(yīng)用程序并分享照片。此外,用戶(hù)在這個(gè)過(guò)程的任何時(shí)候都可能被電話(huà)打斷,并在打完電話(huà)后回來(lái)分享照片。

在Android中,這種應(yīng)用程序跳轉(zhuǎn)行為很常見(jiàn),所以您的應(yīng)用程序必須正確處理這些流程。請(qǐng)記住,移動(dòng)設(shè)備是資源受限,所以在任何時(shí)候,操作系統(tǒng)可能需要?dú)⑺酪恍?yīng)用程序,以騰出空間給新的。

所有這一切的關(guān)鍵是,您的應(yīng)用程序組件可以單獨(dú)和無(wú)序地啟動(dòng),并可以在任何時(shí)候由用戶(hù)或系統(tǒng)銷(xiāo)毀。由于應(yīng)用程序組件是短暫的,它們的生命周期(創(chuàng)建和銷(xiāo)毀時(shí))不在您的控制之下,因此您不應(yīng)該在應(yīng)用程序組件中存儲(chǔ)任何應(yīng)用程序數(shù)據(jù)或狀態(tài),并且應(yīng)用程序組件不應(yīng)相互依賴(lài)。

共同的建筑原則

如果您不能使用應(yīng)用程序組件來(lái)存儲(chǔ)應(yīng)用程序數(shù)據(jù)和狀態(tài),應(yīng)該如何構(gòu)建應(yīng)用程序?

你應(yīng)該關(guān)注的最重要的事情是在你的應(yīng)用程序中分離關(guān)注點(diǎn)。將所有的代碼寫(xiě)入一個(gè)Activity或一個(gè)常見(jiàn)的錯(cuò)誤Fragment。任何不處理UI或操作系統(tǒng)交互的代碼都不應(yīng)該在這些類(lèi)中。盡可能保持精簡(jiǎn)可以避免許多生命周期相關(guān)的問(wèn)題。不要忘記,你不擁有這些類(lèi),它們只是體現(xiàn)操作系統(tǒng)和你的應(yīng)用程序之間的契約的膠水類(lèi)。Android操作系統(tǒng)可能會(huì)隨時(shí)根據(jù)用戶(hù)交互或其他因素(如低內(nèi)存)來(lái)銷(xiāo)毀它們。最好最大限度地減少對(duì)他們的依賴(lài),以提供可靠的用戶(hù)體驗(yàn)。

第二個(gè)重要的原則是你應(yīng)該從一個(gè)模型驅(qū)動(dòng)你的UI,最好是一個(gè)持久模型。持久性是理想的,原因有兩個(gè):如果操作系統(tǒng)破壞您的應(yīng)用程序以釋放資源,則您的用戶(hù)不會(huì)丟失數(shù)據(jù),即使網(wǎng)絡(luò)連接不穩(wěn)定或連接不上,您的應(yīng)用程序也將繼續(xù)工作。模型是負(fù)責(zé)處理應(yīng)用程序數(shù)據(jù)的組件。它們獨(dú)立于應(yīng)用程序中的視圖和應(yīng)用程序組件,因此它們與這些組件的生命周期問(wèn)題是隔離的。保持簡(jiǎn)單的UI代碼和免費(fèi)的應(yīng)用程序邏輯,使管理更容易。將您的應(yīng)用程序放在模型類(lèi)上,具有明確的數(shù)據(jù)管理責(zé)任將使他們可測(cè)試,并使您的應(yīng)用程序保持一致。

推薦的應(yīng)用架構(gòu)

在本節(jié)中,我們將演示如何通過(guò)使用用例來(lái)構(gòu)建使用體系結(jié)構(gòu)組件的應(yīng)用程序。

注意:不可能有一種編寫(xiě)應(yīng)用程序的方法,這對(duì)每種情況都是最好的。這就是說(shuō),這個(gè)推薦的架構(gòu)應(yīng)該是大多數(shù)用例的一個(gè)很好的起點(diǎn)。如果您已經(jīng)有了編寫(xiě)Android應(yīng)用程序的好方法,則不需要更改。

想象一下,我們正在構(gòu)建一個(gè)顯示用戶(hù)配置文件的用戶(hù)界面。該用戶(hù)配置文件將使用REST API從我們自己的私人后端獲取。

構(gòu)建用戶(hù)界面

UI將由一個(gè)片段UserProfileFragment.java及其相應(yīng)的布局文件組成user_profile_layout.xml。

為了驅(qū)動(dòng)用戶(hù)界面,我們的數(shù)據(jù)模型需要保存兩個(gè)數(shù)據(jù)元素。

  • 用戶(hù)ID:用戶(hù)的標(biāo)識(shí)符。最好使用片段參數(shù)將此信息傳遞到片段中。如果Android操作系統(tǒng)破壞您的進(jìn)程,這些信息將被保留,以便在您的應(yīng)用下次重新啟動(dòng)時(shí)可用。
  • 用戶(hù)對(duì)象:保存用戶(hù)數(shù)據(jù)的POJO。

我們將創(chuàng)建一個(gè)UserProfileViewModel基于ViewModel的類(lèi)來(lái)保存這些信息。

甲視圖模型提供了一個(gè)特定的UI組件中的數(shù)據(jù),如一個(gè)片段或活性,和處理與數(shù)據(jù)處理的部分業(yè)務(wù),如主叫其他組件加載數(shù)據(jù)或轉(zhuǎn)發(fā)的用戶(hù)修改的通信。ViewModel不知道視圖,并且不受配置更改的影響,例如由于旋轉(zhuǎn)而重新創(chuàng)建活動(dòng)。

現(xiàn)在我們有3個(gè)文件。

  • user_profile.xml:屏幕的UI定義。
  • UserProfileViewModel.java:為UI準(zhǔn)備數(shù)據(jù)的類(lèi)。
  • UserProfileFragment.java:在ViewModel中顯示數(shù)據(jù)并對(duì)用戶(hù)交互作出反應(yīng)的UI控制器。

下面是我們的開(kāi)始的實(shí)現(xiàn)(布局文件為簡(jiǎn)單起見(jiàn)被省略):

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

現(xiàn)在,我們有這三個(gè)代碼模塊,我們?nèi)绾芜B接它們?畢竟,當(dāng)ViewModel的用戶(hù)字段被設(shè)置,我們需要一種方式來(lái)通知用戶(hù)界面。這是LiveData類(lèi)的地方。

LiveData是一個(gè)可觀察的數(shù)據(jù)持有者。它允許應(yīng)用程序中的組件觀察LiveData 對(duì)象的更改,而不會(huì)在它們之間創(chuàng)建明確的和嚴(yán)格的依賴(lài)關(guān)系路徑。LiveData還尊重您的應(yīng)用程序組件(活動(dòng),片段,服務(wù))的生命周期狀態(tài),并做正確的事情來(lái)防止對(duì)象泄漏,使您的應(yīng)用程序不會(huì)消耗更多的內(nèi)存。

注意:如果您已經(jīng)在使用類(lèi)似RxJava或 Agera的庫(kù) ,則可以繼續(xù)使用它們而不是LiveData。但是,當(dāng)您使用它們或其他方法時(shí),請(qǐng)確保正確處理生命周期,以便在相關(guān)的LifecycleOwner停止時(shí)停止數(shù)據(jù)流,并在銷(xiāo)毀LifecycleOwner時(shí)銷(xiāo)毀數(shù)據(jù)流。您還可以添加 android.arch.lifecycle:reactivestreams工件以將LiveData與另一個(gè)反應(yīng)流庫(kù)(例如RxJava2)一起使用。

現(xiàn)在我們用UserProfileViewModel 替換User字段,以便在數(shù)據(jù)更新時(shí)通知fragment。重要的 是,它是生命周期感知,當(dāng)我們長(zhǎng)時(shí)間不用將自動(dòng)清理

public class UserProfileViewModel extends ViewModel {
    ...
    //private User user;
    private LiveData<User> user;//增加此行
    public LiveData<User> getUser() {
        return user;
    }
}

現(xiàn)在我們修改UserProfileFragment觀察數(shù)據(jù)并更新UI。

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

每次更新用戶(hù)數(shù)據(jù)時(shí), 都會(huì)調(diào)用onChanged回調(diào),并刷新UI。

如果您熟悉使用可觀察回調(diào)的其他庫(kù),您可能已經(jīng)意識(shí)到,我們不必重寫(xiě)片段的onStop()方法來(lái)停止觀察數(shù)據(jù)。這對(duì)于LiveData來(lái)說(shuō)是不必要的,因?yàn)樗巧芷诟兄模@意味著它不會(huì)調(diào)用回調(diào),除非片段處于活動(dòng)狀態(tài)(已接收onStart()但未接收onStop())。當(dāng)數(shù)據(jù)片段收到時(shí),LiveData也會(huì)自動(dòng)移除觀察者onDestroy()。

我們也沒(méi)有做任何特殊的處理配置變化(例如,用戶(hù)旋轉(zhuǎn)屏幕)。當(dāng)配置改變時(shí),ViewModel會(huì)自動(dòng)恢復(fù),所以一旦新的片段生效,它將接收到相同的ViewModel實(shí)例,并且回調(diào)將被當(dāng)前數(shù)據(jù)立即調(diào)用。這就是ViewModel不能直接引用Views的原因。他們可以超越View的生命周期。請(qǐng)參閱 ViewModel的生命周期。

正在提取數(shù)據(jù)

現(xiàn)在我們已經(jīng)將ViewModel連接到了片段,但是ViewModel如何獲取用戶(hù)數(shù)據(jù)呢?在這個(gè)例子中,我們假設(shè)我們的后端提供了一個(gè)REST API。我們將使用 Retrofit庫(kù)來(lái)訪(fǎng)問(wèn)我們的后端,盡管您可以自由使用不同的庫(kù)來(lái)達(dá)到同樣的目的。

以下是我們Webservice與后端進(jìn)行通信的改造:

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

即使存儲(chǔ)庫(kù)模塊看起來(lái)沒(méi)有必要,它也是一個(gè)重要的目的。它從應(yīng)用程序的其余部分提取數(shù)據(jù)源。現(xiàn)在我們的ViewModel不知道數(shù)據(jù)是由Webservice哪個(gè)取得的,這意味著我們可以根據(jù)需要將它交換為其他的實(shí)現(xiàn)。

注意:為了簡(jiǎn)單起見(jiàn),我們忽略了網(wǎng)絡(luò)錯(cuò)誤的情況。有關(guān)公開(kāi)錯(cuò)誤和加載狀態(tài)的替代實(shí)現(xiàn),請(qǐng)參閱 附錄:公開(kāi)網(wǎng)絡(luò)狀態(tài)。

管理組件之間的依賴(lài)關(guān)系:
UserRepository上面的類(lèi)需要一個(gè)實(shí)例Webservice來(lái)完成它的工作。它可以簡(jiǎn)單地創(chuàng)建它,但要做到這一點(diǎn),它也需要知道Webservice類(lèi)的依賴(lài)關(guān)系來(lái)構(gòu)造它。這會(huì)使代碼復(fù)雜化和復(fù)制(例如,每個(gè)需要Webservice實(shí)例的類(lèi) 都需要知道如何用它的依賴(lài)來(lái)構(gòu)造它)。另外,UserRepository可能不是唯一需要的類(lèi)Webservice。如果每個(gè)班級(jí)創(chuàng)建一個(gè)新的WebService,這將是非常重的資源。

有兩種模式可以用來(lái)解決這個(gè)問(wèn)題:

  • 依賴(lài)注入:依賴(lài)注入允許類(lèi)在不構(gòu)造它們的情況下定義它們的依賴(lài)關(guān)系。在運(yùn)行時(shí),另一個(gè)類(lèi)負(fù)責(zé)提供這些依賴(lài)關(guān)系。我們推薦Google的Dagger 2庫(kù)在Android應(yīng)用程序中實(shí)現(xiàn)依賴(lài)注入。Dagger 2通過(guò)遍歷依賴(lài)關(guān)系樹(shù)來(lái)自動(dòng)構(gòu)造對(duì)象,并為依賴(lài)關(guān)系提供編譯時(shí)間保證。
  • 服務(wù)定位器:服務(wù)定位器提供了一個(gè)注冊(cè)表,類(lèi)可以獲得它們的依賴(lài)而不是構(gòu)建它們。實(shí)現(xiàn)起來(lái)比依賴(lài)注入(DI)更容易,所以如果你不熟悉DI,可以使用Service Locator。

在這個(gè)例子中,我們將使用Dagger 2來(lái)管理依賴(lài)關(guān)系。

連接ViewModel和存儲(chǔ)庫(kù)

現(xiàn)在我們修改我們UserProfileViewModel的存儲(chǔ)庫(kù)。

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

緩存數(shù)據(jù)

上面的存儲(chǔ)庫(kù)實(shí)現(xiàn)對(duì)抽象調(diào)用Web服務(wù)是有好處的,但是因?yàn)樗灰蕾?lài)于一個(gè)數(shù)據(jù)源,所以它不是很實(shí)用。

UserRepository上面的實(shí)現(xiàn)的問(wèn)題是,在獲取數(shù)據(jù)之后,它不保存在任何地方。如果用戶(hù)離開(kāi) UserProfileFragment并返回,應(yīng)用程序?qū)⒅匦芦@取數(shù)據(jù)。這是不好的,原因有兩個(gè):浪費(fèi)寶貴的網(wǎng)絡(luò)帶寬并強(qiáng)制用戶(hù)等待新的查詢(xún)完成。為了解決這個(gè)問(wèn)題,我們將添加一個(gè)新的數(shù)據(jù)源,我們UserRepository將緩存User內(nèi)存中的對(duì)象。

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

持久化數(shù)據(jù)

在我們當(dāng)前的實(shí)現(xiàn)中,如果用戶(hù)旋轉(zhuǎn)屏幕或離開(kāi)并返回到應(yīng)用程序,則現(xiàn)有UI將立即可見(jiàn),因?yàn)榇鎯?chǔ)庫(kù)從內(nèi)存中高速緩存中檢索數(shù)據(jù)。但是,如果用戶(hù)離開(kāi)應(yīng)用程序,并在Android操作系統(tǒng)殺死該進(jìn)程后數(shù)小時(shí)后回來(lái),會(huì)發(fā)生什么?

在目前的實(shí)施中,我們將需要從網(wǎng)絡(luò)上重新獲取數(shù)據(jù)。這不僅是一個(gè)糟糕的用戶(hù)體驗(yàn),而且會(huì)浪費(fèi),因?yàn)樗鼤?huì)使用移動(dòng)數(shù)據(jù)重新獲取相同的數(shù)據(jù)。您可以簡(jiǎn)單地通過(guò)緩存Web請(qǐng)求來(lái)解決這個(gè)問(wèn)題,但是會(huì)產(chǎn)生新的問(wèn)題。如果相同的用戶(hù)數(shù)據(jù)顯示來(lái)自另一種類(lèi)型的請(qǐng)求(例如,獲取朋友列表),會(huì)發(fā)生什么情況?那么你的應(yīng)用程序可能會(huì)顯示不一致的數(shù)據(jù),這是一個(gè)混亂的用戶(hù)體驗(yàn)充其量。例如,由于好友列表請(qǐng)求和用戶(hù)請(qǐng)求可以在不同的時(shí)間執(zhí)行,所以相同用戶(hù)的數(shù)據(jù)可能會(huì)以不同的方式顯示。您的應(yīng)用需要合并它們以避免顯示不一致的數(shù)據(jù)。

處理這個(gè)問(wèn)題的正確方法是使用持久模型。這是 Room 持久性庫(kù)來(lái)救援的地方。

Room 是一個(gè)對(duì)象映射庫(kù),提供本地?cái)?shù)據(jù)持久性和最小的樣板代碼。在編譯時(shí),它會(huì)根據(jù)模式驗(yàn)證每個(gè)查詢(xún),以便斷開(kāi)的SQL查詢(xún)導(dǎo)致編譯時(shí)錯(cuò)誤,而不是運(yùn)行時(shí)失敗。會(huì)議室抽象出一些使用原始SQL表和查詢(xún)的底層實(shí)現(xiàn)細(xì)節(jié)。它還允許觀察對(duì)數(shù)據(jù)庫(kù)數(shù)據(jù)(包括集合和連接查詢(xún))的更改,通過(guò)LiveData對(duì)象公開(kāi)這些更改 。另外,它明確定義了解決常見(jiàn)問(wèn)題的線(xiàn)程約束,例如訪(fǎng)問(wèn)主線(xiàn)程上的存儲(chǔ)。

注意:如果您的應(yīng)用程序已經(jīng)使用另一個(gè)持久性解決方案(如SQLite對(duì)象關(guān)系映射(ORM)),則不需要使用Room替換現(xiàn)有的解決方案。但是,如果您正在編寫(xiě)新的應(yīng)用程序或重構(gòu)現(xiàn)有的應(yīng)用程序,我們建議使用Room來(lái)保存應(yīng)用程序的數(shù)據(jù)。這樣,您可以利用庫(kù)的抽象和查詢(xún)驗(yàn)證功能。

要使用Room,我們需要定義我們的本地模式。首先,注釋User 該類(lèi)以@Entity 將其標(biāo)記為數(shù)據(jù)庫(kù)中的表。

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

然后,通過(guò)擴(kuò)展 RoomDatabase 您的應(yīng)用程序來(lái)創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)類(lèi) :

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

注意這MyDatabase是抽象的。房間自動(dòng)提供一個(gè)實(shí)施。有關(guān)詳細(xì)信息,請(qǐng)參見(jiàn)房間文檔

現(xiàn)在我們需要一種將用戶(hù)數(shù)據(jù)插入數(shù)據(jù)庫(kù)的方法。為此,我們將創(chuàng)建一個(gè)數(shù)據(jù)訪(fǎng)問(wèn)對(duì)象(DAO)。

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

然后,從我們的數(shù)據(jù)庫(kù)類(lèi)中引用DAO。

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

請(qǐng)注意,該load方法返回一個(gè)LiveData<User>。Room知道數(shù)據(jù)庫(kù)何時(shí)被修改,當(dāng)數(shù)據(jù)改變時(shí)它會(huì)自動(dòng)通知所有活動(dòng)的觀察者。因?yàn)樗褂玫氖荓iveData,所以這將是有效的,因?yàn)樗粫?huì)在至少有一個(gè)活動(dòng)觀察者的情況下更新數(shù)據(jù)。

注意:Room根據(jù)表格修改檢查失效,這意味著它可能發(fā)送誤報(bào)通知。

現(xiàn)在我們可以修改我們UserRepository來(lái)合并房間數(shù)據(jù)源。

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

請(qǐng)注意,盡管我們改變了數(shù)據(jù)來(lái)自于 UserRepository,我們并不需要改變我們UserProfileViewModel或 UserProfileFragment。這是抽象提供的靈活性。這對(duì)于測(cè)試來(lái)說(shuō)也很棒,因?yàn)槟憧梢訳serRepository 在測(cè)試你的時(shí)候提供一個(gè)假的UserProfileViewModel。

現(xiàn)在我們的代碼是完整的。如果用戶(hù)以后回到相同的用戶(hù)界面,他們會(huì)立即看到用戶(hù)信息,因?yàn)槲覀兂志没?。同時(shí),如果數(shù)據(jù)陳舊,我們的倉(cāng)庫(kù)將在后臺(tái)更新數(shù)據(jù)。當(dāng)然,根據(jù)您的使用情況,如果數(shù)據(jù)太舊,您可能不希望顯示持久數(shù)據(jù)。

在一些使用情況下,如拉到刷新,UI顯示用戶(hù)是否正在進(jìn)行網(wǎng)絡(luò)操作是非常重要的。將UI操作與實(shí)際數(shù)據(jù)分開(kāi)是一種很好的做法,因?yàn)樗赡芤蚋鞣N原因而更新(例如,如果我們獲取朋友列表,可能會(huì)再次觸發(fā)同一用戶(hù)觸發(fā)LiveData<User>更新)。從用戶(hù)界面的角度來(lái)看,有一個(gè)請(qǐng)求在飛行的事實(shí)只是另一個(gè)數(shù)據(jù)點(diǎn),類(lèi)似于任何其他數(shù)據(jù)(如User對(duì)象)。

這個(gè)用例有兩種常見(jiàn)的解決方案:

  • 更改getUser為返回包含網(wǎng)絡(luò)操作狀態(tài)的LiveData。附錄中提供了一個(gè)示例實(shí)現(xiàn):公開(kāi)網(wǎng)絡(luò)狀態(tài)部分
  • 在存儲(chǔ)庫(kù)類(lèi)中提供另一個(gè)可以返回用戶(hù)刷新?tīng)顟B(tài)的公共函數(shù)。如果只想響應(yīng)顯式的用戶(hù)操作(如拉到刷新)來(lái)顯示網(wǎng)絡(luò)狀態(tài),則此選項(xiàng)更好。
單一的事實(shí)來(lái)源

不同的REST API端點(diǎn)通常返回相同的數(shù)據(jù)。例如,如果我們的后端擁有另一個(gè)返回朋友列表的端點(diǎn),則同一個(gè)用戶(hù)對(duì)象可能來(lái)自?xún)蓚€(gè)不同的API端點(diǎn),也許粒度不同。如果原樣UserRepository返回Webservice請(qǐng)求的響應(yīng),我們的UI可能會(huì)顯示不一致的數(shù)據(jù),因?yàn)樵谶@些請(qǐng)求之間數(shù)據(jù)可能在服務(wù)器端發(fā)生更改。這就是為什么在UserRepository實(shí)現(xiàn)中,Web服務(wù)回調(diào)只是將數(shù)據(jù)保存到數(shù)據(jù)庫(kù)中。然后,對(duì)數(shù)據(jù)庫(kù)的更改將觸發(fā)活動(dòng)LiveData對(duì)象上的回調(diào)。

在這個(gè)模型中,數(shù)據(jù)庫(kù)充當(dāng)真相的單一來(lái)源,應(yīng)用程序的其他部分通過(guò)存儲(chǔ)庫(kù)訪(fǎng)問(wèn)它。無(wú)論您使用磁盤(pán)緩存,我們都建議您的存儲(chǔ)庫(kù)將數(shù)據(jù)源指定為應(yīng)用程序其余部分的單一來(lái)源。

測(cè)試

我們已經(jīng)提到分離的好處之一就是可測(cè)試性。讓我們看看我們?nèi)绾螠y(cè)試每個(gè)代碼模塊。

  • 用戶(hù)界面和交互:這將是您唯一需要進(jìn)行 Android UI Instrumentation測(cè)試的時(shí)間。測(cè)試UI代碼的最好方法是創(chuàng)建一個(gè) Espresso測(cè)試。您可以創(chuàng)建片段并為其提供一個(gè)模擬的ViewModel。由于該片段只與ViewModel交談,所以嘲笑它將足以完全測(cè)試這個(gè)UI。

  • ViewModel:可以使用JUnit測(cè)試來(lái)測(cè)試ViewModel 。你只需要嘲笑UserRepository測(cè)試它。

  • UserRepository:你也可以UserRepository用JUnit測(cè)試來(lái)測(cè)試。你需要嘲笑Webservice和DAO。您可以測(cè)試它是否做出正確的Web服務(wù)調(diào)用,將結(jié)果保存到數(shù)據(jù)庫(kù)中,如果數(shù)據(jù)已緩存且最新,則不會(huì)發(fā)出任何不必要的請(qǐng)求。由于這兩個(gè)Webservice和UserDao的界面,你可以嘲笑他們或創(chuàng)建更復(fù)雜的測(cè)試案例假冒實(shí)現(xiàn)..

  • UserDao:測(cè)試DAO類(lèi)的推薦方法是使用儀器測(cè)試。由于這些儀器測(cè)試不需要任何用戶(hù)界面,他們?nèi)匀粫?huì)運(yùn)行得很快。對(duì)于每個(gè)測(cè)試,您可以創(chuàng)建一個(gè)內(nèi)存數(shù)據(jù)庫(kù),以確保測(cè)試沒(méi)有任何副作用(如更改磁盤(pán)上的數(shù)據(jù)庫(kù)文件)。

  • Room還允許指定數(shù)據(jù)庫(kù)的實(shí)現(xiàn),所以你可以通過(guò)提供JUnit實(shí)現(xiàn)來(lái)測(cè)試它 SupportSQLiteOpenHelper。通常不建議使用這種方法,因?yàn)樵O(shè)備上運(yùn)行的SQLite版本可能與主機(jī)上的SQLite版本不同。

  • Webservice:使測(cè)試獨(dú)立于外部是很重要的,所以即使你的Webservice測(cè)試也應(yīng)該避免對(duì)后端進(jìn)行網(wǎng)絡(luò)調(diào)用。有很多圖書(shū)館可以幫助你。例如, MockWebServer 是一個(gè)偉大的庫(kù),可以幫助您為測(cè)試創(chuàng)建一個(gè)假的本地服務(wù)器。

  • 測(cè)試工件體系結(jié)構(gòu)組件提供了一個(gè)Maven工件來(lái)控制其后臺(tái)線(xiàn)程。在android.arch.core:core-testing神器內(nèi)部 ,有2個(gè)JUnit規(guī)則:

    • InstantTaskExecutorRule:此規(guī)則可用于強(qiáng)制架構(gòu)組件立即在調(diào)用線(xiàn)程上執(zhí)行任何后臺(tái)操作。

    • CountingTaskExecutorRule:此規(guī)則可用于檢測(cè)測(cè)試,以等待體系結(jié)構(gòu)組件的后臺(tái)操作或?qū)⑵渥鳛殚e置資源連接到Espresso。

最終的體系結(jié)構(gòu)

下圖顯示了我們推薦的體系結(jié)構(gòu)中的所有模塊以及它們?nèi)绾蜗嗷ソ换ィ?/p>

1111.png

指導(dǎo)原則

編程是一個(gè)創(chuàng)造性的領(lǐng)域,構(gòu)建Android應(yīng)用程序不是一個(gè)例外。解決問(wèn)題的方法有很多種,可以在多個(gè)活動(dòng)或片段之間傳遞數(shù)據(jù),檢索遠(yuǎn)程數(shù)據(jù)并將其保存在本地以進(jìn)行脫機(jī)模式,也可以使用許多其他常見(jiàn)應(yīng)用程序遇到的情況。

雖然以下建議不是強(qiáng)制性的,但是我們的經(jīng)驗(yàn)是,遵循這些建議將使您的代碼基礎(chǔ)更加健壯,可測(cè)試和可維護(hù)。

  • 您在清單中定義的入口點(diǎn)(活動(dòng),服務(wù),廣播接收器等)不是數(shù)據(jù)的來(lái)源。相反,他們只應(yīng)該協(xié)調(diào)與該入口點(diǎn)相關(guān)的數(shù)據(jù)子集。由于每個(gè)應(yīng)用程序組件的壽命相當(dāng)短,這取決于用戶(hù)與設(shè)備的交互以及運(yùn)行時(shí)的整體當(dāng)前運(yùn)行狀況,因此您不希望這些入口點(diǎn)中的任何一個(gè)成為數(shù)據(jù)源。

  • 無(wú)情地在應(yīng)用程序的各個(gè)模塊之間創(chuàng)建明確界定的責(zé)任。例如,不要將從網(wǎng)絡(luò)加載數(shù)據(jù)的代碼跨代碼庫(kù)中的多個(gè)類(lèi)或包傳播。同樣,不要把不相關(guān)的職責(zé) - 比如數(shù)據(jù)緩存和數(shù)據(jù)綁定 - 放到同一個(gè)類(lèi)中。

  • 盡可能少地從每個(gè)模塊公開(kāi)。不要試圖創(chuàng)建“只有那一個(gè)”的快捷方式,從一個(gè)模塊公開(kāi)內(nèi)部實(shí)現(xiàn)細(xì)節(jié)。您可能在短期內(nèi)獲得一些時(shí)間,但隨著您的代碼庫(kù)的發(fā)展,您將多次支付技術(shù)債務(wù)。

  • 在定義模塊之間的交互時(shí),請(qǐng)考慮如何使每個(gè)模塊獨(dú)立地進(jìn)行測(cè)試。例如,如果有一個(gè)定義良好的API從網(wǎng)絡(luò)中獲取數(shù)據(jù),將會(huì)更容易測(cè)試將數(shù)據(jù)保存在本地?cái)?shù)據(jù)庫(kù)中的模塊。相反,如果將這兩個(gè)模塊的邏輯混合在一起,或者在整個(gè)代碼庫(kù)中撒上網(wǎng)絡(luò)代碼,那么要測(cè)試就更加困難了。

  • 你的應(yīng)用程序的核心是什么讓它從其他中脫穎而出。不要花費(fèi)時(shí)間重復(fù)發(fā)明輪子,或者一次又一次地寫(xiě)出相同的樣板代碼。相反,將精力集中在讓您的應(yīng)用獨(dú)特的東西上,讓Android Architecture組件和其他推薦的庫(kù)處理重復(fù)的樣板。

  • 堅(jiān)持盡可能多的相關(guān)和新鮮的數(shù)據(jù),以便您的應(yīng)用程序在設(shè)備處于離線(xiàn)模式時(shí)可用。雖然您可以享受持續(xù)高速的連接,但用戶(hù)可能不會(huì)。

  • 您的存儲(chǔ)庫(kù)應(yīng)該指定一個(gè)數(shù)據(jù)源作為單一的事實(shí)來(lái)源。無(wú)論何時(shí)您的應(yīng)用程序需要訪(fǎng)問(wèn)這些數(shù)據(jù),都應(yīng)始終從單一的事實(shí)源頭開(kāi)始。有關(guān)更多信息,請(qǐng)參閱單一來(lái)源的真相。

附錄:揭露網(wǎng)絡(luò)狀態(tài)

在上面推薦的應(yīng)用程序體系結(jié)構(gòu)部分,我們故意省略網(wǎng)絡(luò)錯(cuò)誤和加載狀態(tài),以保持樣本簡(jiǎn)單。在本節(jié)中,我們演示一種使用Resource類(lèi)來(lái)公開(kāi)網(wǎng)絡(luò)狀態(tài)的方法來(lái)封裝數(shù)據(jù)及其狀態(tài)。

以下是一個(gè)示例實(shí)現(xiàn):

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

因?yàn)樵趶拇疟P(pán)顯示數(shù)據(jù)時(shí)從網(wǎng)絡(luò)加載數(shù)據(jù)是一個(gè)常見(jiàn)的用例,我們將創(chuàng)建一個(gè)NetworkBoundResource可以在多個(gè)地方重復(fù)使用的幫助類(lèi)。以下是決策樹(shù) NetworkBoundResource:

2222.png

它通過(guò)觀察資源的數(shù)據(jù)庫(kù)開(kāi)始。當(dāng)條目從數(shù)據(jù)庫(kù)中第一次被加載時(shí),NetworkBoundResource檢查結(jié)果是否足夠好以便被分派和/或它應(yīng)該從網(wǎng)絡(luò)中獲取。請(qǐng)注意,這兩種情況可能同時(shí)發(fā)生,因?yàn)槟赡芟M趶木W(wǎng)絡(luò)更新緩存數(shù)據(jù)時(shí)顯示緩存的數(shù)據(jù)。

如果網(wǎng)絡(luò)呼叫成功完成,則將響應(yīng)保存到數(shù)據(jù)庫(kù)中并重新初始化流。如果網(wǎng)絡(luò)請(qǐng)求失敗,我們直接發(fā)送失敗。

注意:在將新數(shù)據(jù)保存到磁盤(pán)之后,我們會(huì)重新初始化數(shù)據(jù)庫(kù)中的數(shù)據(jù)流,但通常我們不需要這樣做,因?yàn)閿?shù)據(jù)庫(kù)將分派更改。另一方面,依靠數(shù)據(jù)庫(kù)來(lái)調(diào)度變化將依賴(lài)于不好的副作用,因?yàn)槿绻麛?shù)據(jù)沒(méi)有變化,數(shù)據(jù)庫(kù)可以避免調(diào)度變化,那么它可能會(huì)中斷。我們也不想派遣從網(wǎng)絡(luò)到達(dá)的結(jié)果,因?yàn)檫@將違背單一的事實(shí)來(lái)源(也許在數(shù)據(jù)庫(kù)中有觸發(fā)器會(huì)改變保存的值)。我們也不希望SUCCESS沒(méi)有新的數(shù)據(jù),因?yàn)樗鼤?huì)向客戶(hù)發(fā)送錯(cuò)誤的信息。

以下是NetworkBoundResource班級(jí)為其子女提供的公共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, implemented
    // in the base class.
    public final LiveData<Resource<ResultType>> getAsLiveData();
}

請(qǐng)注意,上面的類(lèi)定義了兩個(gè)類(lèi)型參數(shù)(ResultType, RequestType),因?yàn)閺腁PI返回的數(shù)據(jù)類(lèi)型可能與本地使用的數(shù)據(jù)類(lèi)型不匹配。

另請(qǐng)注意,上面的代碼ApiResponse用于網(wǎng)絡(luò)請(qǐng)求。 ApiResponse是一個(gè)簡(jiǎn)單的Retrofit2.Call類(lèi)包裝,將其響應(yīng)轉(zhuǎn)換為L(zhǎng)iveData。

以下是該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;
    }
}

現(xiàn)在,我們可以使用它NetworkBoundResource來(lái)User在存儲(chǔ)庫(kù)中寫(xiě)入我們的磁盤(pán)和網(wǎng)絡(luò)綁定 實(shí)現(xiàn)。

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

請(qǐng)點(diǎn)贊!因?yàn)槟愕墓膭?lì)是我寫(xiě)作的最大動(dòng)力!

歡迎關(guān)注 Lee-Wang 的簡(jiǎn)書(shū)!

不定期分享關(guān)于安卓開(kāi)發(fā)的干貨,讓堅(jiān)持成為一種習(xí)慣
冰凍三尺,非一日之寒
人生最可悲的事,莫過(guò)于胸懷大志,卻又虛度光陰!!!
不要在該奮斗的年紀(jì)選擇了安逸
生命不息,奮斗不止,萬(wàn)事起于忽微,量變引起質(zhì)變
每當(dāng)你在感嘆,如果有這樣一個(gè)東西就好了的時(shí)候,請(qǐng)注意,其實(shí)這是你的機(jī)會(huì)
人不會(huì)死在絕境,卻往往栽在十字路口
知道+做到=得到

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容