本文章已授权微信公众号郭霖(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<T>对象。
- 不再需要调用协程中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对象。
- Dispatcher.Default:此调度程序适合在主线程之外执行占用大量CPU资源的工作,例如:对列表排序和解析JSON。
- Dispatchers.IO:此调度程序适合在主线程之外执行磁盘或者网络I/O,例如:操作数据库(使用Room)、向文件中写入数据或者从文件中读取数据和运行任何网络操作。
指定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 = "1120571286@qq.com"
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 = "1120571286@qq.com"
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:谭嘉俊