3.4 MVVM
3.4.1 viewmodel
3.4.1.1 RxViewModel
abstract class RxViewModel(private val schedulerProvider: SchedulerProvider) : ViewModel() {
var jobs = mutableListOf<Job>()
fun launch(code: suspend CoroutineScope.() -> Unit) {
jobs.add(coroutineLaunch(schedulerProvider.ui()) { code.invoke(this) })
}
fun launchIo(code: suspend CoroutineScope.() -> Unit) {
jobs.add(coroutineLaunch(schedulerProvider.io()) { code.invoke(this) })
}
override fun onCleared() {
super.onCleared()
jobs.forEach { it.cancel() }
}
}
这里有一些协程代码,还有点不太懂。
3.4.1.2 BaseViewModel
open class BaseViewModel (
schedulerProvider: SchedulerProvider
) : RxViewModel(schedulerProvider) {
val progress = ObservableField<Boolean>(false)
val isRefreshing = ObservableField<Boolean>(false)
val isError = ObservableField<Boolean>(false)
val errMsg = ObservableField<String>("")
}
添加一些状态代码如加载,进度条,错误以及错误信息。都通过ObservableField来定义,具体使用可以通过xml查看,以后还要再细品。
3.4.2 adapter
3.4.2.1 BaseBindableAdapter
interface BaseBindableAdapter<in T> {
fun setHeader(items: T) {}
fun setData(items: List<T>) {}
fun setFooter(items: T) {}
fun bind(data: T) {}
}
3.4.2.2 GenericAdapter
abstract class GenericAdapter<DATA> :
RecyclerView.Adapter<RecyclerView.ViewHolder>,
BaseBindableAdapter<DATA> {
var listItems: List<DATA>
constructor(listItems: List<DATA>) {
this.listItems = listItems
notifyDataSetChanged()
}
constructor() {
listItems = emptyList()
notifyDataSetChanged()
}
override fun setData(items: List<DATA>) {
this.listItems = items
notifyDataSetChanged()
}
// TODO: To add header?
override fun setHeader(items: DATA) {
(this.listItems as MutableList<DATA>).add(items)
notifyItemInserted(0)
}
// TODO: To add footer?
override fun setFooter(items: DATA) {
(this.listItems as MutableList<DATA>).add(items)
notifyItemInserted(this.listItems.size - 1)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return getViewHolder(
DataBindingUtil.inflate(LayoutInflater.from(parent.context)
, viewType
, parent
, false)!!)
}
@SuppressWarnings("Unchecked cast")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as? BaseBindableAdapter<DATA>)?.bind(listItems[position])
}
override fun getItemCount(): Int {
return listItems.size
}
override fun getItemViewType(position: Int): Int {
return getLayoutId(position, listItems[position])
}
protected abstract fun getLayoutId(position: Int, obj: DATA): Int
// TODO: Use generic ViewDataBinding
abstract fun getViewHolder(viewBinding: ViewDataBinding): RecyclerView.ViewHolder
}
3.4.3 数据绑定相关
CardViewBinding、ListBiding、ProgressBinding以及ViewBiding
同时添加了几个Ext类
object ListBinding {
@SuppressLint(value = ["PrivateResource", "UNCHECKED_CAST"])
@BindingAdapter(value = ["list:isGrid",
"list:spanCount",
"list:orientation",
"list:isReversed"], requireAll = false)
@JvmStatic
// TODO: Receive generic ViewDataBinding as args
fun RecyclerView.initAdapter(isGrid: Boolean = false,
spanCount: Int = 0,
orientation: Int = 0,
isReversed: Boolean = false) {
try {
if (isGrid) setupGridLayoutManager(spanCount, orientation, isReversed)
else setupLinearLayoutManager(orientation, isReversed)
} catch (e: Exception) {
e.printStackTrace()
}
}
@SuppressLint(value = ["PrivateResource", "UNCHECKED_CAST"])
@BindingAdapter(value = ["list:layoutId", "list:viewType"], requireAll = false)
@JvmStatic
fun <DATA> RecyclerView.initViewHolder(layoutId: Int,
viewType: Int?) {
try {
adapter = object : GenericAdapter<DATA>() {
override fun getLayoutId(position: Int, obj: DATA): Int {
return layoutId
}
// TODO: Refactor to generic instead of using when condition
override fun getViewHolder(viewBinding: ViewDataBinding): RecyclerView.ViewHolder {
return PostViewHolder(viewBinding as PostItemBinding)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
@SuppressLint(value = ["PrivateResource", "UNCHECKED_CAST"])
@BindingAdapter(value = ["list:items"], requireAll = false)
@JvmStatic
fun <DATA> RecyclerView.initData(items: List<DATA>?) {
try {
if (adapter is GenericAdapter<*>) {
(adapter as GenericAdapter<DATA>).setData(items ?: emptyList())
}
} catch (e: Exception) {
e.printStackTrace()
}
}
@SuppressLint(value = ["PrivateResource", "UNCHECKED_CAST"])
@BindingAdapter(value = ["list:items"], requireAll = false)
@JvmStatic
fun <DATA> RecyclerView.initData(items: Set<DATA>?) {
try {
if (adapter is GenericAdapter<*>) {
(adapter as GenericAdapter<DATA>).setData(items?.toList() ?: emptyList())
}
} catch (e: Exception) {
e.printStackTrace()
}
}
将数据显示和Databinding代码混合在一起,可以考虑分开。因为这样我就必须要开始界面PostViewHolder、以及PostItemBinding的编写(暂时放在后面)。
3.4.4 liveData相关
LiveEvent以及SingleLiveEvent
其他没有用到的暂时不理会,需要时再迁移过来。
3.5 界面部分
3.5.1 基础类
BaseActivity
open class BaseActivity : AppCompatActivity(), ToolbarListener {
override fun onSupportNavigateUp(): Boolean {
finish()
return true
}
override fun onBackPressed() {
super.onBackPressed()
overridePendingTransition(R.anim.slide_up, R.anim.slide_down)
}
override fun setupToolbar(toolbar: Toolbar) {
setSupportActionBar(toolbar)
}
override fun updateTitleToolbar(newTitle: String) {
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
title = newTitle
subtitle = ""
}
}
}
BaseActivity只是增加了一些动画和Toolbar标题,不过这个在我的这个版本里面没有调通。
BaseUserActionListener
interface BaseUserActionListener {
fun onRefresh()
}
用于加载页面的接口。
3.5.2 MainActivity
MainActivity使用的是navigation组件中的Fragment跳转管理(不知对不对,我暂时也只是调通了,没有仔细研究)。
class MainActivity : BaseActivity() {
private val viewBinding: ActivityMainBinding by lazy {
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
}
private lateinit var mNavHost: NavHostFragment
private lateinit var mNavController: NavController
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//初始化数据绑定
viewBinding.executePendingBindings()
setupNavController()
setupAppBar()
if (::mNavController.isInitialized && ::appBarConfiguration.isInitialized) {
setupActionBar(mNavController, appBarConfiguration)
}
updateTitleToolbar("kivy")
}
override fun onBackPressed() {
if (::mNavHost.isInitialized) {
val fragmentsSize = mNavHost.childFragmentManager.fragments.size
if (fragmentsSize >= 1) {
super.onBackPressed()
} else {
findNavController(R.id.navHostFragment).navigateUp(appBarConfiguration)
}
}
}
override fun onSupportNavigateUp(): Boolean {
return if (::mNavHost.isInitialized) {
findNavController(R.id.navHostFragment).navigateUp(appBarConfiguration)
} else {
false
}
}
private fun setupNavController() {
mNavHost = supportFragmentManager
.findFragmentById(R.id.navHostFragment) as NavHostFragment? ?: return
mNavController = mNavHost.navController
}
private fun setupAppBar() {
appBarConfiguration = AppBarConfiguration(
setOf(R.id.postFragment),
null
)
}
private fun setupActionBar(
navController: NavController,
appBarConfiguration: AppBarConfiguration
) {
setupToolbar(viewBinding.toolbar.toolbar)
setupActionBarWithNavController(navController, appBarConfiguration)
}
}
同时要将布局文件以及navigation的跳转xml迁移过来。样式,颜色,名称都不是重点可以直接复制过来。
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" > <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <include android:id="@+id/toolbar" layout="@layout/layout_toolbar"/> <fragment android:id="@+id/navHostFragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/toolbar" app:navGraph="@navigation/home_nav_graph"/> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
fragment指到navigation中的NavHostFragment,具体的操作在home_nav_graph中。
由于实际操作和我分析代码是不一样的,所以这里要将一些Fragment,layout文件先添加上来。
<?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/homeNavGraph" app:startDestination="@id/postFragment" > <fragment android:id="@+id/postFragment" android:name="xyz.wayhua.kivy101.ui.main.fragment.PostFragment" android:label="PostFragment" tools:layout="@layout/post_fragment"> <action android:id="@+id/toPostDetailAction" app:destination="@id/postDetailFragment"> <argument android:name="postItem" app:argType="xyz.wayhua.kivy.ui.main.fragment.PostItem"/> </action> </fragment> <fragment android:id="@+id/postDetailFragment" android:name="xyz.wayhua.kivy101.ui.main.fragment.PostDetailFragment" android:label="PostDetailFragment" tools:layout="@layout/postdetail_fragment"> <argument android:name="postItem" app:argType="xyz.wayhua.kivy101.ui.main.fragment.PostItem" app:nullable="true" /> </fragment> </navigation>
post_fragment,postdetail_fragment布局文件,以及PostFragment,PostDetailFragment两个Fragment以及前面说过的PostItem类都要添加上。
在此过程中还有一些其他的布局文件也一道迁移过来。
3.5.3 PostFragment 重头大戏
围绕PostFragment,其实还有三个关键类Adapter,具体的Item使用的ViewHolder,数据相关的ViewModel。我们可以沿着这个思路一步一步分析下去。
3.5.2.1 adapter
adapter在ListBinding中直接生成过,直接继承并且是object类型。
@SuppressLint(value = ["PrivateResource", "UNCHECKED_CAST"])
@BindingAdapter(value = ["list:layoutId", "list:viewType"], requireAll = false)
@JvmStatic
fun <DATA> RecyclerView.initViewHolder(
layoutId: Int,
viewType: Int?
) = try {
adapter = object : GenericAdapter<DATA>() {
override fun getLayoutId(position: Int, obj: DATA): Int {
return layoutId
}
// TODO: Refactor to generic instead of using when condition
override fun getViewHolder(viewBinding: ViewDataBinding): RecyclerView.ViewHolder {
return PostViewHolder(viewBinding as PostItemBinding)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
3.5.2.2 PostViewHolder
class PostViewHolder(val binding: PostItemBinding) :
RecyclerView.ViewHolder(binding.root),
BaseBindableAdapter<PostItem> {
override fun bind(data: PostItem) {
binding.apply {
item = data
root.setOnClickListener {
// val toPostDetail = PostFragmentDirections.toPostDetailAction(
// data
// )
//
// it.findNavController().navigate(toPostDetail)
}
executePendingBindings()
}
}
}
由于使用了数据绑定,就比较简单了,这里添加的单击事件不在此次考虑中,所以注释了。
3.5.2.3 ViewModel
class PostViewModel(
private val appRepository: AppRepository,
schedulerProvider: SchedulerProvider
) :BaseViewModel(schedulerProvider){
val keywords = Channel<String>(Channel.UNLIMITED)
var postListSet = MutableSetObservableField<PostItem>()
/*
* We use LiveEvent to publish "states"
* No need to publish and retain any view state
*/
private val _states = LiveEvent<State>()
val states: LiveData<State>
get() = _states.toSingleEvent()
fun getPosts() {
_states.value = LoadingState
launch {
try {
val posts = appRepository.getPostsAsync().await()
_states.value = PostListState.from(posts!!)
} catch (error: Throwable) {
_states.value = ErrorState(error)
}
}
}
fun searchPosts(query: String) {
if (query.isNotBlank()) {
_states.value = LoadingState
launch {
try {
val posts = appRepository.searchPostsAsync(query).await()
_states.value = PostListState.from(posts!!)
} catch (error: Throwable) {
_states.value = ErrorState(error)
}
}
}
}
}
viewModel除了引用repository中的方法外增加了一些状态信息,如LoadingState,ErrorState,以及正确的PostListState。这里有几个问题,由于使用的是sealed类,真的使用PostListState好吗?如果有多个Model呢?是不是要写多个?
State类,以前没有迁移过来。
/**
* Abstract State
*/
sealed class State
/**
* Generic Loading State
*/
object LoadingState : State()
/**
* Generic Error state
* @param error - caught error
*/
data class ErrorState(val error: Throwable) : State()
data class PostListState(
val list: List<Post>
) : State() {
companion object {
fun from(list: List<Post>): PostListState {
return with(list) {
when {
// TODO: @mochadwi Move this into strings instead
isEmpty() -> error("There's an empty post instead, please check your keyword")
else -> PostListState(this)
}
}
}
}
}
3.5.2.4 PostFragment
创建布局
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewBinding = PostFragmentBinding.inflate(inflater, container, false)
.apply {
listener = [email protected]
vm = viewModel
}
return viewBinding.root
}
拉取数据
private fun pullToRefresh() {
viewModel.apply {
isRefreshing.set(true)
if (::onLoadMore.isInitialized) onLoadMore.resetState()
getPosts()
}
}
根据状态处理数据
private fun setupObserver() = with(viewModel) {
// Observe ComposeState
states.observe(viewLifecycleOwner, Observer { state ->
state?.let {
when (state) {
is LoadingState -> showIsLoading()
is PostListState -> {
showCategoryItemList(
posts = state.list.map { PostItem.from(it) })
}
is ErrorState -> showError(state.error)
}
}
})
coroutineLaunch(Main) {
keywords.consumeEach { searchPosts(it) }
}
}
当state为PostListState时,有一个转换过程,将域数据Post转为了PostItem。
其他代码基本上是对以上代码的补充。排除编译错误后运行。
运行代码,结果报错:
Caused by: org.koin.core.error.NoBeanDefFoundException: No definition found for class:'xyz.wayhua.kivy101.ui.main.fragment.PostViewModel'. Check your definitions! at org.koin.core.scope.Scope.throwDefinitionNotFound(Scope.kt:247)
其原因是引用viewmodel是通过by viewModel<PostViewModel>()来实现的,要通过koin注入进来
private val viewModel by viewModel<PostViewModel>()
要将viewmodel配置到module中。
3.5.4 di 补充
val viewModelModule = module {
viewModel { PostViewModel(get(), get()) }
}
val allModules = listOf(rxModule, roomModule, viewModelModule,remoteDatasourceModule, repoModule)
构造函数中的get(),get(),其实是告诉我们这里有两个参数,都必须在module中配置。
4 总结
总体来说,这一次将代码进行了一次清理,同时结构更加清晰。仍然有很多问题,前面就说过如ListBinding直接指定Adapter,虽然少了一个类,但是确实也不太方便,现在只有一个页面,如果有非常多的页面是否要同时修改那个地方,如果有多种viewholder要显示呢?
还有就是结合以前编写程序的习惯,我更倾向于职责分离。这个会有深挖该源码以后,进一步深入。还有一个问题是中国人的习惯,上拉刷新,下拉加载更多,怎么处理。这里的页面是一次性加载100条,分页怎么办?
还有数据只是个补充,主要用途可能就是fts,没有起到缓存数据的作用。
5 重构
5.1 seal State类问题
在Status.kt文件中引用了Post类,如下
data class PostListState(
val list: List<Post>
) : State() {
companion object {
fun from(list: List<Post>): PostListState {
return with(list) {
when {
// TODO: @mochadwi Move this into strings instead
isEmpty() -> error("There's an empty post instead, please check your keyword")
else -> PostListState(this)
}
}
}
}
}
这是个seal类,不能扩展,但是每次都这样编写也是麻烦,现在是PostListState,如果还有其他的ListState呢,仍然是要这样编写,这就是一个大麻烦。
因此我编写一个SuccesState类,通过泛型来直接替换掉Post,这开始只是一个设想。
data class SuccessState<T>(val data: List<T>) : State() {
companion object {
fun <T> from(data: List<T>): SuccessState<T> {
return with(data) {
when {
isEmpty() -> error("不能为空")
else -> SuccessState(this)
}
}
}
}
}
如上编写,结果编译通过了。于是我就有一个大胆的想法,替换掉PostListState。
由于前面代码本来就是可以运行的。重构最好做到一次重构一个地方,现在是替换掉PostListState,那最好的办法就是直接注释掉PostListState类,然后编译,将所有的报错一一改掉,再看运行效果,如果没问题,那就表示重构成功了。
PostFragment中要替换后的代码
private fun setupObserver() = with(viewModel) {
// Observe ComposeState
states.observe(viewLifecycleOwner, Observer { state ->
state?.let {
when (state) {
is LoadingState -> showIsLoading()
is SuccessState<*> -> {
showCategoryItemList(
posts = state.data.map { PostItem.from(it as Post ) })
}
is ErrorState -> showError(state.error)
}
}
})
coroutineLaunch(Main) {
keywords.consumeEach { searchPosts(it) }
}
}
SuccessState<*> 表示,里面代码要强制转换。
PostViewModel中要同样替换为
fun getPosts() {
_states.value = LoadingState
launch {
try {
val posts = appRepository.getPostsAsync().await()
_states.value = SuccessState.from(posts!!)
} catch (error: Throwable) {
_states.value = ErrorState(error)
}
}
}
fun searchPosts(query: String) {
if (query.isNotBlank()) {
_states.value = LoadingState
launch {
try {
val posts = appRepository.searchPostsAsync(query).await()
_states.value = SuccessState.from(posts!!)
} catch (error: Throwable) {
_states.value = ErrorState(error)
}
}
}
}
再次运行,发现结果和原来一模一样。