聊聊MVX中的Model

ditclear發表於2019-04-12

寫在前面

隨著Android架構的不斷演進,從最初的MVC到MVP再到MVVM,變化的只有M和V層之間的部分,M和V層開發者似乎都已經統一了意見。

  • Model 層 : 實體模型、資料的獲取、儲存等等
  • View層:向使用者展示UI及處理互動

但據我在GitHub上看過的各種專案程式碼而言,許多人僅僅停留在字面上的理解,而沒有真正的處理好三層間的邊界。

今天,我們來聊一聊MVX中的Model。

Model層

MVVM架構為例,Model層的職責主要是資料的獲取和儲存然後將資料返回給ViewModel層。

聊聊MVX中的Model

在上面的圖解中,我們分離出了一個倉庫類Repository,這是Google開發架構指南中推薦的做法。

對此,在指南中這樣解釋:

ViewModel的一個簡單的實現方式是直接呼叫Webservice獲取資料,然後把它賦值給User物件。雖然這樣可行,但是隨著app的增大會變得難以維護。ViewModel的職責過多也違背了前面提到的關注點分離(separation of concerns)原則。另外,ViewModel的有效時間是和ActivityFragment的生命週期繫結的,因此當它的生命週期結束便丟失所有資料是一種不好的使用者體驗。相反,我們的ViewModel將把這個工作代理給Repository模組。

因此,Repository 就成了一個很關鍵的模組,我們在這裡處理所有關於資料的事情,包括

  1. 從SharedPreference或者資料庫或者伺服器獲取資料

  2. 使用SharedPreference或者資料庫快取資料

  3. 對請求引數的處理以及將返回資料處理為ViewModel層希望的型別

接下來,我們圍繞著這三點來聊聊Model。

推薦的做法

  • 推薦 Model層通過SharedPreference存取資料

在我看過的大部分程式碼中,包括我自己以前也並沒有意識到應該在Model層中通過SharedPreference存取資料,原因可能是沒這個意識或者是因為寫在其它地方也沒影響到流程。

比如我們需要根據SharedPreference中快取的使用者Id來載入使用者的詳細資訊,並且將返回結果也快取在SharedPreference中。這種場景下經常會出現如下的程式碼:

/// View
final userId = spUtil.getString("userId")

viewmodel.getUserDetail(userId)
.subscribe({
 	//成功之後快取詳情
    spUtil.putString("userDetail")
},{})

/// ViewModel
fun getUserDetail(userId:String):Observable = repository.getUserDetail(userId)

/// Model
class UserRepository constructor(private val remote){
    /// 獲取使用者詳情
	fun getUserDetail(userId:String):Observable = remote.getUserDetail(userId)
}
複製程式碼

但是SharedPreference的作用其實類似於資料庫,如果DB應該位於Model層,那麼SharedPreference也同樣,而且也不會出現引數傳來傳去的情況,改造之後的程式碼如下:

/// View
viewmodel.getUserDetail()
.subscribe({
 	//success
},{})

/// ViewModel
fun getUserDetail():Observable = repository.getUserDetail()

/// Model
class UserRepository constructor(private val remote:UserService,private val spUtil:SpUtil){
    /// 獲取使用者詳情
    ///
    /// 通過 [spUtil] 拿到快取的userId,獲取到詳情後再用[spUtil]進行快取
	fun getUserDetail():Observable {
    	final userId = spUtil.getString("userId")
    	return remote.getUserDetail(userId).doOnSuccess{
        	spUtil.putString("userDetail")
    	}
	}
}
複製程式碼
  • 推薦儘量在Repository中管理資料

我常常看到一些開發者只是把Repository當作是一個擺設,或者是一個象徵性的東西,而沒有實質上發揮它該有的作用。

比如有這樣一個場景:展示文章詳情,如果文章以前已被快取過,那麼直接獲取快取的資料,否則拉取服務端的資料並快取到本地資料庫。

就會出現如下的程式碼:

/// ViewModel
fun getArticleDetail(articleId:String):Observable {
    return repository.getLocalArticleById(articleId)
    				 .onErrorResumeNext {
						repository.getArticleById(id)
                        .doOnSuccess { repository.insertArticle(it) }
                     }.doOnSuccess{
                         // 資料轉換成View層需要的資料
                         renderUI(it)
                     }
}

/// Model
class ArticleRepository constructor(private val remote:ArticleService,private val local:ArticleDao){
	/// 根據[articleId]從資料庫中查詢文章詳情
    fun getLocalArticleById(articleId:String) = local.getLocalArticleById(articleId)
    
    /// 根據[articleId]從服務端獲取文章詳情
    fun getArticleById(articleId:String) = remote.getArticleById(articleId)
    
    /// 將文章詳情[article]插入本地資料庫
    fun insertArticle(Article article) = local.insertArticle(article)
}

複製程式碼

這樣的程式碼最大的問題是沒做到關注點分離

ViewModel層並不關心資料怎麼來,也不關心資料應該怎麼儲存。它只關心拿到Model層的原始資料之後應該怎麼將其轉換為View層需要展示的資料。

基於這個原則,改造之後的程式碼如下:

/// ViewModel

fun getArticleDetail(articleId:String):Observable {
    return repository.getArticleDetail(articleId)
    				 .doOnSuccess{
                         // 資料轉換成View層需要的資料
                         renderUI(it)
                     }
                                                  
 }

/// Model
class ArticleRepository constructor(private val remote:ArticleService,private val local:ArticleDao){

    /// 獲取文章詳情,
    ///
    /// 如果文章以前已被快取過,那麼直接獲取快取的資料,否則拉取服務端的資料並快取到本地資料庫
    /// 返回 [Observable] 給 ViewModel層
    fun getArticleDetail(articleId:String):Observable {
    return local.getLocalArticleById(articleId)
    				 .onErrorResumeNext {
						remote.getArticleById(id)
                        .doOnSuccess { local.insertArticle(it) }
            }
    
}


複製程式碼
  • 推薦引數的轉換和返回資料在Model層處理

在進行框架搭建的過程中,我認為能儘量減少錯誤的方式就是儘可能的讓需要呼叫你方法的人少寫程式碼。

比如以下場景:

/// ViewModel
fun login() {
    final token = "basic"+ base64Encode(utf8.encode('$username:$password'))
    return repository.login(token)
}

/// Model
fun login(token:String){
    return remote.login(token)
}
複製程式碼

在這種場景下,假如換成了其它的驗證方式,那麼所有生成token的地方都需要改,耗時耗力,而且如果說有組員生成token的方法錯了,那麼也挺難排查的。

因此,建議在Model層進行引數的處理

/// ViewModel
fun login() {
    return repository.login(username,password)
}

/// Model
fun login(username:String,password:String){
    final token = "basic"+ base64Encode(utf8.encode('$username:$password'))
    return remote.login(token)
}
複製程式碼

另外就是返回資料的轉換

日常開發中,我們從服務端獲取到的資料並不是ViewModel真正需要的,比如會出現BaseResponse<T>這樣的返回資料,而ViewModel真正需要的則是T。

在前文我們也提到Model層的職責之一便是**提供ViewModel層需要的資料,**因此我們需要在Repository中對這型別的資料先處理一番。

/// ViewModel
fun getUserDetail(userId:String) = repository.getUserDetail(userId)

/// Model
fun getUserDetail(userId:String):Observable<User> {
    return remote.getUserDetail(userId)
    .doOnSuccess{
        if(!it.success){
            throw Exception(it.message)
        }
    }
    .map{it.data}
}
//remote
@Get('user/{userId}')
fun getUserDetail(@Path("userId") userId:String):Observable<BaseResponse<User>> 
複製程式碼

經過這番改造,明確了Model層的職責,我們就可以將重心放在業務邏輯上,寫出更加高效的程式碼。

寫在最後

關於Model的概念,我想大多數研究或學習過MVX的人都有所瞭解。但實際應該怎麼做,怎麼確定Model的職責這個還是看個人的積累。

我也不敢說我的寫法就一定是對的,因為我所寫的MVVM和其它人包括AAC都有不同的地方,但對於我來說,依循著這樣的規範,已經給我包括團隊的開發效率帶來了極大的提升。

如果你對於文章中的程式碼有疑問或者感興趣,可以看看我寫的小專欄 《使用Kotlin構建MVVM應用程式》

如果本文對你有幫助,請點贊支援。

==================== 分割線 ======================

如果你想了解更多關於MVVM、Flutter、響應式程式設計方面的知識,歡迎關注我。

你可以在以下地方找到我:

簡書:www.jianshu.com/u/117f1cf0c…

掘金:juejin.im/user/582d60…

Github: github.com/ditclear

聊聊MVX中的Model

相關文章