關於Android開發架構,咱們還能作些什麼?

Android開發架構已經由由最最初的Activity架構(MVC),發展到到如今主流的MVP、MVVM架構了。社區也有很多優秀的實踐。今天筆者想結合本身的經驗談一談,一個合理的Android架構應該是怎麼樣的呢?java

MVC、MVP、MVVM三種分層架構

MVC,上帝模型

相信一些經驗豐富的開發者,都經歷過面向Activity(Fragment)編程的時代,也就是所謂的MVC架構時代。那個時代,在開發的時候,會把咱們的業務模塊氛圍三層。以下圖:git

MVC

若是咱們能嚴格按照上圖來開發咱們的架構的話,那麼這會是一種比較好的實現。可是這種架構在Android中的實踐存在一個很是嚴重的問題。在劃分View層的時候,咱們是要把layout做爲View層,仍是把Activity/Fragment 加layout文件一塊兒做爲View層呢?若是是後者,那麼咱們應該還要引入一個新的Control層。在比較初期(1五、14年前)的Android開發中,通常狀況下都沒有考慮這麼多,直接把layout文件做爲View層,Activity/Fragment做爲所謂的Control層了。github

把xml文件做爲View層會帶來什麼問題呢?xml文件的控制力是很是弱的,它只能在開發更改UI,一旦發佈了,xml的UI就是固定的了。若是你要在App運行期間修改UI(文本更新)的話,那麼咱們就須要藉助Java代碼了。通常咱們會在Activity/Fragment中經過代碼更新UI。數據庫

那麼這就帶來一個新的問題,Activity/Fragment做爲一個C層,同時承載了C層和V層的業務。因此MVC在Android中的實現通常會變成VC模型。而這種模型,在業務複雜的場景中,造就了大量的上帝,也就所謂的God Class。通常來講,咱們的上千行的Activity都是這種模型的結果。編程

MVP,萬物皆回調

在經歷了維護God Class的痛苦時代,因此咱們又選擇了新的MVP架構,其實MVP架構和MVC架構也是很像的。在Android中的MVP就是把layout文件和Activity/Fragment做爲了View層,這裏面只包含UI相關的代碼,不包含業務邏輯的。而Presenter就是做爲邏輯層,這個其實和傳統的MVC架構有點像,可是和MVC又有一點不一樣。MVP架構中,V層是不能直接查詢M層的數據的(MVC中是容許的)。另一個就是,不一樣層級之間數據交互的方式是基於Callback的異步回調模式的,因此MVP的具體實現中,會包含大量的Interface。api

MVP的架構圖以下:緩存

MVP

圖片也印證了,MVP和MVC在架構上來講實際上是很是像的。最大的不一樣點就是在MVP中,V層和M層是不能直接交互的。MVP雖然能把咱們的業務分割成三個相互獨立的部分,可是MVP的缺點也很是明顯。模版代碼很是多、基於接口的通信方式會致使咱們定義大量一次性接口。而且,P層和V層的生命週期是不一致的,會存在P層還存活,V層被回收的狀況。這樣會致使UI更新失敗,很容易Crash。服務器

爲了分割邏輯,增長這麼多代碼與一次性接口是否值得,這是一個須要充分考慮的問題。網絡

MVVM,數據驅動模型

由於MVP樣板代碼太多的緣由,因此咱們又開始嘗試MVVM架構了。Google也推出了很多輔助工具幫助咱們在Android中實現MVVM架構,如:DataBinding,LiveData,ViewModel系列等。MVVM架構的核心就是數據驅動,數據驅動的意思就是,數據更新的時候,自動刷新UI。架構

打個比喻:

咱們把View比做一個罪犯,更新View的行爲比做是把罪犯送進監獄的話。那麼在MVC、MVP中,咱們C,和P就是警察,他要負責把罪犯送進監獄。而在MVVM中,罪犯犯罪(數據變化)後,它本身會走進監獄裏面。

採用MVVM架構會幫咱們節省大量的更新UI的代碼,而且數據更新後主動出發UI更新這種方式,更難出錯,魯棒性更強。咱們不須要關注數據變化的時機,是須要關注數據變化的結果便可。MVVM架構圖以下:

MVVM

MVVM架構,由於有官方Jetpack的加持,因此逐漸成爲了架構中的主流,若是是新的項目開發的話,能夠考慮採用MVVM架構。而針對老業務的新模塊,也能夠嘗試過分到MVVM架構中。

不止於分層

經過分析,上面三種架構都是爲了解決一個問題的,就是把UI、邏輯、數據層分開,實現解耦。這三種架構其實沒有優劣之分,實現的方式纔有優劣之分。MVVM架構由於有官方組件加持,因此實現中通常推薦使用MVVM架構。那麼架構單單隻限於分層嗎?

不是的,由於實際開發中,咱們面對的業務場景是很是複雜的,除了把業務分層以外,咱們還須要關注:緩存、限流、分頁、領域設計(本文不會過於關注)等。下面先簡單介紹下緩存和限流這兩個場景。

緩存

咱們先來關注一個Android中常常遇到的一個問題,緩存問題。在Android開發中,大部分場景中,咱們並不會緩存數據,就算要緩存數據,大部分場景咱們都會使用SharedPreferences來實現。不管是不緩存數據,仍是大量使用SharedPreferences緩存數據,其實都會帶來一些問題。

大部分狀況下,咱們從服務器中請求的數據在某個時間段內都是同樣的。例如,咱們請求我的信息接口,如今請求和一分鐘後請求,極可能數據都是同樣的。因此若是咱們能把第一次請求的數據緩存下來,那麼第二次數據咱們就不必重複請求了。這個就是緩存的做用,固然緩存還存在超時時間,這個超時時間的須要根據具體業務來設置。並且緩存還能讓咱們在網絡異常的時候,讓用戶看到他上一次看到的數據。因此一個設計良好的APP,它確定會考慮到緩存的設計的。

而SharedPreferences的問題就是它的效率問題,由於它是基於文件的實現數據持久化的,而且讀取/寫入數據的時候都是全量讀寫的,因此大量使用SharedPreferences實現緩存也是不合適的。

限流

限流的主要做用是,限制客戶端在短期內發起大量重複請求,致使後臺的流量洪峯問題。通常來講,咱們能夠經過限制重複點擊的時間間隔來實現限流。可是這種方案一是會侵入咱們的UI實現,二是有部分場景的請求不是由UI發起的。

咱們在解決上述問題的時候,能夠說是一千我的眼中有一千個哈姆雷特了,每一個人的可能都實現都不同。並且有些實現能夠說是比較糟糕的。那麼咱們能不能制定個比較統一的機制來解決上述問題呢?答案是能夠的,筆者在閱讀了Google的某個開源項目後,發現其實官方很早就提供了一個很是優秀的實現機制。下面咱們就來分享下這一套方法論,這個機制是基於MVVM架構的。

在Android上實現一個比較合理的MVVM架構

能夠看到,一個知足咱們要求的Repository仍是很是複雜的,若是要在每個Repository都實現這麼一套邏輯實際上是不現實。不過這種通用的邏輯咱們能夠封裝起來,咱們先來看看封裝好後的Rpository是怎麼樣的。

RepoRepository

class RepoRepository constructor(
        private val db: GithubDb,
        private val githubService: GithubService) {

    val repoRateLimiter = RateLimiter<String>(15, TimeUnit.SECONDS)

    fun search(query: String): LiveData<Resource<List<Repo>>> {
        return object : NetworkBoundResource<List<Repo>, RepoSearchResponse>() {

            override fun saveCallResult(item: RepoSearchResponse) {
                val repoIds = item.items.map { it.id }
                val repoSearchResult = RepoSearchResult(
                        query = query,
                        repoIds = repoIds,
                        totalCount = item.total,
                        next = item.nextPage
                )
                db.runInTransaction {
                    db.repoDao().insertRepos(item.items)
                    db.repoDao().insert(repoSearchResult)
                }
            }

            override fun shouldFetch(data: List<Repo>?) =
                    data == null || repoRateLimiter.shouldFetch(query)

            override fun loadFromDb(): LiveData<List<Repo>> {
                return Transformations.switchMap(db.repoDao().search(query)) { searchData ->
                    if (searchData == null) {
                        AbsentLiveData.create()
                    } else {
                        db.repoDao().loadOrdered(searchData.repoIds)
                    }
                }
            }

            override fun createCall() = githubService.searchRepos(query)

        }.asLiveData()
    }
}
複製代碼

這裏用到了Room框架來管理數據庫,Retrofit2來調用Github的接口,這裏不對這兩個框架做過多說明,有興趣的朋友直接閱讀源碼MVVMRecurve,demo源碼在sample目錄下。

咱們把具體的調度邏輯都放到NetworkBoundResource這個類裏面了,這樣全部的業務都能服用這一套邏輯。細心的讀者可能會發現,其實Repository只承擔了不多一部分工做的,網絡請求是經過Retrofit2實現的,而數據緩存是經過Room實現的,Repository只作了資源調度的工做。這是一種比較優秀的設計,實現了比較完全的解耦,並且自然支持單元測試。

在Repository中是經過RateLimiter限流類實現的,它的建立很簡單,就是接收一個時間單位。它的做用是針對同一查詢參數,它的請求時間間隔是多少,在上面的例子中是15秒。

NetworkBoundResource

NetworkBoundResource這個類其實就是封裝了Repository流程圖裏面的數據調度內容,咱們在實現新的業務的時候,不須要重複實現調度邏輯,只須要關注業務自己便可。附上源碼:

abstract class NetworkBoundResource<ResultType, RequestType>
@MainThread constructor() {

    protected val result = MediatorLiveData<Resource<ResultType>>()

    init {
        result.value = Resource.loading(null)
        @Suppress("LeakingThis")
        val dbSource = loadFromDb()

        result.addSource(dbSource) { data ->
            result.removeSource(dbSource)
            if (shouldFetch(data)) {
                fetchFromNetwork(dbSource)
            } else {
                result.addSource(dbSource) { newData ->
                    setValue(Resource.success(newData))
                }
            }
        }
    }

    @MainThread
    protected fun setValue(newValue: Resource<ResultType>) {
        if (result.value != newValue) {
            result.value = newValue
        }
    }

    private fun fetchFromNetwork(dbSource: LiveData<ResultType>) = runBlocking{
        val apiResponse = createCall()
        // we re-attach dbSource as a new source, it will dispatch its latest value quickly
        result.addSource(dbSource) { newData ->
            setValue(Resource.loading(newData))
        }
        result.addSource(apiResponse) { response ->
            result.removeSource(apiResponse)
            result.removeSource(dbSource)
            when (response) {
                is ApiSuccessResponse -> {
                    val ioResult = com.recurve.coroutines.io { saveCallResult(processResponse(response)) }
                    ioResult{
                        result.addSource(loadFromDb()){
                            setValue(Resource.success(it))
                        }
                    }

                }
                is ApiEmptyResponse -> {
                    result.addSource(loadFromDb()) { newData
                        -> setValue(Resource.success(newData))
                    }
                }
                is ApiErrorResponse -> {
                    onFetchFailed()
                    result.addSource(dbSource) { newData ->
                        setValue(Resource.error(response.errorMessage, newData))
                    }
                }
            }
        }
    }

    protected open fun onFetchFailed() {}

    fun asLiveData() = result

    @WorkerThread
    protected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body

    @WorkerThread
    protected abstract fun saveCallResult(item: RequestType)

    @MainThread
    protected abstract fun shouldFetch(data: ResultType?): Boolean

    @MainThread
    protected abstract fun loadFromDb(): LiveData<ResultType>

    @MainThread
    protected abstract fun createCall(): LiveData<ApiResponse<RequestType>>
}

複製代碼

SearchRepoViewModel

class SearchRepoViewModel : ViewModel(){

    var repoRepository: RepoRepository? = null

    private val _query = MutableLiveData<String>()
    val query : LiveData<String> = _query

    val results = Transformations
            .switchMap<String, Resource<List<Repo>>>(_query) { search ->
                if (search.isNullOrBlank()) {
                    AbsentLiveData.create()
                } else {
                    repoRepository?.search(search)
                }
            }

    fun setQuery(originalInput: String) {
        val input = originalInput.toLowerCase(Locale.getDefault()).trim()
        if (input == _query.value) {
            return
        }
        _query.value = input
    }
}
複製代碼

能夠看到,ViewModel的邏輯很是簡單,它包含一個方法,setQuery方法接受查詢參數,而results則是Repository返回的LiveData對象(關於LiveData的使用這裏不做介紹)。若是咱們須要處理回調數據的話,直接使用LiveData的變換功能就好了。

View

View層咱們就很少作介紹了,你能夠經過觀察LiveData的數據變化來手動更新數據,也能夠經過DataBinding來實現自動更新數據。根據本身的習慣來實現就好了。下面咱們來看下例子中的關鍵代碼:

private lateinit var binding: FragmentSearchRepoBinding
    private val searchViewModel by lazy { ViewModelProviders.of(this).get(SearchRepoViewModel::class.java)}
    private lateinit var creator: SearchRepoCreator

    override fun onCreateBinding(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): ViewDataBinding {
        binding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_search_repo, container, false)

        initViewRecyclerView(binding.repoList)
        creator = SearchRepoCreator()
        addItemCreator(creator)
        return binding
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.lifecycleOwner = this
        binding.query = searchViewModel.query
        initSearchInputListener()
        initRepository()

        binding.searchResult = searchViewModel.results
        searchViewModel.results.observe(viewLifecycleOwner, Observer { result ->
            result?.data?.let {
                creator.setDataList(it)
            }
        })

    }
}
複製代碼

快速開始

其實說了這麼多,上面演示的只是一個架構中的一個簡單的功能,若是要在本身的項目中從新實現一遍是不現實的。因此筆者其實已經把這個框架封裝了,框架的名字叫:MVVMRecurve。一切基礎架構相關的都封裝好了,你要作的只是依賴下項目便可。

buildscript {
  ext.recurve_version = '1.0.1'
}

//modules build.gradle
implementation "com.recurve:recurve.core:$recurve_version"
implementation "com.recurve:recurve-retrofit2-support:$recurve_version"
implementation "com.recurve:recurve-module-adapter:$recurve_version"
implementation "com.recurve:coroutines-ktx:$recurve_version"

//若是你想支持更多功能的話,能夠依賴下面的庫
implementation "com.recurve:recurve-apollo-support:$recurve_version"
implementation "com.recurve:recurve-dagger2-support:$recurve_version"
implementation "com.recurve:recurve-module-paging-support:$recurve_version"
implementation "com.recurve:recurve-glide-support:$recurve_version"
implementation "com.recurve:viewpager2-navigation-ktx:$recurve_version"
implementation "com.recurve:bottom-navigation-ktx:$recurve_version"
implementation "com.recurve:viewpager2-tablayout-ktx:$recurve_version"
implementation "com.recurve:navigation-dialog:$recurve_version"
複製代碼

小結

MVVMRecurve致力於打造一個還算可用的Android開發框架,而且會積極跟進官方的新技術。若是你想了解一個開發架構要如何設計的話,能夠多多閱讀源碼,你們一塊兒交流。筆者也在嘗試使用這個框架開發一個高可用的項目,該項目是:GitHubRecurve

對於MVVMRecurve,這篇文章直接少了其中一小部分,後面我會持續推出其它的一些技術模塊和設計思想的,歡迎你們關注。若是你們以爲本文還不錯的話,能夠點個star。

若是你但願瞭解更多

最後的最後,大家的star是我堅持的動力,若是大家以爲上述項目還不錯的話,能夠順手點個star。若是你以爲MVVMRecurve還不錯的話,除了點star外還能夠參與到這個開源項目中。若是你但願開發一個完整的項目的話,你能夠參與到GitHubRecurve中。筆者會持續維護這些項目而且會按期更新一些技術文章的,感謝能看到這裏的每一位讀寫。

相關文章
相關標籤/搜索