聊聊MVX中的Model

寫在前面

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

  • Model 層 : 實體模型、數據的獲取、存儲等等
  • View層:向用戶展現UI及處理交互

但據我在GitHub上看過的各類項目代碼而言,許多人僅僅停留在字面上的理解,而沒有真正的處理好三層間的邊界。android

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

Model層

MVVM架構爲例,Model層的職責主要是數據的獲取和存儲而後將數據返回給ViewModel層。github

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

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

ViewModel的一個簡單的實現方式是直接調用Webservice獲取數據,而後把它賦值給User對象。雖然這樣可行,可是隨着app的增大會變得難以維護。ViewModel的職責過多也違背了前面提到的關注點分離(separation of concerns)原則。另外,ViewModel的有效時間是和ActivityFragment的生命週期綁定的,所以當它的生命週期結束便丟失全部數據是一種很差的用戶體驗。相反,咱們的ViewModel將把這個工做代理給Repository模塊。緩存

所以,Repository 就成了一個很關鍵的模塊,咱們在這裏處理全部關於數據的事情,包括服務器

  1. 從SharedPreference或者數據庫或者服務器獲取數據架構

  2. 使用SharedPreference或者數據庫緩存數據app

  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

相關文章
相關標籤/搜索