3.4 MVVM
3.4.1 viewmodel
3.4.1.1 RxViewModel
abstractclassRxViewModel(privatevalschedulerProvider:SchedulerProvider) :ViewModel() {
varjobs=mutableListOf<Job>()
funlaunch(code:suspendCoroutineScope.()->Unit) {
jobs.add(coroutineLaunch(schedulerProvider.ui()) {code.invoke(this) })
}
funlaunchIo(code:suspendCoroutineScope.()->Unit) {
jobs.add(coroutineLaunch(schedulerProvider.io()) {code.invoke(this) })
}
overridefunonCleared() {
super.onCleared()
jobs.forEach{it.cancel() }
}
}
这里有一些协程代码,还有点不太懂。
3.4.1.2 BaseViewModel
openclassBaseViewModel(
schedulerProvider:SchedulerProvider
) :RxViewModel(schedulerProvider) {
valprogress=ObservableField<Boolean>(false)
valisRefreshing=ObservableField<Boolean>(false)
valisError=ObservableField<Boolean>(false)
valerrMsg=ObservableField<String>("")
}
添加一些状态代码如加载,进度条,错误以及错误信息。都通过ObservableField来定义,具体使用可以通过xml查看,以后还要再细品。
3.4.2 adapter
3.4.2.1 BaseBindableAdapter
interfaceBaseBindableAdapter<inT>{
funsetHeader(items:T) {}
funsetData(items:List<T>) {}
funsetFooter(items:T) {}
funbind(data:T) {}
}
3.4.2.2 GenericAdapter
abstractclassGenericAdapter<DATA>:
RecyclerView.Adapter<RecyclerView.ViewHolder>,
BaseBindableAdapter<DATA>{
varlistItems:List<DATA>
constructor(listItems:List<DATA>) {
this.listItems=listItems
notifyDataSetChanged()
}
constructor() {
listItems=emptyList()
notifyDataSetChanged()
}
overridefunsetData(items:List<DATA>) {
this.listItems=items
notifyDataSetChanged()
}
// TODO: To add header?
overridefunsetHeader(items:DATA) {
(this.listItemsasMutableList<DATA>).add(items)
notifyItemInserted(0)
}
// TODO: To add footer?
overridefunsetFooter(items:DATA) {
(this.listItemsasMutableList<DATA>).add(items)
notifyItemInserted(this.listItems.size-1)
}
overridefunonCreateViewHolder(parent:ViewGroup,viewType:Int):RecyclerView.ViewHolder{
returngetViewHolder(
DataBindingUtil.inflate(LayoutInflater.from(parent.context)
,viewType
,parent
,false)!!)
}
@SuppressWarnings("Unchecked cast")
overridefunonBindViewHolder(holder:RecyclerView.ViewHolder,position:Int) {
(holderas?BaseBindableAdapter<DATA>)?.bind(listItems[position])
}
overridefungetItemCount():Int{
returnlistItems.size
}
overridefungetItemViewType(position:Int):Int{
returngetLayoutId(position,listItems[position])
}
protectedabstractfungetLayoutId(position:Int,obj:DATA):Int
// TODO: Use generic ViewDataBinding
abstractfungetViewHolder(viewBinding:ViewDataBinding):RecyclerView.ViewHolder
}
3.4.3 数据绑定相关
CardViewBinding、ListBiding、ProgressBinding以及ViewBiding
同时添加了几个Ext类
objectListBinding{
@SuppressLint(value=["PrivateResource","UNCHECKED_CAST"])
@BindingAdapter(value=["list:isGrid",
"list:spanCount",
"list:orientation",
"list:isReversed"],requireAll=false)
@JvmStatic
// TODO: Receive generic ViewDataBinding as args
funRecyclerView.initAdapter(isGrid:Boolean=false,
spanCount:Int=0,
orientation:Int=0,
isReversed:Boolean=false) {
try{
if(isGrid)setupGridLayoutManager(spanCount,orientation,isReversed)
elsesetupLinearLayoutManager(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>() {
overridefungetLayoutId(position:Int,obj:DATA):Int{
returnlayoutId
}
// TODO: Refactor to generic instead of using when condition
overridefungetViewHolder(viewBinding:ViewDataBinding):RecyclerView.ViewHolder{
returnPostViewHolder(viewBindingasPostItemBinding)
}
}
}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(adapterisGenericAdapter<*>) {
(adapterasGenericAdapter<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(adapterisGenericAdapter<*>) {
(adapterasGenericAdapter<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
openclassBaseActivity:AppCompatActivity(),ToolbarListener{
overridefunonSupportNavigateUp():Boolean{
finish()
returntrue
}
overridefunonBackPressed() {
super.onBackPressed()
overridePendingTransition(R.anim.slide_up,R.anim.slide_down)
}
overridefunsetupToolbar(toolbar:Toolbar) {
setSupportActionBar(toolbar)
}
overridefunupdateTitleToolbar(newTitle:String) {
supportActionBar?.apply{
setDisplayHomeAsUpEnabled(true)
title=newTitle
subtitle=""
}
}
}
BaseActivity只是增加了一些动画和Toolbar标题,不过这个在我的这个版本里面没有调通。
BaseUserActionListener
interfaceBaseUserActionListener{
funonRefresh()
}
用于加载页面的接口。
3.5.2 MainActivity
MainActivity使用的是navigation组件中的Fragment跳转管理(不知对不对,我暂时也只是调通了,没有仔细研究)。
classMainActivity:BaseActivity() {
privatevalviewBinding:ActivityMainBindingbylazy{
DataBindingUtil.setContentView<ActivityMainBinding>(this,R.layout.activity_main)
}
privatelateinitvarmNavHost:NavHostFragment
privatelateinitvarmNavController:NavController
privatelateinitvarappBarConfiguration:AppBarConfiguration
overridefunonCreate(savedInstanceState:Bundle?) {
super.onCreate(savedInstanceState)
//初始化数据绑定
viewBinding.executePendingBindings()
setupNavController()
setupAppBar()
if(::mNavController.isInitialized&&::appBarConfiguration.isInitialized) {
setupActionBar(mNavController,appBarConfiguration)
}
updateTitleToolbar("kivy")
}
overridefunonBackPressed() {
if(::mNavHost.isInitialized) {
valfragmentsSize=mNavHost.childFragmentManager.fragments.size
if(fragmentsSize>=1) {
super.onBackPressed()
}else{
findNavController(R.id.navHostFragment).navigateUp(appBarConfiguration)
}
}
}
overridefunonSupportNavigateUp():Boolean{
returnif(::mNavHost.isInitialized) {
findNavController(R.id.navHostFragment).navigateUp(appBarConfiguration)
}else{
false
}
}
privatefunsetupNavController() {
mNavHost=supportFragmentManager
.findFragmentById(R.id.navHostFragment)asNavHostFragment??:return
mNavController=mNavHost.navController
}
privatefunsetupAppBar() {
appBarConfiguration=AppBarConfiguration(
setOf(R.id.postFragment),
null
)
}
privatefunsetupActionBar(
navController:NavController,
appBarConfiguration:AppBarConfiguration
) {
setupToolbar(viewBinding.toolbar.toolbar)
setupActionBarWithNavController(navController,appBarConfiguration)
}
}
同时要将布局文件以及navigation的跳转xml迁移过来。样式,颜色,名称都不是重点可以直接复制过来。
<?xmlversion="1.0" encoding="utf-8"?>
<layoutxmlns: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文件先添加上来。
<?xmlversion="1.0" encoding="utf-8"?>
<navigationxmlns: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>() {
overridefungetLayoutId(position:Int,obj:DATA):Int{
returnlayoutId
}
// TODO: Refactor to generic instead of using when condition
overridefungetViewHolder(viewBinding:ViewDataBinding):RecyclerView.ViewHolder{
returnPostViewHolder(viewBindingasPostItemBinding)
}
}
}catch(e:Exception) {
e.printStackTrace()
}
3.5.2.2 PostViewHolder
classPostViewHolder(valbinding:PostItemBinding) :
RecyclerView.ViewHolder(binding.root),
BaseBindableAdapter<PostItem>{
overridefunbind(data:PostItem) {
binding.apply{
item=data
root.setOnClickListener{
// val toPostDetail = PostFragmentDirections.toPostDetailAction(
// data
// )
//
// it.findNavController().navigate(toPostDetail)
}
executePendingBindings()
}
}
}
由于使用了数据绑定,就比较简单了,这里添加的单击事件不在此次考虑中,所以注释了。
3.5.2.3 ViewModel
classPostViewModel(
privatevalappRepository:AppRepository,
schedulerProvider:SchedulerProvider
) :BaseViewModel(schedulerProvider){
valkeywords=Channel<String>(Channel.UNLIMITED)
varpostListSet=MutableSetObservableField<PostItem>()
/*
* We use LiveEvent to publish "states"
* No need to publish and retain any view state
*/
privateval_states=LiveEvent<State>()
valstates:LiveData<State>
get()=_states.toSingleEvent()
fungetPosts() {
_states.value=LoadingState
launch{
try{
valposts=appRepository.getPostsAsync().await()
_states.value=PostListState.from(posts!!)
}catch(error:Throwable) {
_states.value=ErrorState(error)
}
}
}
funsearchPosts(query:String) {
if(query.isNotBlank()) {
_states.value=LoadingState
launch{
try{
valposts=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
*/
sealedclassState
/**
* Generic Loading State
*/
objectLoadingState:State()
/**
* Generic Error state
* @param error - caught error
*/
dataclassErrorState(valerror:Throwable) :State()
dataclassPostListState(
vallist:List<Post>
) :State() {
companionobject{
funfrom(list:List<Post>):PostListState{
returnwith(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
创建布局
overridefunonCreateView(
inflater:LayoutInflater,
container:ViewGroup?,
savedInstanceState:Bundle?
):View?{
viewBinding=PostFragmentBinding.inflate(inflater,container,false)
.apply{
listener=this@PostFragment
vm=viewModel
}
returnviewBinding.root
}
拉取数据
privatefunpullToRefresh() {
viewModel.apply{
isRefreshing.set(true)
if(::onLoadMore.isInitialized)onLoadMore.resetState()
getPosts()
}
}
根据状态处理数据
privatefunsetupObserver()=with(viewModel) {
// Observe ComposeState
states.observe(viewLifecycleOwner,Observer{state->
state?.let{
when(state) {
isLoadingState->showIsLoading()
isPostListState->{
showCategoryItemList(
posts=state.list.map{PostItem.from(it) })
}
isErrorState->showError(state.error)
}
}
})
coroutineLaunch(Main) {
keywords.consumeEach{searchPosts(it) }
}
}
当state为PostListState时,有一个转换过程,将域数据Post转为了PostItem。
其他代码基本上是对以上代码的补充。排除编译错误后运行。
运行代码,结果报错:
Causedby:org.koin.core.error.NoBeanDefFoundException:Nodefinitionfoundforclass:'xyz.wayhua.kivy101.ui.main.fragment.PostViewModel'.Checkyourdefinitions!
atorg.koin.core.scope.Scope.throwDefinitionNotFound(Scope.kt:247)
其原因是引用viewmodel是通过by viewModel<PostViewModel>()来实现的,要通过koin注入进来
privatevalviewModelbyviewModel<PostViewModel>()
要将viewmodel配置到module中。
3.5.4 di 补充
valviewModelModule=module{
viewModel{PostViewModel(get(),get()) }
}
valallModules=listOf(rxModule,roomModule,viewModelModule,remoteDatasourceModule,repoModule)
构造函数中的get(),get(),其实是告诉我们这里有两个参数,都必须在module中配置。
4 总结
总体来说,这一次将代码进行了一次清理,同时结构更加清晰。仍然有很多问题,前面就说过如ListBinding直接指定Adapter,虽然少了一个类,但是确实也不太方便,现在只有一个页面,如果有非常多的页面是否要同时修改那个地方,如果有多种viewholder要显示呢?
还有就是结合以前编写程序的习惯,我更倾向于职责分离。这个会有深挖该源码以后,进一步深入。还有一个问题是中国人的习惯,上拉刷新,下拉加载更多,怎么处理。这里的页面是一次性加载100条,分页怎么办?
还有数据只是个补充,主要用途可能就是fts,没有起到缓存数据的作用。
5 重构
5.1 seal State类问题
在Status.kt文件中引用了Post类,如下
dataclassPostListState(
vallist:List<Post>
) :State() {
companionobject{
funfrom(list:List<Post>):PostListState{
returnwith(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,这开始只是一个设想。
dataclassSuccessState<T>(valdata:List<T>) :State() {
companionobject{
fun<T>from(data:List<T>):SuccessState<T>{
returnwith(data) {
when{
isEmpty()->error("不能为空")
else->SuccessState(this)
}
}
}
}
}
如上编写,结果编译通过了。于是我就有一个大胆的想法,替换掉PostListState。
由于前面代码本来就是可以运行的。重构最好做到一次重构一个地方,现在是替换掉PostListState,那最好的办法就是直接注释掉PostListState类,然后编译,将所有的报错一一改掉,再看运行效果,如果没问题,那就表示重构成功了。
PostFragment中要替换后的代码
privatefunsetupObserver()=with(viewModel) {
// Observe ComposeState
states.observe(viewLifecycleOwner,Observer{state->
state?.let{
when(state) {
isLoadingState->showIsLoading()
isSuccessState<*>->{
showCategoryItemList(
posts=state.data.map{PostItem.from(itasPost) })
}
isErrorState->showError(state.error)
}
}
})
coroutineLaunch(Main) {
keywords.consumeEach{searchPosts(it) }
}
}
SuccessState<*> 表示,里面代码要强制转换。
PostViewModel中要同样替换为
fungetPosts() {
_states.value=LoadingState
launch{
try{
valposts=appRepository.getPostsAsync().await()
_states.value=SuccessState.from(posts!!)
}catch(error:Throwable) {
_states.value=ErrorState(error)
}
}
}
funsearchPosts(query:String) {
if(query.isNotBlank()) {
_states.value=LoadingState
launch{
try{
valposts=appRepository.searchPostsAsync(query).await()
_states.value=SuccessState.from(posts!!)
}catch(error:Throwable) {
_states.value=ErrorState(error)
}
}
}
}
再次运行,发现结果和原来一模一样。