Android通用框架
No Data
本文章已授权微信公众号郭霖(guolin_blog)转载。
本文章讲解的内容是MVC、MVP、MVVM以及使用MVVM搭建GitHub客户端,以下是框架的GitHub地址:
Dagger2版本:Dagger2
Koin版本:Koin
在讲解之前,我想先聊一下MVC、MVP和MVVM相关的概念。
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这个概念。
这里要注意的是,Activity和Fragment并非是标准的Controller,因为它们不仅要负责处理业务逻辑,还要去控制界面显示,这样导致的结果是随着业务的复杂度不断提高,Activity和Fragment会变得非常臃肿,不利于代码的维护。
MVP(Model-View-Presenter)是MVC进一步演化出来的,由Microsoft的Martin Fowler提出。
在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),意思是它没有任何的主动性,而且这样的设计也方便做单元测试,但是也会有如下问题:
MVVM(Model-View-ViewModel)是MVP进一步演化出来的,它也是由Microsoft的Martin Fowler提出。
使用MVVM后,每一层的职责也更加清晰了,也方便做单元测试,同时因为View层和ViewModel层是双向绑定,开发者不需要再去主动处理部分逻辑了,减少了不少胶水代码,如果使用了一些数据绑定的库,例如在Android中的DataBinding,可以减少更加多的胶水代码。
我使用GitHub的API开发了一个简单的客户端,用MVVM来搭建,使用Kotlin编写,界面如下图所示:
登录:
首页:
个人中心:
整体分为六部分,每一部分都按业务逻辑区分:
data存放数据相关的代码,如图所示:
Repository持有LocalDataSource(本地数据源)和RemoteDataSource(远程数据源)的引用,暴露相关的数据出去,外界不必关心repository内部是如何处理数据的。
di存放依赖注入相关的代码。
Dagger2版本:
如图所示:
Koin版本:
如图所示:
ui存放UI相关的代码,例如:Activity、Fragment、ViewModel和自定义View等等,如图所示:
ViewModel持有Repository的引用,从Repository拿到想要的数据;ViewModel不会持有任何View层(例如:Activity(包括xml)、Fragment(包括xml))的引用,通过双向绑定框架(DataBinding)获取View层反馈给ViewModel层的数据,并且对这些数据进行操作。
utils存放工具文件,如图所示:
如图所示:
如图所示:
下面我来介绍下使用到的Android架构组件和库。
网络请求库使用了基于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的协程,和旧版本有如下区别:
框架部分代码如下:
// RepositoryRemoteDataSource.kt interface Service {@GET("search/repositories") suspend fun fetchRepositories(@Query("q") query: String, @Query("sort") sort: String = "stars"): ListData<repositoryresponsedata>
}
图片加载库使用了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是一套库、工具和指南,可以帮助开发者更轻松地编写优质应用,这些组件可以帮助开发者遵循最佳做法,让开发者摆脱编写样板代码的工作,并且简化复杂任务,以便开发者将精力集中放在所需的代码上。我使用了DataBinding、Lifecycle、LiveData、ViewModel,下面我大概地介绍下。
DataBinding是实现MVVM的核心架构组件,它有如下优点:
框架部分代码如下:
Lifecycle组件可以执行操作来响应Activity和Fragment的生命周期状态的变化。
LiveData和ViewModel都使用到Lifecycle组件,框架部分代码如下:
// LoginFragment.kt override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) { lifecycleOwner = [email protected] viewModel = [email protected] handlers = [email protected] }.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是一种可观察的数据存储器类,它具有生命周期感知能力,遵循应用组件(例如:Activity、Fragment、Service(可以使用LifecycleService,它是实现了LifecycleOwner接口的Service))的生命周期,这种感知能力确保LiveData仅更新处于活跃生命周期状态的应用组件观察者。
我之前写过一篇关于LiveData的文章,大家可以阅读一下:
Android Jetpack系列——LiveData源码分析
框架部分代码如下:
// LoginViewModel.kt val username = MutableLiveData() val password = MutableLiveData()private val _isLoginEnable = MutableLiveData() val isLoginEnable: LiveData = _isLoginEnable
val isLoginSuccess = MutableLiveData()
fun checkLoginEnable() { _isLoginEnable.value = !username.value.isNullOrEmpty() && !password.value.isNullOrEmpty() }
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>>() val repositories: LiveData<list>> = _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函数,只能从其他suspend函数进行调用,或者通过使用协程构建器(例如:launch)来启动新的协程。
Kotin使用堆栈帧来管理要运行哪个函数以及所有的局部变量。暂停协程时会复制并保存当前的堆栈帧以供稍后使用;恢复协程时会将堆栈帧从其保存位置复制回来,然后函数再次开始运行。
Kotlin协程使用调度程序来确定哪些线程用于执行协程,所有协程都必须在调度程序中运行,协程可以自行暂停,而调度程序负责将其恢复。
Kotlin提供了三个调度程序,可以使用它们来指定应在何处运行协程:
在定义协程时,必须指定其CoroutineScope,CoroutineScope可以管理一个或者多个相关的协程,可以使用它在指定范围内启动新协程。
与调度程序不同,CoroutineScope不运行协程。
CoroutineScope的一项重要功能就是在用户离开应用中内容区域时停止执行协程,可以确保所有正在运行的操作都能正确停止。
在Android平台上,可以将CoroutineScope实现与组件的生命周期相关联,例如:Lifecycle和ViewModel,这样可以避免内存泄漏和不再对与用户相关的Activity或者Fragment执行额外的工作。
可以通过以下两种方式来启动协程:
同时我还使用了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是针对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比较的话,有如下区别:
因为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 {@Component.Factory interface Factory { fun create(@BindsInstance applicationContext: Context): ApplicationComponent }
}
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.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 { viewModel { SplashViewModel(get()) } }
scope<mainactivity> { viewModel { MainViewModel(get()) } }
}
val userModule = module { scope { viewModel { LoginViewModel(get()) } }
scope<personalcenteractivity> { viewModel { PersonalCenterViewModel(get()) } }
}
val githubRepositoryModule = module { scope { viewModel { RepositoryViewModel(get()) } } }
val applicationModules = listOf( applicationModule, networkModule, repositoryModule, mainModule, userModule, githubRepositoryModule )
private const val SCHEMA_HTTPS = "https"
MMKV是基于mmap内存映射的key-value组件,底层序列化/反序列化使用protobuf实现,性能高,稳定性强,而且Android这边还支持多进程。
我之前写过一篇关于MMKV的文章,大家可以阅读一下:
我使用MMKV代替Android组件中的SharedPreferences,作为本地存储数据组件,框架部分代码如下:
// Preferences.kt /** * Created by TanJiaJun on 2020-01-11. */ private inline fun MMKV.delegate( key: String? = null, defaultValue: T, crossinline getter: MMKV.(String, T) -> T, crossinline setter: MMKV.(String, T) -> Boolean ): ReadWriteProperty = object : ReadWriteProperty { 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 = delegate(key, defaultValue, MMKV::decodeBool, MMKV::encode)
fun MMKV.int(key: String? = null, defaultValue: Int = 0): ReadWriteProperty = delegate(key, defaultValue, MMKV::decodeInt, MMKV::encode)
fun MMKV.long(key: String? = null, defaultValue: Long = 0L): ReadWriteProperty = delegate(key, defaultValue, MMKV::decodeLong, MMKV::encode)
fun MMKV.float(key: String? = null, defaultValue: Float = 0.0F): ReadWriteProperty = delegate(key, defaultValue, MMKV::decodeFloat, MMKV::encode)
fun MMKV.double(key: String? = null, defaultValue: Double = 0.0): ReadWriteProperty = delegate(key, defaultValue, MMKV::decodeDouble, MMKV::encode)
private inline fun MMKV.nullableDefaultValueDelegate( key: String? = null, defaultValue: T?, crossinline getter: MMKV.(String, T?) -> T, crossinline setter: MMKV.(String, T) -> Boolean ): ReadWriteProperty = object : ReadWriteProperty { 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 = nullableDefaultValueDelegate(key, defaultValue, MMKV::decodeBytes, MMKV::encode)
fun MMKV.string(key: String? = null, defaultValue: String? = null): ReadWriteProperty = nullableDefaultValueDelegate(key, defaultValue, MMKV::decodeString, MMKV::encode)
fun MMKV.stringSet( key: String? = null, defaultValue: Set? = null ): ReadWriteProperty> = nullableDefaultValueDelegate(key, defaultValue, MMKV::decodeStringSet, MMKV::encode)
inline fun MMKV.parcelable( key: String? = null, defaultValue: T? = null ): ReadWriteProperty = object : ReadWriteProperty { 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", "")
框架中在展示GitHub的仓库的时候用到了ViewPager2,比起ViewPager,有以下几个好处:
android:orientation="vertical"
android:layoutDirection="rtl"
框架部分代码如下:
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"
我这边是对数据源、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>(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>(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:谭嘉俊