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

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 = this@PostFragment
                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)
                }
            }
        }
    }

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值