如何 JetPack+单Activity实现一个app?

作者:512DIDIDI

链接:https://www.jianshu.com/p/1560de5422ca

Google推JetPack已经有一段时间了,伴随之而来的是MVVM架构,使用 ViewModel   LiveData 等工具来实现 data-binding

JetPack中还附带了一个 Navigation ,顾名思义,即导航功能,主要目的是用来实现单Activity架构,之前写过一篇文章,是利用 fragmentation 来实现单Activity架构,抱着学习的态度,这次的项目采用了 Navigation 来实现单 Activity 架构。

先附带项目的MVVM架构图:

  • 绿色代表View层

  • 蓝色代表ViewModel层

  • 红色代表Model层

各层之间均是单向依赖,即V层向VM层发起请求,VM层向M层获取数据,再通过 LiveData 作为桥梁,V层监听 LiveData 数据,数据变化时更新UI。

举个代码例子吧,登录流程:

首先是V层代码:

class LoginFragment : BaseFragment(), View.OnClickListener {
privateval viewModel by lazy {
ViewModelProviders.of(this).get(LoginViewModel::class.java)
}

... ...

overridefun bindView(savedInstanceState: Bundle?, rootView: View) {
observe()
fragmentLoginSignInLayoutSignIn.setOnClickListener(this)
}

overridefun onClick(v: View?) {
when (v?.id) {
R.id.fragmentLoginSignInLayoutSignIn -> {
//调用登录
viewModel.login(
fragmentLoginSignInLayoutAccount.text.toString(),
fragmentLoginSignInLayoutPassword.text.toString()
)
}
... ...
}
}

/**
* 监听数据变化
*/

privatefun 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 的实例,点击登录按钮时,调用其l ogin(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>()

privateval 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 引用。

abstractclass 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(
privateval net: PackRatNetUtil
) {
companionobject {
@Volatile
privatevar instance: LoginRepository? = null

fun getInstance(net: PackRatNetUtil) =
instance ?: synchronized(this) {
instance ?: LoginRepository(net).apply {
instance = this
}
}
}

suspendfun login(username: String, password: String) =
net.fetchLoginResult(username, password)

suspendfun register(username: String, password: String, repassword: String) =
net.fetchRegisterResult(username, password, repassword)

suspendfun logOut() =
net.fetchQuitResult()
}

这里做的事情也很简单,从网络层获取数据,当然,如果需要存放本地数据库,可以如下实现:

class CollectRepository private constructor(
privatevar collectDao: CollectDao,
privatevar net: PackRatNetUtil
) {
companionobject {
@Volatile
privatevar instance: CollectRepository? = null

fun getInstance(collectDao: CollectDao, net: PackRatNetUtil) =
instance ?: synchronized(this) {
instance ?: CollectRepository(collectDao, net).apply {
instance = this
}
}
}

/**
* 获取收藏列表数据
*/

suspendfun getCollects() = try {
net.fetchCollectList()
} catch (t: Throwable) {
t.printStackTrace()
collectDao.getCollectList()
}


/**
* 设置收藏列表存储入数据库
*/

suspendfun setCollects(collects: List<Collect>) {
collects.forEach {
collectDao.insert(it)
log(content = it.content)
}
}
}

传入本地数据库及网络层的实例,然后依照不同的情况分别获取数据。

class PackRatNetUtil private constructor() {
companionobject {
@Volatile
privatevar instance: PackRatNetUtil? = null

fun getInstance() = instance ?: synchronized(this) {
instance ?: PackRatNetUtil().apply {
instance = this
}
}
}

privateval collectService = ServiceCreator.create(CollectService::class.java)

privateval loginService = ServiceCreator.create(LoginService::class.java)

/**
* 从服务器获取收藏列表
*/

suspendfun fetchCollectList() = collectService.getCollectAsync().await()

/**
* 获取登录结果
*/

suspendfun 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作为回调
*/

privatesuspendfun <T> Call<T>.await(): T = suspendCoroutine { continuation ->
enqueue(object : Callback<T> {
overridefun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}

overridefun 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 架构的思路。

先贴项目代码:https://github.com/512DIDIDI/PackRat

亲,点这涨工资  

热文推荐:

1、 大厂又有新的开源项目了,赶紧来领取...

2、 面试官问我:一个 TCP 连接可以发多少个 HTTP 请求?我竟然回答不上来...

3、 程序员疑似出bug被吊打!菲律宾的高薪工作机会了解一下?

4、 “一键脱衣”的DeepNude下架后,我在GitHub上找到它涉及的技术

5、 原生Android开发的路该怎么走

6、 太厉害了,终于有人能把TCP/IP 协议讲的明明白白了

7、 腾讯开源超实用的UI轮子库,我是轮子搬运工

8、 腾讯新开源一吊炸天神器—零反射全动态Android插件框架正式开源

喜欢 就关注吧,欢迎投稿!

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章