3.4 MVVM
3.4.1 viewmodel
3.4.1.1 RxViewModel
abstractclassRxViewModel(privatevalschedulerProvider:SchedulerProvider) :ViewModel() {
varjobs=mutableListOf
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
valisRefreshing=ObservableField
valisError=ObservableField
valerrMsg=ObservableField
}
添加一些状态代码如加载,进度条,错误以及错误信息。都通过ObservableField来定义,具体使用可以通过xml查看,以后还要再细品。
3.4.2 adapter
3.4.2.1 BaseBindableAdapter
interfaceBaseBindableAdapter
funsetHeader(items:T) {}
funsetData(items:List
funsetFooter(items:T) {}
funbind(data:T) {}
}
3.4.2.2 GenericAdapter
abstractclassGenericAdapter:
RecyclerView.Adapter
BaseBindableAdapter{
varlistItems:List
constructor(listItems:List) {
this.listItems=listItems
notifyDataSetChanged()
}
constructor() {
listItems=emptyList()
notifyDataSetChanged()
}
overridefunsetData(items:List) {
this.listItems=items
notifyDataSetChanged()
}
// TODO: To add header?
overridefunsetHeader(items:DATA) {
(this.listItemsasMutableList).add(items)
notifyItemInserted(0)
}
// TODO: To add footer?
overridefunsetFooter(items:DATA) {
(this.listItemsasMutableList).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)?.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
funRecyclerView.initViewHolder(layoutId:Int,
viewType:Int?) {
try{
adapter=object:GenericAdapter() {
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
funRecyclerView.initData(items:List?) {
try{
if(adapterisGenericAdapter<*>) {
(adapterasGenericAdapter).setData(items?:emptyList())
}
}catch(e:Exception) {
e.printStackTrace()
}
}
@SuppressLint(value=["PrivateResource","UNCHECKED_CAST"])
@BindingAdapter(value=["list:items"],requireAll=false)
@JvmStatic
funRecyclerView.initData(items:Set?) {
try{
if(adapterisGenericAdapter<*>) {
(adapterasGenericAdapter).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
}
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迁移过来。样式,颜色,名称都不是重点可以直接复制过来。
xmlns:app="http://schemas.android.com/apk/res-auto" > android:layout_width="match_parent" android:layout_height="match_parent"> android:id="@+id/toolbar" layout="@layout/layout_toolbar"/> 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"/> fragment指到navigation中的NavHostFragment,具体的操作在home_nav_graph中。 由于实际操作和我分析代码是不一样的,所以这里要将一些Fragment,layout文件先添加上来。 xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/homeNavGraph" app:startDestination="@id/postFragment" > android:id="@+id/postFragment" android:name="xyz.wayhua.kivy101.ui.main.fragment.PostFragment" android:label="PostFragment" tools:layout="@layout/post_fragment"> android:id="@+id/toPostDetailAction" app:destination="@id/postDetailFragment"> android:name="postItem" app:argType="xyz.wayhua.kivy.ui.main.fragment.PostItem"/> android:id="@+id/postDetailFragment" android:name="xyz.wayhua.kivy101.ui.main.fragment.PostDetailFragment" android:label="PostDetailFragment" tools:layout="@layout/postdetail_fragment"> android:name="postItem" app:argType="xyz.wayhua.kivy101.ui.main.fragment.PostItem" app:nullable="true" /> 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 funRecyclerView.initViewHolder( layoutId:Int, viewType:Int? )=try{ adapter=object:GenericAdapter() { 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 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 varpostListSet=MutableSetObservableField /* * We use LiveEvent to publish "states" * No need to publish and retain any view state */ privateval_states=LiveEvent valstates:LiveData 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 ) :State() { companionobject{ funfrom(list:List 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 privatevalviewModelbyviewModel 要将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 ) :State() { companionobject{ funfrom(list:List 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 companionobject{ fun 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) } } } } 再次运行,发现结果和原来一模一样。