本文章已授權微信公衆號郭霖(guolin_blog)轉載。
本篇文章講解的內容是MVC、MVP、MVVM以及使用MVVM搭建GitHub客戶端,以下是框架的GitHub地址:
Dagger2版本:Dagger2
Koin版本:Koin
在講解之前,我想先聊一下MVC、MVP和MVVM相關的概念。
MVC
MVC(Model-View-Controller)的概念最早源自於Erich Gamma、Richard Helm、Raplph Johnson、John Vlissides這四位大牛在討論設計模式中的觀察者模式時的想法;Trygve Reenskaug在1979年5月的時候發表了一篇文章叫做Thing-Model-View-Editor,這篇文章中雖然沒提到Controller,但是他提到的Editor就是非常接近這個概念,7個月後,他在發表的一篇叫做Models-Views-Controllers中正式提出了MVC這個概念。
- Model(數據層):負責處理數據邏輯。
- View(視圖層):負責處理視圖顯示,在Android中使用xml描述視圖。
- Controller(控制層):在Android中的Activity和Fragment承擔此層的重任,負責處理業務邏輯。
這裏要注意的是,Activity和Fragment並非是標準的Controller,因爲它們不僅要負責處理業務邏輯,還要去控制界面顯示,這樣導致的結果是隨着業務的複雜度不斷提高,Activity和Fragment會變得非常臃腫,不利於代碼的維護。
MVP
MVP(Model-View-Presenter)是MVC進一步演化出來的,由Microsoft的Martin Fowler提出。
- Model(數據層):負責處理數據邏輯。
- View(視圖層):負責處理視圖顯示,在Android中使用xml或者Java/Kotlin代碼去實現視圖,Activity和Fragment承擔了此層的責任。
- Presenter:負責連接Model層和View層,是這兩層的中間紐帶,負責處理業務邏輯。
在MVP中,Model層和View層之間不能有交互,要通過Presenter層進行交互,其中View層和Presenter層是通過接口進行交互,可以定義Contract(契約)接口來指定View層和Presenter之間的契約,官方代碼如下:
interface AddEditTaskContract {
interface View : BaseView<Presenter> {
var isActive: Boolean
fun showEmptyTaskError()
fun showTasksList()
fun setTitle(title: String)
fun setDescription(description: String)
}
interface Presenter : BasePresenter {
var isDataMissing: Boolean
fun saveTask(title: String, description: String)
fun populateTask()
}
}
在MVP中,View層不會部署任何的業務邏輯,從而比較薄,它被稱爲被動視圖(Passive View),意思是它沒有任何的主動性,而且這樣的設計也方便做單元測試,但是也會有如下問題:
- 儘管減少了View層的代碼,但是隨着業務的複雜度不斷提高,Presenter層的代碼也會變得越來越臃腫。
- View層和Presenter層是通過接口交互的,隨着業務的複雜度不斷提高,接口數量會大量增加。
- 如果View層更新的話,就像UI的輸入和數據的變化,都需要主動去調用Presenter層的代碼,缺乏自動性和監聽性。
- MVP是以UI和事件爲驅動的傳統模型,更新UI需要保證能持有控件的引用,而且更新UI需要考慮Activity或者Fragment的生命週期,防止內存泄漏。
MVVM
MVVM(Model-View-ViewModel)是MVP進一步演化出來的,它也是由Microsoft的Martin Fowler提出。
- Model(數據層):負責處理數據邏輯。
- View(視圖層):負責處理視圖顯示,在Android中使用xml或者Java/Kotlin代碼去實現視圖,Activity和Fragment承擔了此層的責任。
- ViewModel:負責連接Model層和View層,是這兩層的中間紐帶,負責處理業務邏輯,View層和ViewModel層是雙向綁定的,View層的變動會自動反映在ViewModel層,ViewModel層的變動也會自動反映在View層。
使用MVVM後,每一層的職責也更加清晰了,也方便做單元測試,同時因爲View層和ViewModel層是雙向綁定,開發者不需要再去主動處理部分邏輯了,減少了不少膠水代碼,如果使用了一些數據綁定的庫,例如在Android中的DataBinding,可以減少更加多的膠水代碼。
實踐
我使用GitHub的API開發了一個簡單的客戶端,用MVVM來搭建,使用Kotlin編寫,界面如下圖所示:
登錄:
首頁:
個人中心:
架構設計
整體分爲六部分,每一部分都按業務邏輯區分:
data
data存放數據相關的代碼,如圖所示:
- local:本地數據,存放本地存儲邏輯(MMKV相關的邏輯),例如:UserLocalDataSource(用戶本地數據源)。
- model:數據類,存放請求數據類(request)和響應數據類(response),例如:LoginRequestData(登錄請求數據類)、UserAccessTokenData(用戶訪問Token數據類)、UserInfoData(用戶信息數據類)、ListData(基礎的列表數據類)和Repository(GitHub倉庫請求和響應數據類)。
- remote:遠程數據,存放網絡請求邏輯(OkHttp3和Retrofit2相關的邏輯),例如:UserRemoteDataSource(用戶遠程數據源)和RepositoryRemoteDataSource(GitHub倉庫遠程數據源)。
- repository:倉庫,例如:UserInfoRepository(用戶信息倉庫)和GitHubRepository(GitHub倉庫)。
Repository持有LocalDataSource(本地數據源)和RemoteDataSource(遠程數據源)的引用,暴露相關的數據出去,外界不必關心repository內部是如何處理數據的。
di
di存放依賴注入相關的代碼。
Dagger2版本:
如圖所示:
- ApplicationComponent:Application組件,將AndroidSupportInjectionModule、ApplicationModule、NetworkModule、RepositoryModule、MainModule、UserModule和GitHubRepositoryModule注入到Application。
- ApplicationModule:提供跟隨Application生命週期的業務模塊,例如:LocalDataSource(本地數據源)和RemoteDataSource(遠程數據源)。
- GitHubRepositoryModule:業務模塊,提供GitHub倉庫業務的模塊。
- MainModule:業務模塊,提供main(啓動頁和主頁)業務的模塊。
- NetworkModule:網絡模塊,例如:OkHttp3和Retrofit2。
- RepositoryModule:倉庫模塊,例如:UserInfoRepository(用戶信息倉庫)和GitHubRepository(GitHub倉庫)。
- UserModule:業務模塊,提供用戶業務的模塊。
- ViewModelFactory:ViewModel工廠,創建不同業務的ViewModel。
Koin版本:
如圖所示:
- ApplicationModule:存放ApplicationModule、NetworkModule、RepositoryModule、MainModule、UserModule和GitHubRepositoryModule,並且生成ApplicationModules的List提供Koin使用。
ui
ui存放UI相關的代碼,例如:Activity、Fragment、ViewModel和自定義View等等,如圖所示:
- main:main(啓動頁和主頁)相關的Activity和ViewModel代碼。
- recyclerview:RecyclerView相關的代碼,包括BaseViewHolder、BaseViewType、NoDataViewType、BaseDataBindingAdapter和MultiViewTypeDataBindingAdapter。
- repository:GitHub倉庫相關的Activity、Fragment、ViewModel和Adapter代碼。
- user:用戶相關的Activity、Fragment和ViewModel代碼。
- BaseActivity:Acitivity的基類。
- BaseFragment:Fragment的基類。
- BaseViewModel:ViewModel的基類。
- NoViewModel:一個繼承BaseViewModel的類,如果該Acitivity或者Fragment不需要用到ViewModel的話可以使用這個類。
ViewModel持有Repository的引用,從Repository拿到想要的數據;ViewModel不會持有任何View層(例如:Activity(包括xml)、Fragment(包括xml))的引用,通過雙向綁定框架(DataBinding)獲取View層反饋給ViewModel層的數據,並且對這些數據進行操作。
utils
utils存放工具文件,如圖所示:
- ActivityExt:存放Activity的擴展函數。
- BindingAdapters:存放使用DataBinding的**@BindingAdapters**註解的代碼。
- BooleanExt:存放Boolean的擴展函數,如果想深入瞭解的話,可以看下我這篇文章:Kotlin系列——泛型型變
- DateUtils:存放日期相關的代碼。
- FragmentExt:存放Fragment的擴展函數。
- GsonExt:存放Gson相關的擴展函數。
- Language:存放GitHub倉庫相關的名字和圖片。
- OnTabSelectedListenerBuilder:存放OnTabSelectedListener相關的代碼,用作使用DSL,如果想深入瞭解的話,可以看下我這篇文章:Kotlin系列——DSL
- Preferences:存放MMKV相關的代碼,如果想深入瞭解的話,可以看下我這篇文章:Kotlin系列——封裝MMKV及其相關Kotlin特性
- SingleLiveEvent:一個生命週期感知的觀察對象,在訂閱後只發送新的功能,可以用於導航和SnackBar消息等事件,它可以避免一個常見問題,就是如果觀察者處於活躍狀態,在配置更改(例如:旋轉)的時候是可以發射事件,這個類可以解決這個問題,它只在你顯式地調用setValue()方法或者call()方法,它纔會調用可觀察對象。
- ToastExt:存放Toast的擴展函數。
前綴AndroidGenericFramework的文件
如圖所示:
- AndroidGenericFrameworkAppGlideModule:定義在應用程序(Application)內初始化Glide時要使用的一組依賴項和選項,要注意的是,在一個應用程序(Application)中只能存在一個AppGlideModule,如果是庫(Libraries)就必須使用LibraryGlideModule。
- AndroidGenericFrameworkApplication:本框架的Application。
- AndroidGenericFrameworkConfiguration:存放本框架的配置信息。
- AndroidGenericFrameworkExtra:存放Activity和Fragment的附加數據的名稱。
- AndroidGenericFrameworkFragmentTag:存放Fragment的標記名,這個標記名是爲了以後使用FragmentManager的findFragmentByTag(String)方法的時候檢索Fragment。
單元測試
如圖所示:
- data:FakeDataSource用來創建假的數據源,UserRemoteDataSourceTest(用戶遠程數據源測試類)和RepositoryRemoteDataSourceTest(GitHub倉庫遠程數據源測試類)都是模擬API調用。
- utils:存放工具文件的測試類。
- viewmodel:存放ViewModel的測試類。
下面我來介紹下使用到的Android架構組件和庫。
OkHttp3和Retrofit2
網絡請求庫使用了基於OkHttp3封裝的Retrofit2,框架部分代碼如下:
// NetworkModule.kt
/**
* Created by TanJiaJun on 2020/4/4.
*/
@Suppress("unused")
@Module
open class NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(localDataSource: UserLocalDataSource): OkHttpClient =
OkHttpClient.Builder()
.connectTimeout(AndroidGenericFrameworkConfiguration.CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
.readTimeout(AndroidGenericFrameworkConfiguration.READ_TIMEOUT, TimeUnit.MILLISECONDS)
.addInterceptor(BasicAuthInterceptor(localDataSource))
.build()
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit =
Retrofit.Builder()
.client(client)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(String.format("%1\$s://%2\$s/", "https", AndroidGenericFrameworkConfiguration.HOST))
.build()
}
Retrofit2.6以後支持Kotlin的協程,和舊版本有如下區別:
- 可以直接作用於掛起函數(suspend fun)。
- 可以直接返回我們想要的數據對象,而不再返回**Deferred**對象。
- 不再需要調用協程中await函數,因爲Retrofit已經幫我們調用了。
框架部分代碼如下:
// RepositoryRemoteDataSource.kt
interface Service {
@GET("search/repositories")
suspend fun fetchRepositories(@Query("q") query: String,
@Query("sort") sort: String = "stars"): ListData<RepositoryResponseData>
}
Glide v4
圖片加載庫使用了Glide v4,我這裏用到DataBinding組件中的**@BindingAdapter**註解,框架部分代碼如下:
// BindingAdapters.kt
@BindingAdapter(value = ["url", "placeholder", "error"], requireAll = false)
fun ImageView.loadImage(url: String?, placeholder: Drawable?, error: Drawable?) =
Glide
.with(context)
.load(url)
.placeholder(placeholder ?: context.getDrawable(R.mipmap.ic_launcher))
.error(error ?: context.getDrawable(R.mipmap.ic_launcher))
.transition(DrawableTransitionOptions.withCrossFade(DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build()))
.into(this)
Android Jetpack
Android Jetpack是一套庫、工具和指南,可以幫助開發者更輕鬆地編寫優質應用,這些組件可以幫助開發者遵循最佳做法,讓開發者擺脫編寫樣板代碼的工作,並且簡化複雜任務,以便開發者將精力集中放在所需的代碼上。我使用了DataBinding、Lifecycle、LiveData、ViewModel,下面我大概地介紹下。
DataBinding
DataBinding是實現MVVM的核心架構組件,它有如下優點:
- 可以降低佈局和邏輯的耦合度,使代碼邏輯更加清晰。
- 可以省去findViewById這樣的代碼,大量減少View層的代碼。
- 數據能單向和雙向綁定到layout文件。
- 能夠自動進行空判斷,可以避免空指針異常。
框架部分代碼如下:
<!-- activity_personal_center.xml -->
<ImageView
android:id="@+id/iv_head_portrait"
error="@{@drawable/ic_default_avatar}"
placeholder="@{@drawable/ic_default_avatar}"
url="@{viewModel.avatarUrl}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:contentDescription="@string/head_portrait"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider_line"
tools:background="@drawable/ic_default_avatar" />
Lifecycle
Lifecycle組件可以執行操作來響應Activity和Fragment的生命週期狀態的變化。
LiveData和ViewModel都使用到Lifecycle組件,框架部分代碼如下:
// LoginFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) =
with(binding) {
lifecycleOwner = this@LoginFragment
viewModel = this@LoginFragment.viewModel
handlers = this@LoginFragment
}.also {
registerLoadingProgressBarEvent()
registerSnackbarEvent()
observe()
}
我們看下ViewDataBinding的setLifecycleOwner方法,代碼如下:
// ViewDataBinding.java
@MainThread
public void setLifecycleOwner(@Nullable LifecycleOwner lifecycleOwner) {
if (mLifecycleOwner == lifecycleOwner) {
return;
}
if (mLifecycleOwner != null) {
mLifecycleOwner.getLifecycle().removeObserver(mOnStartListener);
}
mLifecycleOwner = lifecycleOwner;
if (lifecycleOwner != null) {
if (mOnStartListener == null) {
mOnStartListener = new OnStartListener(this);
}
lifecycleOwner.getLifecycle().addObserver(mOnStartListener);
}
for (WeakListener<?> weakListener : mLocalFieldObservers) {
if (weakListener != null) {
weakListener.setLifecycleOwner(lifecycleOwner);
}
}
}
這裏的LifecyclerOwner是一個具有Android生命週期的類,自定義組件可以使用它的事件來處理生命週期更改,而無需在Activity或者Fragment實現任何代碼。
LiveData
LiveData是一種可觀察的數據存儲器類,它具有生命週期感知能力,遵循應用組件(例如:Activity、Fragment、Service(可以使用LifecycleService,它是實現了LifecycleOwner接口的Service))的生命週期,這種感知能力確保LiveData僅更新處於活躍生命週期狀態的應用組件觀察者。
我之前寫過一篇關於LiveData的文章,大家可以閱讀一下:
Android Jetpack系列——LiveData源碼分析
框架部分代碼如下:
// LoginViewModel.kt
val username = MutableLiveData<String>()
val password = MutableLiveData<String>()
private val _isLoginEnable = MutableLiveData<Boolean>()
val isLoginEnable: LiveData<Boolean> = _isLoginEnable
val isLoginSuccess = MutableLiveData<Boolean>()
fun checkLoginEnable() {
_isLoginEnable.value = !username.value.isNullOrEmpty() && !password.value.isNullOrEmpty()
}
ViewModel
ViewModel是一個負責準備和管理Activity或者Fragment的類,它還可以處理Activity和Fragment與應用程序其餘部分的通信(例如:調用業務邏輯類)。
ViewModel總是在一個Activity或者一個Fragment創建的,並且只要對應的Activity或者Fragment處於活動狀態的話,它就會被保留(例如:如果它是個Activity,就會直到它finished)。
換句話說,這意味着一個ViewModel不會因爲配置的更改(例如:旋轉)而被銷燬,所有的新實例將被重新連接到現有的ViewModel。
ViewModel的目的是獲取和保存Activity或者Fragment所需的信息,Activity或者Fragment應該能夠觀察到ViewModel中的變化,通常通過LiveData或者Android Data Binding公開這些信息。
我之前寫過一篇關於ViewModel的文章,大家可以閱讀一下:
Android Jetpack系列——ViewModel源碼分析
框架部分代碼如下:
// RepositoryViewModel.kt
/**
* Created by TanJiaJun on 2020-02-07.
*/
class RepositoryViewModel @Inject constructor(
private val repository: GitHubRepository
) : BaseViewModel() {
private val _isShowRepositoryView = MutableLiveData<Boolean>()
val isShowRepositoryView: LiveData<Boolean> = _isShowRepositoryView
private val _repositories = MutableLiveData<List<RepositoryData>>()
val repositories: LiveData<List<RepositoryData>> = _repositories
fun getRepositories(languageName: String) =
launch(
uiState = UIState(isShowLoadingView = true, isShowErrorView = true),
block = { repository.getRepositories(languageName) },
success = {
if (it.isNotEmpty()) {
_repositories.value = it
_isShowRepositoryView.value = true
}
}
)
}
協程
協程源自Simula和Modula-2語言,它是一種編程思想,並不侷限於特定的語言,在1958年的時候,Melvin Edward Conway提出這個術語並用於構建彙編程序。在Android中使用它可以簡化異步執行的代碼,它是在版本1.3中添加到Kotlin。
在Android平臺上,協程有助於解決兩個主要問題:
- 管理長時間運行的任務,如果管理不當,這些任務可能會阻塞主線程並導致你的應用界面凍結。
- 提供主線程安全性,或者從主線程安全地調用網絡或者磁盤操作。
管理長時間運行的任務
在Android平臺上,每個應用都有一個用於處理界面並且管理用戶交互的主線程。如果你的應用爲主線程分配的工作太多,會導致界面呈現速度緩慢或者界面凍結,對觸摸事件的響應速度很慢,例如:網絡請求、JSON解析、寫入或者讀取數據庫、遍歷大型列表,這些都應該在工作線程完成。
協程在常規函數的基礎上添加了兩項操作,用於處理長時間運行的任務。在invoke或者call和return之外,協程添加了suspend和resume:
- suspend用於暫停執行當前協程,並保存所有的局部變量。
- resume用於讓已暫停的協程從其暫停處繼續執行。
要調用suspend函數,只能從其他suspend函數進行調用,或者通過使用協程構建器(例如:launch)來啓動新的協程。
Kotin使用堆棧幀來管理要運行哪個函數以及所有的局部變量。暫停協程時會複製並保存當前的堆棧幀以供稍後使用;恢復協程時會將堆棧幀從其保存位置複製回來,然後函數再次開始運行。
使用協程確保主線程安全
Kotlin協程使用調度程序來確定哪些線程用於執行協程,所有協程都必須在調度程序中運行,協程可以自行暫停,而調度程序負責將其恢復。
Kotlin提供了三個調度程序,可以使用它們來指定應在何處運行協程:
- Dispatchers.Main:使用此調度程序可在Android主線程上運行協程,只能用於界面交互和執行快速工作,例如:調用suspend函數、運行Android界面框架操作和更新LiveData對象。
- Dispatchers.IO:此調度程序適合在主線程之外執行磁盤或者網絡I/O,例如:操作數據庫(使用Room)、向文件中寫入數據或者從文件中讀取數據和運行任何網絡操作。
- Dispatcher.Default:此調度程序適合在主線程之外執行佔用大量CPU資源的工作,例如:對列表排序和解析JSON。
指定CoroutineScope
在定義協程時,必須指定其CoroutineScope,CoroutineScope可以管理一個或者多個相關的協程,可以使用它在指定範圍內啓動新協程。
與調度程序不同,CoroutineScope不運行協程。
CoroutineScope的一項重要功能就是在用戶離開應用中內容區域時停止執行協程,可以確保所有正在運行的操作都能正確停止。
在Android平臺上,可以將CoroutineScope實現與組件的生命週期相關聯,例如:Lifecycle和ViewModel,這樣可以避免內存泄漏和不再對與用戶相關的Activity或者Fragment執行額外的工作。
啓動協程
可以通過以下兩種方式來啓動協程:
- launch:可以啓動新協程,但是不將結果返回給調用方。
- async:可以啓動新協程,並且允許使用await暫停函數返回結果。
同時我還使用了Kotlin的流(Flow),它的設計靈感來源於響應式流(Reactive Streams),所以如果開發者熟悉RxJava的話,也應該很快就能熟悉它。
我之前寫過幾篇關於RxJava的文章,大家可以閱讀一下:
RxJava2源碼分析——FlatMap和ConcatMap及其相關併發編程分析
框架部分代碼如下:
// LoginViewModel.kt
@ExperimentalCoroutinesApi
@FlowPreview
fun login() =
launchUI {
launchFlow {
repository.run {
cacheUsername(username.value ?: "")
cachePassword(password.value ?: "")
authorizations()
}
}
.flatMapMerge {
launchFlow { repository.getUserInfo() }
}
.flowOn(Dispatchers.IO)
.onStart { uiLiveEvent.showLoadingProgressBarEvent.call() }
.catch {
val responseThrowable = ExceptionHandler.handleException(it)
uiLiveEvent.showSnackbarEvent.value = "${responseThrowable.errorCode}:${responseThrowable.errorMessage}"
}
.onCompletion { uiLiveEvent.dismissLoadingProgressBarEvent.call() }
.collect {
repository.run {
cacheUserId(it.id)
cacheName(it.login)
cacheAvatarUrl(it.avatarUrl)
}
isLoginSuccess.value = true
}
}
Dagger2
Dagger2是針對Java和Android的全靜態、編譯階段完成依賴注入的框架。
Dagger這個庫的取名不僅僅是來自它的本意——匕首,Jake Wharton在介紹Dagger的時候指出,Dagger的意思是DAG-er,DAG的意思有向無環圖(Directed Acyclic Graph),也就是說Dagger是一個基於有向無環圖結構的依賴注入庫,因此Dagger在使用過程中不能出現循環依賴。
Square公司受到Guice的啓發開發了Dagger,它是一種半靜態、半運行時的依賴注入框架,雖然說依賴注入是完全靜態的,但是生成有向無環圖還是基於反射來實現,這無論在大型服務端應用或者Android應用上都不是最優方案,然後Google的工程師fork了這個項目後,受到AutoValue項目的啓發,對其進行改造,就有了現在這個Dagger2,Dagger2和Dagger比較的話,有如下區別:
- 更好的性能:Google聲稱提高了13%的處理性能,沒有使用反射生成有向無環圖,而是在編譯階段生成。
- 更高效和優雅,而且更容易調試:作爲升級版的Dagger,從半靜態變成完全靜態,從Map式API變成申明式API(例如:@Module),生成的代碼更加高效和優雅,一旦出錯在編譯階段就能發現。
因爲Dagger2沒使用反射,缺乏動態機制,所以喪失一定的靈活性,但是總體來說是利遠遠大於弊的。
我在主分支(master)使用的是Dagger2和相關的Dagger-Android,框架部分代碼如下:
// ApplicationComponent.kt
/**
* Created by TanJiaJun on 2020/3/4.
*/
@Singleton
@Component(
modules = [
AndroidSupportInjectionModule::class,
ApplicationModule::class,
NetworkModule::class,
RepositoryModule::class,
MainModule::class,
UserModule::class,
GitHubRepositoryModule::class
]
)
interface ApplicationComponent : AndroidInjector<AndroidGenericFrameworkApplication> {
@Component.Factory
interface Factory {
fun create(@BindsInstance applicationContext: Context): ApplicationComponent
}
}
Koin
Koin是一個面向Kotlin開發人員實用的輕量級依賴注入框架。
官方聲稱是用純Kotlin編寫,只使用函數解析,沒有代理、沒有代碼生成、沒有反射。
我在分支mvvm-koin使用的是Koin,框架部分代碼如下:
// ApplicationModule.kt
/**
* Created by TanJiaJun on 2020/5/5.
*/
val applicationModule = module {
single {
UserLocalDataSource(MMKV.mmkvWithID(
AndroidGenericFrameworkConfiguration.MMKV_ID,
MMKV.SINGLE_PROCESS_MODE,
AndroidGenericFrameworkConfiguration.MMKV_CRYPT_KEY
))
}
single { UserRemoteDataSource(get()) }
single { RepositoryRemoteDataSource(get()) }
}
val networkModule = module {
single<OkHttpClient> {
OkHttpClient.Builder()
.connectTimeout(AndroidGenericFrameworkConfiguration.CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
.readTimeout(AndroidGenericFrameworkConfiguration.READ_TIMEOUT, TimeUnit.MILLISECONDS)
.addInterceptor(BasicAuthInterceptor(get()))
.build()
}
single<Retrofit> {
Retrofit.Builder()
.client(get())
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(String.format("%1\$s://%2\$s/", SCHEMA_HTTPS, AndroidGenericFrameworkConfiguration.HOST))
.build()
}
}
val repositoryModule = module {
single { UserInfoRepository(get(), get()) }
single { GitHubRepository(get()) }
}
val mainModule = module {
scope<SplashActivity> {
viewModel { SplashViewModel(get()) }
}
scope<MainActivity> {
viewModel { MainViewModel(get()) }
}
}
val userModule = module {
scope<LoginFragment> {
viewModel { LoginViewModel(get()) }
}
scope<PersonalCenterActivity> {
viewModel { PersonalCenterViewModel(get()) }
}
}
val githubRepositoryModule = module {
scope<RepositoryFragment> {
viewModel { RepositoryViewModel(get()) }
}
}
val applicationModules = listOf(
applicationModule,
networkModule,
repositoryModule,
mainModule,
userModule,
githubRepositoryModule
)
private const val SCHEMA_HTTPS = "https"
MMKV
MMKV是基於mmap內存映射的key-value組件,底層序列化/反序列化使用protobuf實現,性能高,穩定性強,而且Android這邊還支持多進程。
我之前寫過一篇關於MMKV的文章,大家可以閱讀一下:
我使用MMKV代替Android組件中的SharedPreferences,作爲本地存儲數據組件,框架部分代碼如下:
// Preferences.kt
/**
* Created by TanJiaJun on 2020-01-11.
*/
private inline fun <T> MMKV.delegate(
key: String? = null,
defaultValue: T,
crossinline getter: MMKV.(String, T) -> T,
crossinline setter: MMKV.(String, T) -> Boolean
): ReadWriteProperty<Any, T> =
object : ReadWriteProperty<Any, T> {
override fun getValue(thisRef: Any, property: KProperty<*>): T =
getter(key ?: property.name, defaultValue)
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
setter(key ?: property.name, value)
}
}
fun MMKV.boolean(
key: String? = null,
defaultValue: Boolean = false
): ReadWriteProperty<Any, Boolean> =
delegate(key, defaultValue, MMKV::decodeBool, MMKV::encode)
fun MMKV.int(key: String? = null, defaultValue: Int = 0): ReadWriteProperty<Any, Int> =
delegate(key, defaultValue, MMKV::decodeInt, MMKV::encode)
fun MMKV.long(key: String? = null, defaultValue: Long = 0L): ReadWriteProperty<Any, Long> =
delegate(key, defaultValue, MMKV::decodeLong, MMKV::encode)
fun MMKV.float(key: String? = null, defaultValue: Float = 0.0F): ReadWriteProperty<Any, Float> =
delegate(key, defaultValue, MMKV::decodeFloat, MMKV::encode)
fun MMKV.double(key: String? = null, defaultValue: Double = 0.0): ReadWriteProperty<Any, Double> =
delegate(key, defaultValue, MMKV::decodeDouble, MMKV::encode)
private inline fun <T> MMKV.nullableDefaultValueDelegate(
key: String? = null,
defaultValue: T?,
crossinline getter: MMKV.(String, T?) -> T,
crossinline setter: MMKV.(String, T) -> Boolean
): ReadWriteProperty<Any, T> =
object : ReadWriteProperty<Any, T> {
override fun getValue(thisRef: Any, property: KProperty<*>): T =
getter(key ?: property.name, defaultValue)
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
setter(key ?: property.name, value)
}
}
fun MMKV.byteArray(
key: String? = null,
defaultValue: ByteArray? = null
): ReadWriteProperty<Any, ByteArray> =
nullableDefaultValueDelegate(key, defaultValue, MMKV::decodeBytes, MMKV::encode)
fun MMKV.string(key: String? = null, defaultValue: String? = null): ReadWriteProperty<Any, String> =
nullableDefaultValueDelegate(key, defaultValue, MMKV::decodeString, MMKV::encode)
fun MMKV.stringSet(
key: String? = null,
defaultValue: Set<String>? = null
): ReadWriteProperty<Any, Set<String>> =
nullableDefaultValueDelegate(key, defaultValue, MMKV::decodeStringSet, MMKV::encode)
inline fun <reified T : Parcelable> MMKV.parcelable(
key: String? = null,
defaultValue: T? = null
): ReadWriteProperty<Any, T> =
object : ReadWriteProperty<Any, T> {
override fun getValue(thisRef: Any, property: KProperty<*>): T =
decodeParcelable(key ?: property.name, T::class.java, defaultValue)
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
encode(key ?: property.name, value)
}
}
可以這樣使用,框架部分代碼如下:
// UserLocalDataSource.kt
var accessToken by mmkv.string("user_access_token", "")
var userId by mmkv.int("user_id", -1)
var username by mmkv.string("username", "")
var password by mmkv.string("password", "")
var name by mmkv.string("name", "")
var avatarUrl by mmkv.string("avatar_url", "")
ViewPager2
框架中在展示GitHub的倉庫的時候用到了ViewPager2,比起ViewPager,有以下幾個好處:
-
支持垂直方向分頁:ViewPager2除了支持水平方向分頁,也支持垂直方向分頁,可以通過android:orientation屬性或者setOrientation()方法來啓動垂直分頁,代碼如下:
android:orientation="vertical"
-
支持從右到做(RTL):ViewPager2會根據語言環境自動啓動從右到做(RTL)分頁,可以通過設置android:layoutDirection屬性或者setLayoutDirection()方法來啓動RTL分頁,代碼如下:
android:layoutDirection="rtl"
框架部分代碼如下:
<!-- activity_main.xml -->
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/vp_repository"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tl_repository" />
MockK
MockK一個專門爲Kotlin這門語言打造的測試框架。在Java中,我們常用的是Mockito,但是如果我們使用Kotlin的話,就會遇到一些問題,常見的問題如下:
-
不能測試靜態方法:可以使用PowerMock解決。
-
Mockito cannot mock/spy because:-final class:這是因爲在Kotlin中任何類預設都是final的,Mockito預設情況下不能mock一個final的類。
-
java.lang.illegalStateException:anyObjecet() must not be null:如果我們使用eq()、any()、capture()和argumentCaptor()的話就會遇到這個問題了,因爲這些方法返回的對象可能是null,如果作用在一個非空的參數的話,就會報這個異常了,解決辦法是可以使用如下文件:
-
when要加上反引號才能使用:因爲when是Kotlin中的關鍵字。
Kotlin和Mockito同時使用會有如上說的種種不便,最後我決定使用MockK這個庫,我使用的測試相關的庫如下:
// build.gradle(:app)
testImplementation "junit:junit:$junitVersion"
testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion"
testImplementation "io.mockk:mockk:$mockkVersion"
testImplementation "com.google.truth:truth:$truthVersion"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion"
testImplementation "android.arch.core:core-testing:$coreTestingVersion"
-
com.squareup.okhttp3:mockwebserver:用來模擬Web服務器的。
-
com.google.truth:truth:可以使測試斷言和失敗消息更具有可讀性,與AssertJ相似,它支持很多JDK和Guava類型,並且可以擴展到其他類型。
我這邊是對數據源、ViewModel和工具文件進行單元測試。
框架部分代碼如下:
// LoginViewModelTest.kt
@ExperimentalCoroutinesApi
@FlowPreview
@Test
fun login_success() {
runBlocking {
viewModel.username.value = "[email protected]"
viewModel.password.value = "password"
coEvery { repository.authorizations() } returns userAccessTokenData
coEvery { repository.getUserInfo() } returns userInfoData
viewModel.login()
val observer = mockk<Observer<Boolean>>(relaxed = true)
viewModel.isLoginSuccess.observeForever(observer)
viewModel.viewModelScope.coroutineContext[Job]?.children?.forEach { it.join() }
verify { observer.onChanged(match { it }) }
}
}
@ExperimentalCoroutinesApi
@FlowPreview
@Test
fun login_failure() {
runBlocking {
viewModel.username.value = "[email protected]"
viewModel.password.value = "password"
coEvery { repository.authorizations() } returns userAccessTokenData
coEvery { repository.getUserInfo() } throws Throwable("UnknownError")
viewModel.login()
val observer = mockk<Observer<String>>(relaxed = true)
viewModel.uiLiveEvent.showSnackbarEvent.observeForever(observer)
viewModel.viewModelScope.coroutineContext[Job]?.children?.forEach { it.join() }
verify { observer.onChanged(match { it == "0:UnknownError" }) }
}
}
我的GitHub:TanJiaJunBeyond
Android通用框架:Android通用框架
我的掘金:譚嘉俊
我的簡書:譚嘉俊
我的CSDN:譚嘉俊