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

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

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

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

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

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

niph6-j368d.gif

基于我们的模块化设计,我们所有的数据交互将封装在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非常好用。

完整项目地址

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值