经过几期的基础封装,我们的模块化项目基本已经达到了可用的状态,那么今天就来试试开发一个带搜索的列表页面开发吧~
好吧,我承认偷懒了,中间漏掉了mvvm、paging的基础封装,不过没关系,代码都在传送门
至于mvvm、paging这些并不算新的技术,我想来想去也不知道写什么,就直接看样例代码吧,借着demo我简单说一下基础封装~
老规矩,先看效果~ 由于图片限制大小,这里可能看起来比例和流畅度不太行~~~不过实际体验效果非常棒。
由于没有后台支持,搜索的结果都是静态页,搜索栏中添加的是页码数,理解为实际的搜索条件即可~
基于我们的模块化设计,我们所有的数据交互将封装在data_xxx模块中,这里由于没有后台支持,我随便抓取了一些双色球开奖数据作为基础。
由于使用paging作为媒介,所以首先我们在common_room_db模块中创建entity和dao:
@Entity(primaryKeys = ["number", "lotteryType", "remoteName"])
data class LotteryEntity(
val lotteryType: String,
val numbers: MutableList<String>,
val dateTime: String,
val number: String,
val remoteName: String
)
@Dao
interface LotteryDao {
@Query("SELECT * FROM LotteryEntity WHERE remoteName = :remoteName ORDER BY number desc")
fun getLotteryPagingSource(
remoteName: String
): PagingSource<Int, LotteryEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllAsync(mList: List<LotteryEntity>)
@Query("DELETE FROM LotteryEntity WHERE remoteName = :remoteName")
suspend fun clearLocalDataByRemoteNameAsync(remoteName: String)
}
然后我们创建paging的Mediator,没有用过paging的请看官方教程
这里的BaseRemoteMediator我做了简单封装,没有做过多处理,可以查看BaseRemoteMediator
class LotteryMediator(private val queryStr: String) :
BaseRemoteMediator<LotteryEntity, LotteryModel>("LotteryMediator") {
override suspend fun load(
loadKey: String,
loadType: LoadType,
pageConfig: PagingConfig
): Boolean {
//由于没有后台支持,这里我的数据全是静态页,因此搜索条件最终也拼成了url地址。
//本文提供的是一个思路,这里把queryStr当成参数就可以了
val repo = repo {
api { loadKey.ifBlank { queryStr } }
}
val result = repo.request<LotteryList>()
val lotteryEntities = result.data.map {
it.toLotteryEntity(remoteName)
}
RoomDB.INSTANCE.withTransaction {
if (loadType == LoadType.REFRESH) {
//拿到结果后,如果判断出是刷新,先清空数据库
clearLocalData()
}
LotteryDB.insertAll(lotteryEntities)
RemoteDB.insertAsync(RemoteEntity(remoteName, result.next))
}
return result.next.isBlank()
}
override suspend fun clearLocalData() {
//LotteryDB为数据库查询类,之前讲room的章节有提到过。
LotteryDB.clearLocalDataByRemoteNameAsync(remoteName)
}
}
将Mediator写完后,我们的工作已经完成了一半~没错,paging就是这么简单易用。
接下来我们在feature_xxxx中写页面,并创建相关的provider和service_xxx模块,以便跨模块调用。
页面非常简单,仅包含EditTextView,SwipeRefreshLayout以及RecyclerView
activity代码如下
class LotteriesAct : BaseBindingAct<LotteriedActBinding>() {
override val mBinding by binding<LotteriedActBinding>(R.layout.lotteried_act)
private val mViewModel by viewModels<LotteriesViewModel>()
override fun setupView() {
super.setupView()
setupRv()
setupSearch()
}
override fun variables(): SparseArray<ViewModel> {
return sparse(BR.lotteryVM to mViewModel)
}
override fun setupData() {
super.setupData()
fetchData()
}
private val adapter by lazy { LotteryAdapter() }
@OptIn(FlowPreview::class)
private fun setupSearch() {
mBinding.search
.toFlow()
.debounce(1000)
.distinctUntilChangedBy {
mViewModel.searchObs.value = it.toString()
}.launchIn(lifecycleScope).start()
}
private fun setupRv() {
mBinding.swipeRl.setOnRefreshListener {
adapter.refresh()
}
val concatAdapter = adapter.concat(
VerticalFooterAdapter(adapter),
EmptyAdapter(adapter, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
)
mBinding.rv.layoutManager = LinearLayoutManager(this)
mBinding.rv.adapter = concatAdapter
adapter.setup(lifecycleScope, mBinding.rv) {
mViewModel.loadingObs.value = it.mediator?.refresh == LoadState.Loading
}
}
private fun fetchData() {
lifecycleScope.launch {
mViewModel.posts.collectLatest {
adapter.submitData(it)
}
}
}
}
上述adapter的扩展方法,查看这里
以及viewModel,代码如下:
class LotteriesViewModel : BaseViewModel() {
val loadingObs = MutableLiveData(false)
val searchObs = MutableLiveData("")
@OptIn(ExperimentalCoroutinesApi::class)
val posts = searchObs.asFlow()
.flatMapLatest {
//由于没有服务器支持,所以这里将输入文本框的其实是页码数,这里当作正常的query条件看就可以啦~
val page = it.ifBlank { "1" }
val api = "https://liyuzheng.github.io/bigfile.io/lottery/shuangseqiu$page.html"
fetch(api)
}.cachedIn(viewModelScope)
suspend fun fetch(queryStr: String) = LotteryListRepo.getPagingFlow(this, queryStr)
}
@BindingAdapter("bindLoadState")
fun bindLoadState(view: SwipeRefreshLayout, loading: Boolean?) {
view.isRefreshing = loading == true
}
看吧~代码是不是非常简洁,当然,不要漏了xml,这里使用了databinding库作为页面逻辑展示
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="lotteryVM"
type="yz.l.feature_lottery.LotteriesViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:textSize="16sp"
android:enabled="@{lotteryVM.loadingObs != true}"
bind:layout_constraintTop_toTopOf="parent" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_rl"
android:layout_width="match_parent"
android:layout_height="0dp"
bind:bindLoadState="@{lotteryVM.loadingObs}"
bind:layout_constraintBottom_toBottomOf="parent"
bind:layout_constraintTop_toBottomOf="@id/search">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
bind:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
然后我们简单讲一下viewModel中的两个searchObs和posts
这里是由于使用paging,我们反馈到页面上的数据均来源于room,因此我们需要使用flow的方式监听数据库数据的变动,也就是posts,可以看到posts等同于searchObs的flow模式,并在searchObs值变更时,转换为Mediator的查询,查询的结果转换成页面监听的flow,从而达到查询的目的。
也就是说editTextView值变动->searchObs值变动并转换->调用 LotteryListRepo.getPagingFlow(this, queryStr)触发查询->以flow的形式反馈到posts变量->activity监听flow并调用adapter.submit方式反馈到页面。此页面唯一的难点也就是这里的联动理解了。
本篇章有大量的扩展方法没有贴出,可能造成阅读困难,还是推荐clone完整项目配合文章,并自己打印log尝试理解~
可能好多小伙伴并没有使用过paing,这里还是建议去了解一下,尤其是使用paging做列表的点赞~评论等对列表有修改的地方,paging非常好用。