Android應(yīng)用架構(gòu)指南

這篇指南源自于Google開發(fā)文檔,有興趣的朋友去官網(wǎng)進行查看。我們不生產(chǎn)代碼,我們只是代碼的搬運工!
本指南包含一些最佳做法和推薦架構(gòu),有助于構(gòu)建強大而優(yōu)質(zhì)的應(yīng)用。

移動應(yīng)用用戶體驗

在大多數(shù)情況下,桌面應(yīng)用會在桌面或程序啟動器中有一個入口點,且作為一個單體式進程運行。Android 應(yīng)用則不然,它們的結(jié)構(gòu)要復(fù)雜得多。典型的 Android 應(yīng)用包含多個應(yīng)用組件,包括 ActivityFragment、Service、ContentProvider廣播接收器。
您需要在應(yīng)用清單中聲明其中的大多數(shù)應(yīng)用組件。Android 操作系統(tǒng)隨后會使用此文件來決定如何將您的應(yīng)用集成到設(shè)備的整體用戶體驗中。鑒于正確編寫的 Android 應(yīng)用包含多個組件,并且用戶經(jīng)常會在短時間內(nèi)與多個應(yīng)用進行互動,因此應(yīng)用需要適應(yīng)不同類型的用戶驅(qū)動型工作流和任務(wù)。
例如,思考一下當(dāng)您在自己喜歡的社交網(wǎng)絡(luò)應(yīng)用中分享照片時會發(fā)生什么:

  • 該應(yīng)用將觸發(fā)相機 intent。Android 操作系統(tǒng)隨后會啟動相機應(yīng)用來處理請求。此時,用戶已離開社交網(wǎng)絡(luò)應(yīng)用,但他們的體驗仍然是無縫的。
  • 相機應(yīng)用可能會觸發(fā)其他 intent(如啟動文件選擇器),而這可能會再啟動一個應(yīng)用。
  • 最后,用戶返回社交網(wǎng)絡(luò)應(yīng)用并分享照片。

在此過程中,用戶隨時可能會被電話或通知打斷。處理之后,用戶希望能夠返回并繼續(xù)分享照片。這種應(yīng)用跳躍行為在移動設(shè)備上很常見,因此您的應(yīng)用必須正確處理這些流程。
請注意,移動設(shè)備的資源也很有限,因此操作系統(tǒng)可能會隨時終止某些應(yīng)用進程,以便為新的進程騰出空間。
鑒于這種環(huán)境條件,您的應(yīng)用組件可以不按順序地單獨啟動,并且操作系統(tǒng)或用戶可以隨時銷毀它們。由于這些事件不受您的控制,因此您不應(yīng)在應(yīng)用組件中存儲任何應(yīng)用數(shù)據(jù)或狀態(tài),并且應(yīng)用組件不應(yīng)相互依賴。

常見的架構(gòu)原則

如果您不應(yīng)使用應(yīng)用組件存儲應(yīng)用數(shù)據(jù)和狀態(tài),那么您應(yīng)該如何設(shè)計應(yīng)用呢?

分離關(guān)注點

要遵循的最重要的原則是分離關(guān)注點。一種常見的錯誤是在一個 ActivityFragment 中編寫所有代碼。這些基于界面的類應(yīng)僅包含處理界面和操作系統(tǒng)交互的邏輯。您應(yīng)使這些類盡可能保持精簡,這樣可以避免許多與生命周期相關(guān)的問題。
請注意,您并非擁有 Activity 和 Fragment 的實現(xiàn);它們只是表示 Android 操作系統(tǒng)與應(yīng)用之間關(guān)系的粘合類。操作系統(tǒng)可能會根據(jù)用戶互動或因內(nèi)存不足等系統(tǒng)條件隨時銷毀它們。為了提供令人滿意的用戶體驗和更易于管理的應(yīng)用維護體驗,您最好盡量減少對它們的依賴。

通過模型驅(qū)動界面

另一個重要原則是您應(yīng)該通過模型驅(qū)動界面(最好是持久性模型)。模型是負責(zé)處理應(yīng)用數(shù)據(jù)的組件。它們獨立于應(yīng)用中的 View對象和應(yīng)用組件,因此不受應(yīng)用的生命周期以及相關(guān)的關(guān)注點的影響。
持久性是理想之選,原因如下:

  • 如果 Android 操作系統(tǒng)銷毀應(yīng)用以釋放資源,用戶不會丟失數(shù)據(jù)。
  • 當(dāng)網(wǎng)絡(luò)連接不穩(wěn)定或不可用時,應(yīng)用會繼續(xù)工作。

應(yīng)用所基于的模型類應(yīng)明確定義數(shù)據(jù)管理職責(zé),這樣將使應(yīng)用更可測試且更一致。

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

在本部分,我們將通過一個端到端用例演示如何使用架構(gòu)組件構(gòu)建應(yīng)用。
注意:任何應(yīng)用編寫方式都不可能是每種情況的最佳選擇。話雖如此,但推薦的這個架構(gòu)是個不錯的起點,適合大多數(shù)情況和工作流。如果您已經(jīng)有編寫 Android 應(yīng)用的好方法(遵循常見的架構(gòu)原則),則無需更改。
假設(shè)我們要構(gòu)建一個用于顯示用戶個人資料的界面。我們使用私有后端和 REST API 獲取給定個人資料的數(shù)據(jù)。

概覽

首先,請查看下圖,該圖顯示了設(shè)計應(yīng)用后所有模塊應(yīng)如何彼此交互:


architecture.png

請注意,每個組件僅依賴于其下一級的組件。例如,Activity 和 Fragment 僅依賴于視圖模型。存儲區(qū)是唯一依賴于其他多個類的類;在本例中,存儲區(qū)依賴于持久性數(shù)據(jù)模型和遠程后端數(shù)據(jù)源。
這種設(shè)計打造了一致且愉快的用戶體驗。無論用戶上次使用應(yīng)用是在幾分鐘前還是幾天之前,現(xiàn)在回到應(yīng)用時都會立即看到應(yīng)用在本地保留的用戶信息。如果此數(shù)據(jù)已過時,則應(yīng)用的存儲區(qū)模塊將開始在后臺更新數(shù)據(jù)。

構(gòu)建界面

界面由 Fragment UserProfileFragment 及其對應(yīng)的布局文件 user_profile_layout.xml 組成。
如需驅(qū)動該界面,數(shù)據(jù)模型需要存儲以下數(shù)據(jù)元素:

  • 用戶 ID:用戶的標(biāo)識符。最好使用 Fragment 參數(shù)將此類信息傳遞到相應(yīng) Fragment 中。如果 Android 操作系統(tǒng)銷毀我們的進程,此類信息將保留,以便下次重啟應(yīng)用時 ID 可用。
  • 用戶對象:用于存儲用戶詳細信息的數(shù)據(jù)類。

我們將使用 UserProfileViewModel(基于 ViewModel 架構(gòu)組件)存儲這些信息。

ViewModel 對象為特定的界面組件(如 Fragment 或 Activity)提供數(shù)據(jù),并包含數(shù)據(jù)處理業(yè)務(wù)邏輯,以與模型進行通信。例如,ViewModel 可以調(diào)用其他組件來加載數(shù)據(jù),還可以轉(zhuǎn)發(fā)用戶請求來修改數(shù)據(jù)。ViewModel 不了解界面組件,因此不受配置更改(如在旋轉(zhuǎn)設(shè)備時重新創(chuàng)建 Activity)的影響。
我們現(xiàn)在定義了以下文件:

  • user_profile.xml:屏幕的界面布局定義。
  • UserProfileFragment:顯示數(shù)據(jù)的界面控制器。
  • UserProfileViewModel:準(zhǔn)備數(shù)據(jù)以便在 UserProfileFragment 中查看并對用戶互動做出響應(yīng)的類。

以下代碼段顯示了這些文件的起始內(nèi)容(為簡單起見,省略了布局文件)。
UserProfileViewModel

class UserProfileViewModel : ViewModel() {
   val userId : String = TODO()
   val user : User = TODO()
}

UserProfileFragment

class UserProfileFragment : Fragment() {
        // To use the viewModels() extension function, include
        // "androidx.fragment:fragment-ktx:latest-version " in your app
        // module's build.gradle file.
  private val viewModel: UserProfileViewModel by viewModels()

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View {
          return inflater.inflate(R.layout.main_fragment, container, false)
        }
}

現(xiàn)在,我們有了這些代碼模塊,如何將它們連接起來?畢竟,在 UserProfileViewModel 類中設(shè)置 user 字段時,我們需要一種方法來通知界面。
要獲取 user,我們的 ViewModel 需要訪問 Fragment 參數(shù)。我們可以通過 Fragment 傳遞它們,或者更好的辦法是使用 SavedState 模塊,我們可以讓 ViewModel 直接讀取參數(shù):
注意:SavedStateHandle 允許 ViewModel 訪問相關(guān) Fragment 或 Activity 的已保存狀態(tài)和參數(shù)。

// UserProfileViewModel
class UserProfileViewModel(
   savedStateHandle: SavedStateHandle
) : ViewModel() {
   val userId : String = savedStateHandle["uid"] ?:
          throw IllegalArgumentException("missing user id")
   val user : User = TODO()
}

// UserProfileFragment
private val viewModel: UserProfileViewModel by viewModels(
   factoryProducer = { SavedStateVMFactory(this) }
   ...
)

獲取用戶對象后,我們需要通知 Fragment。這就是 LiveData 架構(gòu)組件的用武之地。
LiveData 是一種可觀察的數(shù)據(jù)存儲器。應(yīng)用中的其他組件可以使用此存儲器監(jiān)控對象的更改,而無需在它們之間創(chuàng)建明確且嚴格的依賴路徑。LiveData 組件還遵循應(yīng)用組件(如 Activity、Fragment 和 Service)的生命周期狀態(tài),并包括清理邏輯以防止對象泄漏和過多的內(nèi)存消耗。
為了將 LiveData 組件納入應(yīng)用,我們將 UserProfileViewModel 中的字段類型更改為 LiveData<User>?,F(xiàn)在,更新數(shù)據(jù)時,系統(tǒng)會通知 UserProfileFragment。
此外,由于此 LiveData字段具有生命周期感知能力,因此當(dāng)不再需要引用時,會自動清理它們。
UserProfileViewModel

class UserProfileViewModel(
   savedStateHandle: SavedStateHandle
) : ViewModel() {
   val userId : String = savedStateHandle["uid"] ?:
          throw IllegalArgumentException("missing user id")
   val user : LiveData<User> = TODO()
}

現(xiàn)在,我們修改 UserProfileFragment 以觀察數(shù)據(jù)并更新界面:
UserProfileFragment

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   viewModel.user.observe(viewLifecycleOwner) {
       // update UI
   }
}

每次更新用戶個人資料數(shù)據(jù)時,系統(tǒng)都會調(diào)用 onChanged() 回調(diào)并刷新界面。
如果您熟悉使用可觀察回調(diào)的其他庫,那么您可能已經(jīng)意識到,我們并未替換 Fragment 的 onStop()方法以停止觀察數(shù)據(jù)。使用 LiveData 時沒必要執(zhí)行此步驟,因為它具有生命周期感知能力。這意味著,除非 Fragment 處于活躍狀態(tài)(即,已接收 onStart() 但尚未接收 onStop(),否則它不會調(diào)用 onChanged() 回調(diào)。當(dāng)調(diào)用 Fragment 的 onDestroy()方法時,LiveData 還會自動移除觀察者。

此外,我們也沒有添加任何邏輯來處理配置更改(例如,用戶旋轉(zhuǎn)設(shè)備的屏幕)。UserProfileViewModel 會在配置更改后自動恢復(fù),所以一旦創(chuàng)建新的 Fragment,它就會接收相同的 ViewModel 實例,并且會立即使用當(dāng)前的數(shù)據(jù)調(diào)用回調(diào)。鑒于 ViewModel 對象應(yīng)該比它們更新的相應(yīng) View 對象存在的時間更長,因此 ViewModel 實現(xiàn)中不得包含對 View 對象的直接引用。

獲取數(shù)據(jù)

現(xiàn)在,我們已使用 LiveData 將 UserProfileViewModel 連接到 UserProfileFragment,那么如何獲取用戶個人資料數(shù)據(jù)?
在本例中,我們假定后端提供 REST API。我們使用 Retrofit 庫訪問后端,不過您可以隨意使用起著相同作用的其他庫。
下面是與后端進行通信的 Webservice 的定義:
Webservice

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}")
   fun getUser(@Path("user") userId: String): Call<User>
}

實現(xiàn) ViewModel 的第一個想法可能是直接調(diào)用 Webservice 來獲取數(shù)據(jù),然后將該數(shù)據(jù)分配給 LiveData 對象。這種設(shè)計行得通,但如果采用這種設(shè)計,隨著應(yīng)用的擴大,應(yīng)用會變得越來越難以維護。這樣會使 UserProfileViewModel 類承擔(dān)太多的責(zé)任,這就違背了分離關(guān)注點原則。此外,ViewModel 的時間范圍與 ActivityFragment生命周期相關(guān)聯(lián),這意味著,當(dāng)關(guān)聯(lián)界面對象的生命周期結(jié)束時,會丟失 Webservice 的數(shù)據(jù),進而影響用戶體驗。
ViewModel 會將數(shù)據(jù)獲取過程委派給一個新的模塊,即存儲區(qū)。
存儲區(qū)模塊會處理數(shù)據(jù)操作。它們會提供一個干凈的 API,以便應(yīng)用的其余部分可以輕松檢索該數(shù)據(jù)。數(shù)據(jù)更新時,它們知道從何處獲取數(shù)據(jù)以及進行哪些 API 調(diào)用。您可以將存儲區(qū)視為不同數(shù)據(jù)源(如持久性模型、網(wǎng)絡(luò)服務(wù)和緩存)之間的媒介。
UserRepository 類(如以下代碼段中所示)使用 WebService 實例來獲取用戶的數(shù)據(jù):
UserRepository

class UserRepository {
   private val webservice: Webservice = TODO()
   // ...
   fun getUser(userId: String): LiveData<User> {
       // This isn't an optimal implementation. We'll fix it later.
       val data = MutableLiveData<User>()
       webservice.getUser(userId).enqueue(object : Callback<User> {
           override fun onResponse(call: Call<User>, response: Response<User>) {
               data.value = response.body()
           }
           // Error case is left out for brevity.
           override fun onFailure(call: Call<User>, t: Throwable) {
               TODO()
           }
       })
       return data
   }
}

雖然存儲區(qū)模塊看起來不必要,但它起著一項重要的作用:它會從應(yīng)用的其余部分中提取數(shù)據(jù)源?,F(xiàn)在,UserProfileViewModel 不知道如何獲取數(shù)據(jù),因此我們可以為視圖模型提供從幾個不同的數(shù)據(jù)獲取實現(xiàn)獲得的數(shù)據(jù)。
注意:為簡單起見,我們省去了網(wǎng)絡(luò)錯誤情況。有關(guān)公開錯誤和加載狀態(tài)的替代實現(xiàn),后面會介紹如何處理錯誤等狀態(tài)

連接 ViewModel 與存儲區(qū)

現(xiàn)在,我們修改 UserProfileViewModel 以使用 UserRepository 對象:
UserProfileViewModel

class UserProfileViewModel @Inject constructor(
   savedStateHandle: SavedStateHandle,
   userRepository: UserRepository
) : ViewModel() {
   val userId : String = savedStateHandle["uid"] ?:
          throw IllegalArgumentException("missing user id")
   val user : LiveData<User> = userRepository.getUser(userId)
}

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

UserRepository 實現(xiàn)會抽象化對 Webservice 對象的調(diào)用,但由于它只依賴于一個數(shù)據(jù)源,因此不是很靈活。
UserRepository 實現(xiàn)的關(guān)鍵問題是,它從后端獲取數(shù)據(jù)后,不會將該數(shù)據(jù)存儲在任何位置。因此,如果用戶在離開 UserProfileFragment 后再返回該類,則應(yīng)用必須重新獲取數(shù)據(jù),即使數(shù)據(jù)未發(fā)生更改也是如此。
這種設(shè)計不夠理想,原因如下:

  • 浪費了寶貴的網(wǎng)絡(luò)帶寬。
  • 迫使用戶等待新的查詢完成。

為了彌補這些缺點,我們向 UserRepository 添加了一個新的數(shù)據(jù)源,用于將 User 對象緩存在內(nèi)存中:
UserRepository

// Informs Dagger that this class should be constructed only once.
@Singleton
class UserRepository @Inject constructor(
   private val webservice: Webservice,
   // Simple in-memory cache. Details omitted for brevity.
   private val userCache: UserCache
) {
   fun getUser(userId: String): LiveData<User> {
       val cached : LiveData<User> = userCache.get(userId)
       if (cached != null) {
           return cached
       }
       val data = MutableLiveData<User>()
       // The LiveData object is currently empty, but it's okay to add it to the
       // cache here because it will pick up the correct data once the query
       // completes.
       userCache.put(userId, data)
       // This implementation is still suboptimal but better than before.
       // A complete implementation also handles error cases.
       webservice.getUser(userId).enqueue(object : Callback<User> {
           override fun onResponse(call: Call<User>, response: Response<User>) {
               data.value = response.body()
           }

           // Error case is left out for brevity.
           override fun onFailure(call: Call<User>, t: Throwable) {
               TODO()
           }
       })
       return data
   }
}

保留數(shù)據(jù)

使用我們當(dāng)前的實現(xiàn)時,如果用戶旋轉(zhuǎn)設(shè)備或離開應(yīng)用后立即返回應(yīng)用,則現(xiàn)有界面將立即變?yōu)榭梢姡驗榇鎯^(qū)將從內(nèi)存中的緩存檢索數(shù)據(jù)。

不過,如果用戶離開應(yīng)用,數(shù)小時后當(dāng) Android 操作系統(tǒng)已終止進程后再回來,會發(fā)生什么?在這種情況下,如果依賴我們當(dāng)前的實現(xiàn),則需要再次從網(wǎng)絡(luò)中獲取數(shù)據(jù)。這一重新獲取的過程不僅是一種糟糕的用戶體驗,而且很浪費資源,因為它會消耗寶貴的移動數(shù)據(jù)。

您可以通過緩存網(wǎng)絡(luò)請求來解決此問題,但這樣做會帶來一個值得關(guān)注的新問題:如果相同的用戶數(shù)據(jù)因另一種類型的請求(如獲取好友列表)而顯示出來,會發(fā)生什么?應(yīng)用將會顯示不一致的數(shù)據(jù),這樣比較容易讓用戶感到困惑。例如,如果用戶在不同的時間發(fā)出好友列表請求和單一用戶請求,應(yīng)用可能會顯示同一用戶的數(shù)據(jù)的兩個不同版本。應(yīng)用將需要弄清楚如何合并這些不一致的數(shù)據(jù)。

處理這種情況的正確方法是使用持久性模型。這就是Room持久性庫的用武之地。
Room 是一個對象映射庫,可利用最少的樣板代碼實現(xiàn)本地數(shù)據(jù)持久性。在編譯時,它會根據(jù)數(shù)據(jù)架構(gòu)驗證每個查詢,這樣損壞的 SQL 查詢會導(dǎo)致編譯時錯誤而不是運行時失敗。Room 可以抽象化處理原始 SQL 表格和查詢的一些底層實現(xiàn)細節(jié)。它還允許您觀察對數(shù)據(jù)庫數(shù)據(jù)(包括集合和連接查詢)的更改,并使用 LiveData 對象公開這類更改。它甚至明確定義了解決一些常見線程問題(如訪問主線程上的存儲空間)的執(zhí)行約束。
注意:如果您的應(yīng)用已使用 SQLite 對象關(guān)系映射 (ORM) 等其他持久性解決方案,那么您無需將現(xiàn)有解決方案替換為 Room。不過,如果您正在編寫新應(yīng)用或重構(gòu)現(xiàn)有應(yīng)用,那么我們建議您使用 Room 保留應(yīng)用數(shù)據(jù)。這樣一來,您便可以利用該庫的抽象和查詢驗證功能。
要使用 Room,我們需要定義本地架構(gòu)。首先,我們向 User 數(shù)據(jù)模型類添加 @Entity注釋,并向該類的 id 字段添加 @PrimaryKey 注釋。這些注釋會將 User 標(biāo)記為數(shù)據(jù)庫中的表格,并將 id 標(biāo)記為該表格的主鍵:
User

@Entity
data class User(
   @PrimaryKey private val id: String,
   private val name: String,
   private val lastName: String
)

然后,我們通過為應(yīng)用實現(xiàn) RoomDatabase來創(chuàng)建一個數(shù)據(jù)庫類:
UserDatabase

@Database(entities = [User::class], version = 1)
abstract class UserDatabase : RoomDatabase()

請注意,UserDatabase 是抽象類。Room 將自動提供它的實現(xiàn)。
現(xiàn)在,我們需要一種將用戶數(shù)據(jù)插入數(shù)據(jù)庫的方法。為了完成此任務(wù),我們創(chuàng)建一個數(shù)據(jù)訪問對象 (DAO)
UserDao

@Dao
interface UserDao {
   @Insert(onConflict = REPLACE)
   fun save(user: User)

   @Query("SELECT * FROM user WHERE id = :userId")
   fun load(userId: String): LiveData<User>
}

請注意,load 方法將返回一個 LiveData<User> 類型的對象。Room 知道何時修改了數(shù)據(jù)庫,并會自動在數(shù)據(jù)發(fā)生更改時通知所有活躍的觀察者。由于 Room 使用 LiveData,因此此操作很高效;僅當(dāng)至少有一個活躍的觀察者時,它才會更新數(shù)據(jù)。

定義 UserDao 類后,從我們的數(shù)據(jù)庫類引用該 DAO:
UserDatabase

@Database(entities = [User::class], version = 1)
abstract class UserDatabase : RoomDatabase() {
   abstract fun userDao(): UserDao
}

現(xiàn)在,我們可以修改 UserRepository 以納入 Room 數(shù)據(jù)源:

// Informs Dagger that this class should be constructed only once.
@Singleton
class UserRepository @Inject constructor(
   private val webservice: Webservice,
   // Simple in-memory cache. Details omitted for brevity.
   private val executor: Executor,
   private val userDao: UserDao
) {
   fun getUser(userId: String): LiveData<User> {
       refreshUser(userId)
       // Returns a LiveData object directly from the database.
       return userDao.load(userId)
   }

   private fun refreshUser(userId: String) {
       // Runs in a background thread.
       executor.execute {
           // Check if user data was fetched recently.
           val userExists = userDao.hasUser(FRESH_TIMEOUT)
           if (!userExists) {
               // Refreshes the data.
               val response = webservice.getUser(userId).execute()

               // Check for errors here.

               // Updates the database. The LiveData object automatically
               // refreshes, so we don't need to do anything else here.
               userDao.save(response.body()!!)
           }
       }
   }

   companion object {
       val FRESH_TIMEOUT = TimeUnit.DAYS.toMillis(1)
   }
}

請注意,雖然我們在 UserRepository 中更改了數(shù)據(jù)的來源,但不需要更改 UserProfileViewModel 或 UserProfileFragment。這種小范圍的更新展示了我們的應(yīng)用架構(gòu)所具有的靈活性。這也很適合測試,因為我們可以提供虛假的 UserRepository,與此同時測試正式版 UserProfileViewModel。

如果用戶等待幾天后再返回使用此架構(gòu)的應(yīng)用,他們很可能會看到過時的信息,直到存儲區(qū)可以獲取更新的信息。根據(jù)您的用例,您可能不希望顯示這些過時的信息。您可以改為顯示占位符數(shù)據(jù),此類數(shù)據(jù)將顯示示例值并指示您的應(yīng)用當(dāng)前正在獲取并加載最新信息。

顯示正在執(zhí)行的操作

在某些用例(如下拉刷新)中,界面務(wù)必要向用戶顯示當(dāng)前正在執(zhí)行某項網(wǎng)絡(luò)操作。將界面操作與實際數(shù)據(jù)分離開來是一種很好的做法,因為數(shù)據(jù)可能會因各種原因而更新。例如,如果我們獲取了好友列表,可能會程序化地再次獲取相同的用戶,從而觸發(fā) LiveData<User> 更新。從界面的角度來看,傳輸中的請求只是另一個數(shù)據(jù)點,類似于 User 對象本身中的其他任何數(shù)據(jù)。

我們可以使用以下某個策略,在界面中顯示一致的數(shù)據(jù)更新狀態(tài)(無論更新數(shù)據(jù)的請求來自何處):

  • 更改 getUser() 以返回一個 LiveData 類型的對象。此對象將包含網(wǎng)絡(luò)操作的狀態(tài)。
    有關(guān)示例,請參閱 Android 架構(gòu)組件 GitHub 項目中的 NetworkBoundResource 實現(xiàn)。
  • UserRepository 類中再提供一個可以返回 User 刷新狀態(tài)的公共函數(shù)。如果您只想在數(shù)據(jù)獲取過程源自于顯式用戶操作(如下拉刷新)時在界面中顯示網(wǎng)絡(luò)狀態(tài),使用此策略效果會更好。

最佳做法

編程是一個創(chuàng)造性的領(lǐng)域,構(gòu)建 Android 應(yīng)用也不例外。無論是在多個 Activity 或 Fragment 之間傳遞數(shù)據(jù),檢索遠程數(shù)據(jù)并將其保留在本地以在離線模式下使用,還是復(fù)雜應(yīng)用遇到的任何其他常見情況,解決問題的方法都會有很多種。

雖然以下建議不是強制性的,但根據(jù)我們的經(jīng)驗,從長遠來看,遵循這些建議會使您的代碼庫更強大、可測試性更高且更易維護:

  • 避免將應(yīng)用的入口點(如 Activity、Service 和廣播接收器)指定為數(shù)據(jù)源。
  • 在應(yīng)用的各個模塊之間設(shè)定明確定義的職責(zé)界限。
  • 盡量少公開每個模塊中的代碼
  • 考慮如何使每個模塊可獨立測試。
  • 專注于應(yīng)用的獨特核心,以使其從其他應(yīng)用中脫穎而出。
  • 保留盡可能多的相關(guān)數(shù)據(jù)和最新數(shù)據(jù)。

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

在上文的推薦應(yīng)用架構(gòu)部分中,我們省略了網(wǎng)絡(luò)錯誤和加載狀態(tài)以簡化代碼段。
本部分將演示如何使用可封裝數(shù)據(jù)及其狀態(tài)的 Resource 類來公開網(wǎng)絡(luò)狀態(tài)。
以下代碼段提供了 Resource 的實現(xiàn)示例:

// A generic class that contains data and status about loading this data.
sealed class Resource<T>(
   val data: T? = null,
   val message: String? = null
) {
   class Success<T>(data: T) : Resource<T>(data)
   class Loading<T>(data: T? = null) : Resource<T>(data)
   class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
}

有一個很常見的情況是,一邊從網(wǎng)絡(luò)加載數(shù)據(jù),一邊顯示這些數(shù)據(jù)在磁盤上的副本,因此建議您創(chuàng)建一個可在多個地方重復(fù)使用的輔助程序類。在本例中,我們創(chuàng)建一個名為 NetworkBoundResource 的類。
下圖顯示了 NetworkBoundResource 的決策樹:


network-bound-resource.png

它首先觀察資源的數(shù)據(jù)庫。首次從數(shù)據(jù)庫中加載條目時,NetworkBoundResource 會檢查結(jié)果是好到足以分派,還是應(yīng)從網(wǎng)絡(luò)中重新獲取。請注意,考慮到您可能會希望在通過網(wǎng)絡(luò)更新數(shù)據(jù)的同時顯示緩存的數(shù)據(jù),這兩種情況可能會同時發(fā)生。
如果網(wǎng)絡(luò)調(diào)用成功完成,它會將響應(yīng)保存到數(shù)據(jù)庫中并重新初始化數(shù)據(jù)流。如果網(wǎng)絡(luò)請求失敗,NetworkBoundResource 會直接分派失敗消息。

注意:在將新數(shù)據(jù)保存到磁盤后,我們會重新初始化來自數(shù)據(jù)庫的數(shù)據(jù)流。不過,通常我們不需要這樣做,因為數(shù)據(jù)庫本身正好會分派更改。
此外,不要分派來自網(wǎng)絡(luò)的結(jié)果,因為這樣將違背單一可信來源原則。畢竟,數(shù)據(jù)庫可能包含在“保存”操作期間更改數(shù)據(jù)值的觸發(fā)器。同樣,不要在沒有新數(shù)據(jù)的情況下分派 SUCCESS,因為如果這樣做,客戶端會接收錯誤版本的數(shù)據(jù)。

以下代碼段顯示了 NetworkBoundResource 類為其子類提供的公共 API:
NetworkBoundResource.kt

// ResultType: Type for the Resource data.
// RequestType: Type for the API response.
abstract class NetworkBoundResource<ResultType, RequestType> {
   // Called to save the result of the API response into the database
   @WorkerThread
   protected abstract fun saveCallResult(item: RequestType)

   // Called with the data in the database to decide whether to fetch
   // potentially updated data from the network.
   @MainThread
   protected abstract fun shouldFetch(data: ResultType?): Boolean

   // Called to get the cached data from the database.
   @MainThread
   protected abstract fun loadFromDb(): LiveData<ResultType>

   // Called to create the API call.
   @MainThread
   protected abstract fun createCall(): LiveData<ApiResponse<RequestType>>

   // Called when the fetch fails. The child class may want to reset components
   // like rate limiter.
   protected open fun onFetchFailed() {}

   // Returns a LiveData object that represents the resource that's implemented
   // in the base class.
   fun asLiveData(): LiveData<ResultType> = TODO()
}

請注意有關(guān)該類定義的以下重要細節(jié):

  • 它定義了兩個類型參數(shù)(ResultType 和 RequestType),因為從 API 返回的數(shù)據(jù)類型可能與本地使用的數(shù)據(jù)類型不匹配。
  • 它對網(wǎng)絡(luò)請求使用了一個名為 ApiResponse 的類。ApiResponse 是 Retrofit2.Call 類的一個簡單封裝容器,可將響應(yīng)轉(zhuǎn)換為 LiveData 實例。

NetworkBoundResource 類的完整實現(xiàn)作為 Android 架構(gòu)組件 GitHub 項目的一部分出現(xiàn)。
創(chuàng)建 NetworkBoundResource 后,我們可以使用它在 UserRepository 類中編寫磁盤和網(wǎng)絡(luò)綁定的 User 實現(xiàn):
UserRepository

// Informs Dagger that this class should be constructed only once.
@Singleton
class UserRepository @Inject constructor(
   private val webservice: Webservice,
   private val userDao: UserDao
) {
   fun getUser(userId: String): LiveData<User> {
       return object : NetworkBoundResource<User, User>() {
           override fun saveCallResult(item: User) {
               userDao.save(item)
           }

           override fun shouldFetch(data: User?): Boolean {
               return rateLimiter.canFetch(userId) && (data == null || !isFresh(data))
           }

           override fun loadFromDb(): LiveData<User> {
               return userDao.load(userId)
           }

           override fun createCall(): LiveData<ApiResponse<User>> {
               return webservice.getUser(userId)
           }
       }.asLiveData()
   }
}

今天的內(nèi)容就介紹到這里,相信大家對應(yīng)用架構(gòu)有了大致的了解,但是真正的運用到自己的項目中肯定還是無從下手的,尤其是還在使用Java開發(fā)Android的朋友來說。下一篇文章我會用具體的代碼介紹如何用Java配合應(yīng)用架構(gòu)進行實戰(zhàn),敬請期待~~~

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

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