前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >手把手教你搭建android模块化项目框架(八)小试牛刀——带搜索列表的页面 paging,mvvm及flow的运用

手把手教你搭建android模块化项目框架(八)小试牛刀——带搜索列表的页面 paging,mvvm及flow的运用

原创
作者头像
支离破碎_superLee
发布2023-09-01 14:04:32
2280
发布2023-09-01 14:04:32
举报
文章被收录于专栏:android模块化框架搭建篇

经过几期的基础封装,我们的模块化项目基本已经达到了可用的状态,那么今天就来试试开发一个带搜索的列表页面开发吧~

好吧,我承认偷懒了,中间漏掉了mvvm、paging的基础封装,不过没关系,代码都在传送门

至于mvvm、paging这些并不算新的技术,我想来想去也不知道写什么,就直接看样例代码吧,借着demo我简单说一下基础封装~

老规矩,先看效果~ 由于图片限制大小,这里可能看起来比例和流畅度不太行~~~不过实际体验效果非常棒。

由于没有后台支持,搜索的结果都是静态页,搜索栏中添加的是页码数,理解为实际的搜索条件即可~

niph6-j368d.gif
niph6-j368d.gif

基于我们的模块化设计,我们所有的数据交互将封装在data_xxx模块中,这里由于没有后台支持,我随便抓取了一些双色球开奖数据作为基础。

由于使用paging作为媒介,所以首先我们在common_room_db模块中创建entity和dao:

代码语言:text
复制
@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

代码语言:text
复制
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代码如下

代码语言:text
复制
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,代码如下:

代码语言:text
复制
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库作为页面逻辑展示

代码语言:html
复制
<?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非常好用。

完整项目地址

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档