Py学习  »  Git

巧用MVVM搭建GitHub客户端

郭霖 • 3 年前 • 299 次点击  

/   今日科技快讯   /

近日,据外媒报道,在美国各地爆发的抗议活动中,苹果零售店成为个别暴徒们袭击的目标,有些零售店的店面遭受了严重破坏,甚至里面的商品也遭到抢掠。为了保护员工安全,苹果于当地时间周日决定暂时关闭其在美国各地刚刚重新开放的多家店面。

/   作者简介   /

本篇文章来自谭嘉俊的投稿,分享了他如何使用MVVM搭建GitHub客户端的理解,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!

谭嘉俊的博客地址
https://juejin.im/user/593f7b33fe88c2006a37eb9b

/   开始   /

本篇文章讲解的内容是MVC、MVP、MVVM以及使用MVVM搭建GitHub客户端。在讲解之前,我想先聊一下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),意思是它没有任何的主动性,而且这样的设计也方便做单元测试,但是也会有如下问题:

  1. 尽管减少了View层的代码,但是随着业务的复杂度不断提高,Presenter层的代码也会变得越来越臃肿。
  2. View层和Presenter层是通过接口交互的,随着业务的复杂度不断提高,接口数量会大量增加。
  3. 如果View层更新的话,就像UI的输入和数据的变化,都需要主动去调用Presenter层的代码,缺乏自动性和监听性。
  4. 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。

Kotlin版本如图所示:


  • 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的扩展函数
  • DateUtils:存放日期相关的代码。
  • FragmentExt:存放Fragment的扩展函数。
  • GsonExt:存放Gson相关的扩展函数。
  • Language:存放GitHub仓库相关的名字和图片。
  • OnTabSelectedListenerBuilder:存放OnTabSelectedListener相关的代码,用作使用DSL
  • Preferences:存放MMKV相关的代码
  • 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的协程,和旧版本有如下区别:

  1. 可以直接作用于挂起函数(suspend fun)。
  2. 可以直接返回我们想要的数据对象,而不再返回**Deferred**对象。
  3. 不再需要调用协程中await函数,因为Retrofit已经帮我们调用了。

框架部分代码如下:

// RepositoryRemoteDataSource.kt
interface Service {

    @GET("search/repositories")
    suspend fun fetchRepositories(@Query("q") query: String,
                                  @Query("sort") sort: String 
"stars"): ListData

}

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的核心架构组件,它有如下优点:

  1. 可以降低布局和逻辑的耦合度,使代码逻辑更加清晰。
  2. 可以省去findViewById这样的代码,大量减少View层的代码。
  3. 数据能单向和双向绑定到layout文件。
  4. 能够自动进行空判断,可以避免空指针异常。

框架部分代码如下:


    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仅更新处于活跃生命周期状态的应用组件观察者。框架部分代码如下:

// 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

ViewModel是一个负责准备和管理Activity或者Fragment的类,它还可以处理Activity和Fragment与应用程序其余部分的通信(例如:调用业务逻辑类)。

ViewModel总是在一个Activity或者一个Fragment创建的,并且只要对应的Activity或者Fragment处于活动状态的话,它就会被保留(例如:如果它是个Activity,就会直到它finished)。

换句话说,这意味着一个ViewModel不会因为配置的更改(例如:旋转)而被销毁,所有的新实例将被重新连接到现有的ViewModel。

ViewModel的目的是获取和保存Activity或者Fragment所需的信息,Activity或者Fragment应该能够观察到ViewModel中的变化,通常通过LiveData或者Android Data Binding公开这些信息。框架部分代码如下:

// RepositoryViewModel.kt
/**
 * Created by TanJiaJun on 2020-02-07.
 */

class RepositoryViewModel @Inject constructor(
        private val repositoryGitHubRepository
) : BaseViewModel() 
{

    private val _isShowRepositoryView = MutableLiveData()
    val isShowRepositoryView: LiveData = _isShowRepositoryView

    private val _repositories = MutableLiveData>()
    val repositories: LiveData> = _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的话,也应该很快就能熟悉它。框架部分代码如下:

// 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.Builder()
                .connectTimeout(AndroidGenericFrameworkConfiguration.CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
                .readTimeout(AndroidGenericFrameworkConfiguration.READ_TIMEOUT, TimeUnit.MILLISECONDS)
                .addInterceptor(BasicAuthInterceptor(get()))
                .build()
    }

    single {
        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 {
        viewModel { MainViewModel(get()) }
    }
}

val userModule = module {
    scope {
        viewModel { LoginViewModel(get()) }
    }

    scope {
        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

MMKV是基于mmap内存映射的key-value组件,底层序列化/反序列化使用protobuf实现,性能高,稳定性强,而且Android这边还支持多进程。

我使用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.javadefaultValue)

            override fun setValue(thisRefAnypropertyKPropertyvalueT
{
                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"

框架部分代码如下:


    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类型,并且可以扩展到其他类型。

框架部分代码如下:

// 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>(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>(relaxed = true)
        viewModel.uiLiveEvent.showSnackbarEvent.observeForever(observer)
        viewModel.viewModelScope.coroutineContext[Job]?.children?.forEach { it.join() }
        verify { observer.onChanged(match { it == "0:UnknownError" }) }
    }
}

我的GitHub:TanJiaJunBeyond
https://github.com/TanJiaJunBeyond

Android通用框架:Android通用框架
https://github.com/TanJiaJunBeyond/AndroidGenericFramework

推荐阅读:
时隔两年,LitePal终于又更新了!
这本《第三行代码》,让大家久等了!
我新开发了一个特别好用的开源库

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/64242
 
299 次点击