Android開發架構已經由由最最初的Activity架構(MVC),發展到到如今主流的MVP、MVVM架構了。社區也有很多優秀的實踐。今天筆者想結合本身的經驗談一談,一個合理的Android架構應該是怎麼樣的呢?java
相信一些經驗豐富的開發者,都經歷過面向Activity(Fragment)編程的時代,也就是所謂的MVC架構時代。那個時代,在開發的時候,會把咱們的業務模塊氛圍三層。以下圖:git
若是咱們能嚴格按照上圖來開發咱們的架構的話,那麼這會是一種比較好的實現。可是這種架構在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都是這種模型的結果。編程
在經歷了維護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和MVC在架構上來講實際上是很是像的。最大的不一樣點就是在MVP中,V層和M層是不能直接交互的。MVP雖然能把咱們的業務分割成三個相互獨立的部分,可是MVP的缺點也很是明顯。模版代碼很是多、基於接口的通信方式會致使咱們定義大量一次性接口。而且,P層和V層的生命週期是不一致的,會存在P層還存活,V層被回收的狀況。這樣會致使UI更新失敗,很容易Crash。服務器
爲了分割邏輯,增長這麼多代碼與一次性接口是否值得,這是一個須要充分考慮的問題。網絡
由於MVP樣板代碼太多的緣由,因此咱們又開始嘗試MVVM架構了。Google也推出了很多輔助工具幫助咱們在Android中實現MVVM架構,如:DataBinding,LiveData,ViewModel系列等。MVVM架構的核心就是數據驅動,數據驅動的意思就是,數據更新的時候,自動刷新UI。架構
打個比喻:
咱們把View比做一個罪犯,更新View的行爲比做是把罪犯送進監獄的話。那麼在MVC、MVP中,咱們C,和P就是警察,他要負責把罪犯送進監獄。而在MVVM中,罪犯犯罪(數據變化)後,它本身會走進監獄裏面。
採用MVVM架構會幫咱們節省大量的更新UI的代碼,而且數據更新後主動出發UI更新這種方式,更難出錯,魯棒性更強。咱們不須要關注數據變化的時機,是須要關注數據變化的結果便可。MVVM架構圖以下:
MVVM架構,由於有官方Jetpack的加持,因此逐漸成爲了架構中的主流,若是是新的項目開發的話,能夠考慮採用MVVM架構。而針對老業務的新模塊,也能夠嘗試過分到MVVM架構中。
經過分析,上面三種架構都是爲了解決一個問題的,就是把UI、邏輯、數據層分開,實現解耦。這三種架構其實沒有優劣之分,實現的方式纔有優劣之分。MVVM架構由於有官方組件加持,因此實現中通常推薦使用MVVM架構。那麼架構單單隻限於分層嗎?
不是的,由於實際開發中,咱們面對的業務場景是很是複雜的,除了把業務分層以外,咱們還須要關注:緩存、限流、分頁、領域設計(本文不會過於關注)等。下面先簡單介紹下緩存和限流這兩個場景。
咱們先來關注一個Android中常常遇到的一個問題,緩存問題。在Android開發中,大部分場景中,咱們並不會緩存數據,就算要緩存數據,大部分場景咱們都會使用SharedPreferences來實現。不管是不緩存數據,仍是大量使用SharedPreferences緩存數據,其實都會帶來一些問題。
大部分狀況下,咱們從服務器中請求的數據在某個時間段內都是同樣的。例如,咱們請求我的信息接口,如今請求和一分鐘後請求,極可能數據都是同樣的。因此若是咱們能把第一次請求的數據緩存下來,那麼第二次數據咱們就不必重複請求了。這個就是緩存的做用,固然緩存還存在超時時間,這個超時時間的須要根據具體業務來設置。並且緩存還能讓咱們在網絡異常的時候,讓用戶看到他上一次看到的數據。因此一個設計良好的APP,它確定會考慮到緩存的設計的。
而SharedPreferences的問題就是它的效率問題,由於它是基於文件的實現數據持久化的,而且讀取/寫入數據的時候都是全量讀寫的,因此大量使用SharedPreferences實現緩存也是不合適的。
限流的主要做用是,限制客戶端在短期內發起大量重複請求,致使後臺的流量洪峯問題。通常來講,咱們能夠經過限制重複點擊的時間間隔來實現限流。可是這種方案一是會侵入咱們的UI實現,二是有部分場景的請求不是由UI發起的。
咱們在解決上述問題的時候,能夠說是一千我的眼中有一千個哈姆雷特了,每一個人的可能都實現都不同。並且有些實現能夠說是比較糟糕的。那麼咱們能不能制定個比較統一的機制來解決上述問題呢?答案是能夠的,筆者在閱讀了Google的某個開源項目後,發現其實官方很早就提供了一個很是優秀的實現機制。下面咱們就來分享下這一套方法論,這個機制是基於MVVM架構的。
能夠看到,一個知足咱們要求的Repository仍是很是複雜的,若是要在每個Repository都實現這麼一套邏輯實際上是不現實。不過這種通用的邏輯咱們能夠封裝起來,咱們先來看看封裝好後的Rpository是怎麼樣的。
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這個類其實就是封裝了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>>
}
複製代碼
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層咱們就很少作介紹了,你能夠經過觀察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。
若是你想了解屏幕適配的話,能夠點擊:calces-gradle-plugin
若是你想了解jetpack以及一些新技術的使用和demo的話,能夠點擊:Android-advanced-blueprint
最後的最後,大家的star是我堅持的動力,若是大家以爲上述項目還不錯的話,能夠順手點個star。若是你以爲MVVMRecurve還不錯的話,除了點star外還能夠參與到這個開源項目中。若是你但願開發一個完整的項目的話,你能夠參與到GitHubRecurve中。筆者會持續維護這些項目而且會按期更新一些技術文章的,感謝能看到這裏的每一位讀寫。