Kotlin学习之开源代码分析、重构(二)

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)

               }

           }

       }

   }

再次运行,发现结果和原来一模一样。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342