Google推JetPack已经有一段时间了,伴随之而来的是MVVM架构,使用ViewModel
LiveData
等工具来实现MVVM。
JetPack中还附带了一个Navigation
,顾名思义,即导航功能,主要目的是用来实现单Activity架构,之前写过一篇文章,是利用fragmentation
来实现单Activity架构,抱着学习的态度,这次的项目采用了Navigation
来实现单Activity架构。
JetPack实现MVVM架构思路:
先附带项目的MVVM架构图:
绿色代表View层
蓝色代表ViewModel层
红色代表Model层
各层之间均是单向依赖,即V层向VM层发起请求,VM层向M层获取数据,再通过LiveData
作为桥梁,V层监听LiveData
数据,数据变化时更新UI。
举个代码例子吧,登录流程:
- 首先是V层代码:
class LoginFragment : BaseFragment(), View.OnClickListener {
private val viewModel by lazy {
ViewModelProviders.of(this).get(LoginViewModel::class.java)
}
... ...
override fun bindView(savedInstanceState: Bundle?, rootView: View) {
observe()
fragmentLoginSignInLayoutSignIn.setOnClickListener(this)
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.fragmentLoginSignInLayoutSignIn -> {
//调用登录
viewModel.login(
fragmentLoginSignInLayoutAccount.text.toString(),
fragmentLoginSignInLayoutPassword.text.toString()
)
}
... ...
}
}
/**
* 监听数据变化
*/
private fun observe() {
//登录监听
viewModel.loginLiveData.observe(this, Observer {
if (it.errorCode != 0) {
toast(it.errorMsg)
} else {
Navigation.findNavController(fragmentLoginSignInLayoutSignIn)
.navigate(R.id.action_loginFragment_to_mainFragment)
}
})
//注册监听
viewModel.registerLiveData.observe(this, Observer {
if (it.errorCode != 0) {
toast(it.errorMsg)
} else {
toast("注册成功,返回登录页登录")
getSignInAnimation(fragmentLoginSignUpLayout)
}
})
//loading窗口监听
viewModel.isLoading.observe(this, Observer {
if (it) {
showLoading()
} else {
dismissAllLoading()
}
})
}
}
获取LoginViewModel
的实例,点击登录按钮时,调用其login(userName:String,password:String)
方法,在observe()
方法中获取其LiveData
数据的observe
方法,监听其数据变化,在Observer
匿名类里进行UI的更新。
- VM层代码:
class LoginViewModel(application: Application) : BaseViewModel(application) {
/**
* 登录数据
*/
var loginLiveData = MutableLiveData<LoginResponse>()
/**
* 注册数据
*/
var registerLiveData = MutableLiveData<LoginResponse>()
/**
* 登出数据
*/
var logOutLiveData = MutableLiveData<LoginResponse>()
private val repository = LoginRepository.getInstance(PackRatNetUtil.getInstance())
fun login(username: String, password: String) {
launch {
loginLiveData.postValue(repository.login(username, password))
}
}
fun register(username: String, password: String, repassword: String) {
launch {
registerLiveData.postValue(repository.register(username, password, repassword))
}
}
fun logOut() {
launch {
logOutLiveData.postValue(repository.logOut())
}
}
}
很简单,做的就是实例化LiveData
和repository
数据依赖层,并调用repository
获取数据,最后往里postValue
赋值。我这里包装了一层BaseViewModel
,它继承了AndroidViewModel,它与普通的ViewModel不同之处在于可以需要传入application
参数,也就是可以获取一个全局context引用。
abstract class BaseViewModel(application: Application) : AndroidViewModel(application){
/**
* 加载变化
*/
var isLoading = MutableLiveData<Boolean>()
/**
* 统一协程处理
*/
fun launch(block:suspend() -> Unit) = viewModelScope.launch {
try {
isLoading.value = true
withContext(Dispatchers.IO){
block()
}
isLoading.value = false
}catch (t:Throwable){
t.printStackTrace()
getApplication<PackRatApp>().toast(t.message)
isLoading.value = false
}
}
}
抽离了一个协程方法,耗时操作统一到IO线程操作,loading在耗时方法完成时置为false,通知页面关闭弹窗。
- M层代码
class LoginRepository private constructor(
private val net: PackRatNetUtil
) {
companion object {
@Volatile
private var instance: LoginRepository? = null
fun getInstance(net: PackRatNetUtil) =
instance ?: synchronized(this) {
instance ?: LoginRepository(net).apply {
instance = this
}
}
}
suspend fun login(username: String, password: String) =
net.fetchLoginResult(username, password)
suspend fun register(username: String, password: String, repassword: String) =
net.fetchRegisterResult(username, password, repassword)
suspend fun logOut() =
net.fetchQuitResult()
}
这里做的事情也很简单,从网络层获取数据,当然,如果需要存放本地数据库,可以如下实现:
class CollectRepository private constructor(
private var collectDao: CollectDao,
private var net: PackRatNetUtil
) {
companion object {
@Volatile
private var instance: CollectRepository? = null
fun getInstance(collectDao: CollectDao, net: PackRatNetUtil) =
instance ?: synchronized(this) {
instance ?: CollectRepository(collectDao, net).apply {
instance = this
}
}
}
/**
* 获取收藏列表数据
*/
suspend fun getCollects() = try {
net.fetchCollectList()
} catch (t: Throwable) {
t.printStackTrace()
collectDao.getCollectList()
}
/**
* 设置收藏列表存储入数据库
*/
suspend fun setCollects(collects: List<Collect>) {
collects.forEach {
collectDao.insert(it)
log(content = it.content)
}
}
}
传入本地数据库及网络层的实例,然后依照不同的情况分别获取数据。
class PackRatNetUtil private constructor() {
companion object {
@Volatile
private var instance: PackRatNetUtil? = null
fun getInstance() = instance ?: synchronized(this) {
instance ?: PackRatNetUtil().apply {
instance = this
}
}
}
private val collectService = ServiceCreator.create(CollectService::class.java)
private val loginService = ServiceCreator.create(LoginService::class.java)
/**
* 从服务器获取收藏列表
*/
suspend fun fetchCollectList() = collectService.getCollectAsync().await()
/**
* 获取登录结果
*/
suspend fun fetchLoginResult(username: String, password: String) =
loginService.loginAsync(username, password).await()
/**
* 此方法用于retrofit使用 [Call] 的 [Callback] 回调与协程 [await] 的回调相连
* 不过 retrofit 后续提供了[CoroutineCallAdapterFactory],可返回[Deferred]作为回调
* @Deprecated 引入[com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter]包可以使用Deferred作为回调
*/
private suspend fun <T> Call<T>.await(): T = suspendCoroutine { continuation ->
enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
val body = response.body()
if (body != null) {
continuation.resume(body)
} else {
continuation.resumeWithException(NullPointerException("response body is null"))
}
}
})
}
}
这里提下,之前retrofit没有提供coroutines-adapter依赖包时,不能使用Deferred
作为回调,可重写其Call
的await方法,将协程的resume
方法与resumeWithException
方法与之对应,从而使retrofit能更好的与协程使用,不过retrofit后续提供了
implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$retrofitCoroutineVersion"
所以可在其apiService里使用Deferred
作为回调。
interface LoginService {
@FormUrlEncoded
@POST("user/login")
fun loginAsync(
@Field("username") username: String,
@Field("password") password: String
): Deferred<LoginResponse>
@FormUrlEncoded
@POST("user/register")
fun registerAsync(
@Field("username") username: String,
@Field("password") password: String,
@Field("repassword") repassword: String
): Deferred<LoginResponse>
@FormUrlEncoded
@GET("user/logout/json")
fun quitAsync(): Deferred<LoginResponse>
}
MVVM其核心思路就在于各层之间单向依赖单向交流,不能出现V层直接请求数据等操作。
Navigation实现单activity思路:
尝试了一段时间的Navigation,总体使用下来感觉还行,代码思路挺清晰的,通过xml来管理fragment的行为,activity中只需执行行为就ok了。当然,我这里只是简单的使用无传参的Navigation,其实官方的教程已经很详细的说明了每一个步骤:官方Navigation使用介绍
按照步骤下来应该能轻易的实现tab切换页面的效果,着重说下文档外的一些封装实现:
我的项目分为了两个navigation
分别是navigation_main
和 navigation_home
navigation_main
主要是负责 登录页 首页 独立页面的fragment处理 起着全局导航的作用navigation_home
主要负责Collect Todo Me首页三个模块的处理
对应的抽象了一层Fragment
用来处理不同的navigation
首页的导航:
abstract class BaseHomeNavFragment:BaseFragment() {
/**
* 全局的fragment导航,不是首页三个tab的导航
* 实际上用的是PackRat\app\src\main\res\navigation\navigation_main.xml
*/
protected val mainNavController by lazy {
Navigation.findNavController(activity!!, R.id.activityRootFragment)
}
/**
* 首页三个tab的导航
*/
protected val homeNavController by lazy {
Navigation.findNavController(activity!!, R.id.fragmentMainFragment)
}
override fun onBackPressed(): Boolean {
homeNavController.popBackStack()
return true
}
}
全局导航:
abstract class BaseMainNavFragment : BaseFragment() {
/**
* 全局的fragment导航
* 用的是PackRat\app\src\main\res\navigation\navigation_main.xml
*/
protected val mainNavController by lazy {
Navigation.findNavController(activity!!, R.id.activityRootFragment)
}
override fun onBackPressed(): Boolean {
//执行全局导航的fragment返回栈
mainNavController.popBackStack()
return true
}
}
实例化出来的两个 NavController
主要是方便子类调用,不需要反复实例化。后续在使用过程中仅需如下使用:
//登录监听
viewModel.loginLiveData.observe(this, Observer {
if (it.errorCode != 0) {
toast(it.errorMsg)
} else {
mainNavController.navigate(R.id.action_login_to_home)
}
})
... ...
private fun clickEvent(){
fragmentCollectFab.setOnClickListener {
mainNavController.navigate(R.id.action_home_to_addCollect)
}
}
... ...
private fun clickEvent(){
fragmentAddCollectBackBtn.setOnClickListener {
mainNavController.navigateUp()
}
}
复写的onBackPressed
用于处理实体返回键是否需要拦截,虽然在xml中的fragment
可以通过设置app:defaultNavHost="true"
来实现拦截返回键,但由于我的项目页面逻辑是先登录 登录成功后才跳转首页,如果直接用默认的拦截返回键,会出现首页按返回键退回到登陆页面,因此做了一层封装,主要思路参考的Android Jetpack之Fragment里监听返回键的最佳写法
在RootActivity
中复写OnBackPressed
方法
class RootActivity : AppCompatActivity() {
lateinit var currentFragment:BaseFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_root)
//如果已登录 跳转到首页
if (!getSharedPreferences(
Config.COOKIE_FILE,
Context.MODE_PRIVATE
).getStringSet(Config.COOKIE_KEY, null).isNullOrEmpty()
) {
Navigation.findNavController(this, R.id.activityRootFragment)
.navigate(R.id.action_login_to_home)
}
}
/**
* 实体返回键的处理
*/
override fun onBackPressed() {
when{
currentFragment.onBackPressed() -> {}
else -> super.onBackPressed()
}
}
}
在BaseFragment中:
abstract class BaseFragment : Fragment() {
/**
* 提供AppCompatActivity实例
*/
protected lateinit var mActivity: RootActivity
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is RootActivity) {
mActivity = context
}
}
... ...
/**
* 是否拦截返回键 默认不拦截
*/
open fun onBackPressed() = false
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mActivity.currentFragment = this
}
}
意思很简单,就是在需要拦截返回键的Fragment
中实现BaseFragment
的onBackPressed
方法,并返回true
,返回true
意味着在RootActivity
的OnBackPressed
中执行BaseFragment
的onBackPressed
方法。