LoginActivity解析
题记
最近在研究MVVM,起因是以前的一个类应用商城的项目,我用MVP写的框架,当我准备引入Jetpack组件的时候,结果发现JetPack跟MVP想结合的话有点不伦不类。并这些组件之间的联系很深,甚至可以说是耦合了,我不准备强行融合了,索性就拉上MVVM重构一把吧,当然个人精力有限,组内的健神给了我很大帮助。我也索性把自己作为MVVM小白研究架构的经验分享一波。
凡事都讲究方法,我认为最快的途径就是站在巨人的肩膀上。在结束了Jetpack组件基础知识的浏览后,我把眼光瞄向了Android Studio自带的模板类。
一. 整体结构
当我们新建一个Activity的时候,发现在仅有一个页面的情况下,产生了居多的文件,一个复杂的工程结构。
如上图所示,主要分为ui和data,这个整体分包我还是十分认可的。但是个人认为根据分包的清晰度而言,ui下的login分包本身没有问题,但是viewmodel和factory文件,以我个人理解而言,我会选择把它单独放在一个包内。暂且就按google工程师的思维解析吧,希望大佬的光辉能够庇护我有个更好的代码思路。
二. ui/login
按照个人习惯,还是先从页面开始分析功能,比较容易入手。
1. LoginActivity.kt
故名思议,这个界面就是最终的展示界面,从这个文件作为突破口。从代码
加注释开始解析。
private lateinit var loginViewModel: LoginViewModel
这个首先在view层引入了一个ViewModel
,主要是作为数据更新和View刷新关联起来。和注意其中的lateinit
关键字,这个是作为懒加载,目的在与代码编译阶段避开编译检查(不用添加!! / ?
)。
相关文档:1.Kotlin类和加载 2.ViewModel 概览loginViewModel = ViewModelProviders.of(this, LoginViewModelFactory()) .get(LoginViewModel::class.java)
这个就是简单的ViewModel的构造方法,注意第二个参数的LoginViewModelFactory
这个是一个重写的构造工厂,主要在这里传入默认的构造工程,只能创建空构造函数的ViewModel,重写之后,可以使ViewModel具有有参数的构造函数,并且在工厂中添加参数。loginViewModel.loginFormState.observe(this@LoginActivity, Observer {...}
包括下面的那个observer,等同于一个观察 viewmodel的变量LoginFormState/loginResult
对象的回调,当被观察对象发生变化的时候,触发这里的操作。
val loginState = it ?: return@Observer
这是一个简单的语法糖,判空并返回。password.apply {...}
这里有一个很有意思的语法糖apply
→fun T.apply(block: T.() -> Unit): T { block(); return this }
。this指代当前对象或者省略,返回this。一般用于多个扩展函数链式调用,这里操作对象属性,并最终返回这个对象。
2. LoginViewModelFactory
看看自动生成的官方解释: * ViewModel provider factory to instantiate LoginViewModel.Required given LoginViewModel has a non-empty constructor. 理解为LoginViewModel具有非空构造函数需要定制工厂。
因为继承了ViewModelProvider.Factory
,所以我们需要重写fun <T : ViewModel> create(modelClass: Class<T>): T {}
。在return
关键字之后new出这个带参数的实例。
3. LoginViewModel
private val _loginForm = MutableLiveData<LoginFormState>() val loginFormState: LiveData<LoginFormState> = _loginForm
注意前面4行定义的课观察数据容器。为什么一个LoginFormState
会放在几乎相同的MutableLiveData/LiveData
中呢?这个操作看似很骚,实则很妙。因为LiveData
没有公开的方法可以更新存储的数据。MutableLiveData
暴露了setValue(T)和postValue(T)方法,如果想要编辑LiveData对象中存储的数据就必须使用MutableLiveData
。而加上private
也符合数据封装的安全原则,使ViewModel只对观察者暴露不可修改的LiveData对象,而下面的loginDataChanged(...)
方法正是暴露到外面的可操作存储数据的方法。
PS:其实我个人感觉只用MutableLiveData就好,多一个LiveData有点画蛇添足,可能我的境界尚浅吧。
4. LoggedInUserView、LoginResult、LoginFormState
这三个类为什么归类为一起呢?因为都是date
关键字修饰的数据类,我也比较喜欢成为数据bean。简单来说就是存储一些组合数据,作为一个组合起来的信息类进行数据传递。data类可以自动生成一些方法。
相关资料:Kotlin 数据类与密封类
6. LoggedInUserView
User details post authentication that is exposed to the UI → 用户界面展示信息
7. LoginFormState
Data validation state of the login form. → 登陆信息请求体
8. LoginResult
Authentication result : success (user details) or error message. → 登陆返回结果体
三. data
顾名思义,这个包下的文件均是存储一些过程/结果数据。
1. LoggedInUser
从LoginRepository
检索到的,捕获到的已登录用户的用户信息的数据类。简单说就是当前存储当前登陆用户信息。
下面就说
2. LoginDataSource
处理带有登录凭据的身份验证,并检索用户信息。简单说就是检索用户登陆信息,做出登陆动作,并返回结果。
3. LoginRepository
Class that requests authentication and user information from the remote data source andmaintains an in-memory cache of login status and user credentials information.
该类从远程数据源(LoginDataSource.login
)请求身份验证和用户信息,并维护登录状态和用户凭据信息的内存缓存。
先说说缓存了什么。
9. 首先缓存的是当前登陆用户信息LoggedInUser
,之所以用private
修饰,为的是防止无故的修改,只有再做出登陆动作&登陆成功才会初始化登陆信息。
kotlin var user: LoggedInUser? = null private set //注意私有
10. 接下来缓存的是登陆状态boolean。重写了get方法,目的在与,如果没有当前用户登陆信息,则证明未登录。正好呼应了上面的private
关键字。
kotlin val isLoggedIn: Boolean get() = user != null
11. 注意init{...}
.关于init代码块的运行顺序,无非是是类的加载首先执行init代码块,然后再执行构造方法。所以我可以在init代码块中做各种初始化操作,比如声明属性。这里的注释,其实告诉我们,如果我们的用户信息在本地缓存(数据库…),依据安全性,在初始化这里做一些加密操作。
```kotlin
init {
// If user credentials will be cached in local storage, it is recommended it be encrypted
// @see https://developer.android.com/training/articles/keystore
user = null
}
```
- 这个类里面同样也有
login
方法,调用的就是入参传进来的LoginDataSource
的同名方法,根据返回值来判断,是否登陆成功(给LoggedInUser
赋值来判断isLoggedIn
)。
fun login(username: String, password: String): Result<LoggedInUser> {
// handle login
val result = dataSource.login(username, password)
if (result is Result.Success) {
setLoggedInUser(result.data)
}
return result
}
四. Result
这个是我看着最难受,但是感觉很抓人的一个类。目前我用的很少或者说基本不用,但是我是觉得珍惜奇妙无比,以后一定会在我的架构中加入这个多样化的一个糖果。sealed
关键字修饰的class真的是骚气无比。
基本用法如下 ↓ ↓ ↓
class 基类名称
sealed class 基类名称 {
class 子类1
class 子类2
fun 123()
}
怎么说呢?就是说sealed class
命名的最外面的类是一个父类(也称为密封类),其类中跟属性一样命名的类是其子类。注意这个子类只能在同一个.kt文件内。并且无法通过本体的实例化对象,直接调用其中的方法,比如在这个工程中,result.tostring
是直接报错的,但是 Result.Success()
是可以的。
其实我现在无法用一些浅显的语言来说明这个玩意的好处,我就是感觉他很精妙,是一个枚举的加强版甚至说是完美版本。
我们现在用它可以做很多事情。
- 我们可以定义继承同一个类的一组类。也就是说同一个类可以有我们指定数量的实现。
- 这些子类的类型可以确定,或者说约束起来了。
这对函数式编程有了很大的推动。看看最精妙的一段代码:
override fun toString(): String {
return when (this) {
is Success<*> -> "Success[data=$data]"
is Error -> "Error[exception=$exception]"
}
}
同一个类的同一个方法执行了不同的操作。比枚举骚气的是,toString()
的调用是一个实例化后的类,或者说是一个object,类经过了实例化就可以保存当前状态。这不就我一个类对应不同状态吗?不管别人怎么认为,我就是感觉特别酷!!!
总结
整体架构分析下来,感觉思路清晰异常,但是还是感觉这个结构/分包过于臃肿甚至无聊。其中绝妙的语法糖,可以省去很多代码量。但是,福兮祸所依,过多语法糖的代码,对后来的观看者包括自己可能说是一种灾难,可阅读性在代码的高潮期过后,可能会降到冰点。
最后补一句,Kotlin语法的学习任重道远,架构的分析与攀登长路漫漫。愿我们在这条路上一飞冲天,与君共勉。
2020年6月18日23:36:13