如何在列表中使用mutableStateFlow搜索项目

发布于 2025-02-06 11:11:22 字数 8324 浏览 3 评论 0原文

我正在学习Android中的Kotlin流动。我想在我的列表中基本上即时搜索,然后过滤以在reyclerview中显示。我在Google中搜索,发现了这个惊人的 edimport 文章。这篇文章基本上是从Google搜索的。我想在列表中搜索项目,并在reyclerview中显示。有人可以指导我如何开始。我正在更详细地解释

我有一个搜索框和一个 reyclerview 一个项目 abc One,ABC二,xyz One,XYZ ONE,xyz二...等等/strong>。

主图像当所有数据均组合时

​或资本a 我只想在recyclerview中显示两个项目,看起来

“ nofollow noreferrer”>

” alt =“在 搜索框中的错误文本我想基本上显示一个未找到的短信,看起来像

“在此处输入图像说明”

任何指导都很棒。谢谢,

我添加了我的一件代码

ExploreViewModel.kt

class ExploreViewModel(private var list: ArrayList<Category>) : BaseViewModel() {

    val filteredTopics = MutableStateFlow<List<opics>>(emptyList())
    var topicSelected: TopicsArea? = TopicsArea.ALL
        set(value) {
            field = value
            handleTopicSelection(field ?: TopicsArea.ALL)
        }


    private fun handleTopicSelection(value: TopicsArea) {
        if (value == TopicsArea.ALL) {
            filterAllCategories(true)
        } else {
            filteredTopics.value = list.firstOrNull { it.topics != null && it.title == value.title }
                ?.topics?.sortedBy { topic -> topic.title }.orEmpty()
        }
    }

    fun filterAllCategories(isAllCategory: Boolean) {
        if (isAllCategory && topicSelected == TopicsArea.ALL && !isFirstItemIsAllCategory()) {
            list.add(0, code = TopicsArea.ALL.categoryCode))
        } else if (isFirstItemIsAllCategory()) {
            list.removeAt(0)
        }
        filteredTopics.value = list.flatMap { it.topics!! }.distinctBy { topic -> topic.title }.sortedBy { topic -> topic.title }
    }

    private fun isFirstItemIsAllCategory() = list.firstOrNull()?.code == TopicsArea.ALL
}

xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.appcompat.widget.SearchView
        android:id="@+id/searchView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="16dp"
        app:closeIcon="@drawable/ic_cancel"
        app:layout_constraintBottom_toTopOf="@+id/exploreScroll"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0"
        app:layout_constraintVertical_chainStyle="packed" />

    <HorizontalScrollView
        android:id="@+id/exploreScroll"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="10dp"
        android:scrollbars="none"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/searchView">

        <com.google.android.material.chip.ChipGroup
            android:id="@+id/exploreChips"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:chipSpacingHorizontal="10dp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:singleLine="true"
            app:singleSelection="true" />

    </HorizontalScrollView>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/exploreList"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginBottom="20dp"
        android:paddingTop="10dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHeight_default="wrap"
        app:layout_constraintVertical_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/exploreScroll" />

</androidx.constraintlayout.widget.ConstraintLayout>

category.kt

@Parcelize
data class Category(
    val id: String? = null,
    val title: String? = null,
    val code: String? = null,
    val topics: List<Topics>? = null,
) : Parcelable

topics.kt

@Parcelize
data class Topics(
    val id: String? = null,
    val title: String? = null
) : Parcelable

Dummy数据并来自服务器

fun categoriesList() = listOf(
    Categories("21", "physical", listOf(Topics("1", "Abc one"), Topics("2", "Abc Two"))),
    Categories("2211", "mind", listOf(Topics("1", "xyz one"), Topics("2", "xyz two"))),
    Categories("22131", "motorized", listOf(Topics("1", "xyz three"), Topics("2", "xyz four"))),
)

在我的视图模型列表中持有上方的虚拟数据。在我的recyclerview中,我正在传递整个对象,我正在做flatmap将所有数据组合到列表中。确保在recyclerview中使用主题,并使用title属性。在图像中,ABC一个ABC两个主题中保存。谢谢

我将转到 a2 建议,因为我已经有数据转换为流程并通过适配器。我也添加了我的活动代码。

exploreactivity.kt

class ExploreActivity : AppCompatActivity() {

    private val binding by lazy { ExploreLayoutBinding.inflate(layoutInflater) }
    val viewModel by viewModel<ExploreViewModel> {
        val list = intent?.getParcelableArrayListExtra(LIST_KEY) ?: emptyList<Category>()
        parametersOf(list)
    }
    var exploreAdapter = ExploreAdapter { topic -> handleNextActivity(topic) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        setupView()
    }

    fun setupView() {
        setupSearchView()
        setupFilteredTopic()
        setupExploreAdapter()
    }

    private fun setupFilteredTopic() {
        lifecycleScope.launchWhenCreated {
            repeatOnLifecycle(Lifecycle.State.CREATED) {
                viewModel.filteredTopics.collect { filteredTopicsList ->
                    exploreAdapter.submitList(filteredTopicsList)
                }
            }
        }
    }

    fun setupSearchView() {
        binding.searchView.apply {
            setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                override fun onQueryTextSubmit(query: String?) = false
                override fun onQueryTextChange(newText: String?): Boolean {
                    return true
                }
            })
            
        }
    }

    fun setupExploreAdapter() {
        with(binding.exploreList) {
            adapter = exploreAdapter
        }
    }
}

更新2

ExploreViewModel.kt

 val filteredCategories = query
        .debounce(200) // low debounce because we are just filtering local data
        .distinctUntilChanged()
        .combine(filteredTopics) { queryText, categoriesList ->
            val criteria = queryText.lowercase()
            if (criteria.isEmpty()) {
                    return@combine filteredTopics 
            } else {
                categoriesList.filter { category -> category.title?.lowercase()?.let { criteria.contains(it) } == true }
            }
        }

时,我会遇到错误

当我设置在适配器固定

中 a href =“ https://i.sstatic.net/dkasf.png” rel =“ nofollow noreferrer”>

I am learning kotlin flow in android. I want to basically instant search in my list and filter to show in reyclerview. I searched in google and found this amazing medium post. This post is basically search from google. I want to search item in list and show in reyclerview. Can someone guide me how can I start this. I am explanning in more detail

Suppose I have one SearchBox and one Reyclerview which one item abc one, abc two, xyz one, xyz two... etc.

main image when all data is combine

enter image description here

Scenario 1

when I start typing in SearchBox and enter small a or capital A I want to show only two item matching in recyclerview, look like this

enter image description here

Scenario 2

when I enter any wrong text in SearchBox I want to basically show a text message that not found, look like this

enter image description here

Any guidance would be great. Thanks

I am adding my piece of code

ExploreViewModel.kt

class ExploreViewModel(private var list: ArrayList<Category>) : BaseViewModel() {

    val filteredTopics = MutableStateFlow<List<opics>>(emptyList())
    var topicSelected: TopicsArea? = TopicsArea.ALL
        set(value) {
            field = value
            handleTopicSelection(field ?: TopicsArea.ALL)
        }


    private fun handleTopicSelection(value: TopicsArea) {
        if (value == TopicsArea.ALL) {
            filterAllCategories(true)
        } else {
            filteredTopics.value = list.firstOrNull { it.topics != null && it.title == value.title }
                ?.topics?.sortedBy { topic -> topic.title }.orEmpty()
        }
    }

    fun filterAllCategories(isAllCategory: Boolean) {
        if (isAllCategory && topicSelected == TopicsArea.ALL && !isFirstItemIsAllCategory()) {
            list.add(0, code = TopicsArea.ALL.categoryCode))
        } else if (isFirstItemIsAllCategory()) {
            list.removeAt(0)
        }
        filteredTopics.value = list.flatMap { it.topics!! }.distinctBy { topic -> topic.title }.sortedBy { topic -> topic.title }
    }

    private fun isFirstItemIsAllCategory() = list.firstOrNull()?.code == TopicsArea.ALL
}

xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.appcompat.widget.SearchView
        android:id="@+id/searchView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="16dp"
        app:closeIcon="@drawable/ic_cancel"
        app:layout_constraintBottom_toTopOf="@+id/exploreScroll"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0"
        app:layout_constraintVertical_chainStyle="packed" />

    <HorizontalScrollView
        android:id="@+id/exploreScroll"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="10dp"
        android:scrollbars="none"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/searchView">

        <com.google.android.material.chip.ChipGroup
            android:id="@+id/exploreChips"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:chipSpacingHorizontal="10dp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:singleLine="true"
            app:singleSelection="true" />

    </HorizontalScrollView>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/exploreList"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginBottom="20dp"
        android:paddingTop="10dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHeight_default="wrap"
        app:layout_constraintVertical_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/exploreScroll" />

</androidx.constraintlayout.widget.ConstraintLayout>

Category.kt

@Parcelize
data class Category(
    val id: String? = null,
    val title: String? = null,
    val code: String? = null,
    val topics: List<Topics>? = null,
) : Parcelable

Topics.kt

@Parcelize
data class Topics(
    val id: String? = null,
    val title: String? = null
) : Parcelable

Dummy data and coming from server

fun categoriesList() = listOf(
    Categories("21", "physical", listOf(Topics("1", "Abc one"), Topics("2", "Abc Two"))),
    Categories("2211", "mind", listOf(Topics("1", "xyz one"), Topics("2", "xyz two"))),
    Categories("22131", "motorized", listOf(Topics("1", "xyz three"), Topics("2", "xyz four"))),
)

In my view model list is holding above dummy data. And In my recyclerview I am passing the whole object and I am doing flatMap to combine all data into list. Make sure In recyclerview is using Topic and using title property. In Image Abc one, Abc two is holding in Topic. Thanks

After @Tenfour04 suggestion I will go to A2 suggestion because I have already data which converted into flow and passing in my adapter. I am adding my activity code as well.

ExploreActivity.kt

class ExploreActivity : AppCompatActivity() {

    private val binding by lazy { ExploreLayoutBinding.inflate(layoutInflater) }
    val viewModel by viewModel<ExploreViewModel> {
        val list = intent?.getParcelableArrayListExtra(LIST_KEY) ?: emptyList<Category>()
        parametersOf(list)
    }
    var exploreAdapter = ExploreAdapter { topic -> handleNextActivity(topic) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        setupView()
    }

    fun setupView() {
        setupSearchView()
        setupFilteredTopic()
        setupExploreAdapter()
    }

    private fun setupFilteredTopic() {
        lifecycleScope.launchWhenCreated {
            repeatOnLifecycle(Lifecycle.State.CREATED) {
                viewModel.filteredTopics.collect { filteredTopicsList ->
                    exploreAdapter.submitList(filteredTopicsList)
                }
            }
        }
    }

    fun setupSearchView() {
        binding.searchView.apply {
            setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                override fun onQueryTextSubmit(query: String?) = false
                override fun onQueryTextChange(newText: String?): Boolean {
                    return true
                }
            })
            
        }
    }

    fun setupExploreAdapter() {
        with(binding.exploreList) {
            adapter = exploreAdapter
        }
    }
}

UPDATE 2

ExploreViewModel.kt

 val filteredCategories = query
        .debounce(200) // low debounce because we are just filtering local data
        .distinctUntilChanged()
        .combine(filteredTopics) { queryText, categoriesList ->
            val criteria = queryText.lowercase()
            if (criteria.isEmpty()) {
                    return@combine filteredTopics 
            } else {
                categoriesList.filter { category -> category.title?.lowercase()?.let { criteria.contains(it) } == true }
            }
        }

I am getting error when I set in adapter

fixed

filteredTopics.value

enter image description here

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

草莓酥 2025-02-13 11:11:22

您链接的教程具有搜索视图产生的流程。如果要将搜索功能保留在ViewModel中,则可以将MutableStateFlow放在视图模型中,该搜索视图将间接更新。您可以公开一个属性以更新查询。

这可以做到两种不同的方法,具体取决于您(a)是否已经拥有要快速查询的数据的完整列表,或者(b)您想查询服务器或数据库,每当您的查询文本更改时。

然后甚至(a)可以分解为:(a1)您有一个静态普通旧列表,或(a2)您的源列表来自流量,例如不基于查询参数的返回的房间流。

下面的所有代码都在ViewModel类中。

a1:

private val allCategories = categoriesList()
private val query = MutableStateFlow("")

// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
    get() = query.value
    set(value) { query.value = value }

// This is the flow that should be observed for the updated list that 
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
    .debounce(200) // low debounce because we are just filtering local data
    .distinctUntilChanged()
    .map { 
        val criteria = it.lowercase()
        allCategories.filter { category -> criteria in category.title.lowercase }
    }

a2:

在此示例中,我为上游服务器查询提供了一个简单的占位符流。这可能是任何流程。

private val allCategories = flow {
    categoriesList()
}
private val query = MutableStateFlow("")

// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
    get() = query.value
    set(value) { query.value = value }

// This is the flow that should be observed for the updated list that 
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
    .debounce(200) // low debounce because we are just filtering local data
    .distinctUntilChanged()
    .combine(allCategories) { queryText, categoriesList ->
        val criteria = queryText.lowercase()
        categoriesList.filter { category -> criteria in category.title.lowercase }
    }

b

private val query = MutableStateFlow("")

// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
    get() = query.value
    set(value) { query.value = value }

// This is the flow that should be observed for the updated list that 
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
    .debounce(500) // maybe bigger to avoid too many queries
    .distinctUntilChanged()
    .map { 
        val criteria = it.lowercase()
        categoriesList(criteria) // up to you to implement this depending on source
    }

The tutorial you linked has a Flow produced by the SearchView. If you want to keep the search functionality in your ViewModel, you can put a MutableStateFlow in your ViewModel that will be updated by the SearchView indirectly. You can expose a property for updating the query.

There are two different ways this could be done, depending on whether you (A) already have a complete list of your data that you want to query quickly or (B) you want to query a server or your database every time your query text changes.

And then even (A) can be broken up into: (A1) you have a static plain old List, or (A2) your source List comes from a Flow, such as a returned Room flow that is not based on query parameters.

All code below is in the ViewModel class.

A1:

private val allCategories = categoriesList()
private val query = MutableStateFlow("")

// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
    get() = query.value
    set(value) { query.value = value }

// This is the flow that should be observed for the updated list that 
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
    .debounce(200) // low debounce because we are just filtering local data
    .distinctUntilChanged()
    .map { 
        val criteria = it.lowercase()
        allCategories.filter { category -> criteria in category.title.lowercase }
    }

A2:

In this example I put a simple placeholder flow for the upstream server query. This could be any flow.

private val allCategories = flow {
    categoriesList()
}
private val query = MutableStateFlow("")

// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
    get() = query.value
    set(value) { query.value = value }

// This is the flow that should be observed for the updated list that 
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
    .debounce(200) // low debounce because we are just filtering local data
    .distinctUntilChanged()
    .combine(allCategories) { queryText, categoriesList ->
        val criteria = queryText.lowercase()
        categoriesList.filter { category -> criteria in category.title.lowercase }
    }

B

private val query = MutableStateFlow("")

// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
    get() = query.value
    set(value) { query.value = value }

// This is the flow that should be observed for the updated list that 
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
    .debounce(500) // maybe bigger to avoid too many queries
    .distinctUntilChanged()
    .map { 
        val criteria = it.lowercase()
        categoriesList(criteria) // up to you to implement this depending on source
    }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文