Rerun StateFlow When Filter Needs to Change? - kotlin

I have a StateFlow containing a simple list of strings. I want to be able to filter that list of Strings. Whenever the filter gets updated, I want to push out a new update to the StateFlow.
class ResultsViewModel(
private val api: API
) : ViewModel() {
var filter: String = ""
val results: StateFlow<List<String>> = api.resultFlow()
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
}
It's easy enough to stick a map onto api.resultFlow():
val results: StateFlow<List<String>> = api.resultFlow()
.map { result ->
val filtered = mutableListOf<String>()
for (r in result) {
if (r.contains(filter)) {
filtered.add(r)
}
}
filtered
}
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
But how do I get the flow to actually emit an update when filter changes? Right now, this only works with whatever the initial value of filter is set to.

You could have the filter update another StateFlow that is combined with the other one. By the way, there's filter function that is easier to use than manually creating another list and iterating to get your results.
class ResultsViewModel(
private val api: API
) : ViewModel() {
private val filterFlow = MutableStateFlow("")
var filter: String
get() = filterFlow.value
set(value) {
filterFlow.value = value
}
val results: StateFlow<List<String>> =
api.resultFlow()
.combine(filterFlow) { list, filter ->
list.filter { it.contains(filter) }
}
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
}

Related

Combine search and sort with kotlin flow

I need to search and sort data simultaneously. I did it for search but it wont trigger for sort. I'm also using pagination.
User can type in searchView and flow will trigger, but problem is when i change sortState (ascending or descending) it wont trigger flow for searching articles on api endpoint.
ViewModel:
private val currentQuery = MutableStateFlow(DEFAULT_QUERY)
private val sortState = MutableStateFlow<SortOrderState>(SortOrderState.Ascending)
val flow = currentQuery
.debounce(2300)
.filter {
it.trim().isNotEmpty()
}
.distinctUntilChanged()
.flatMapLatest { query ->
articleRepository.getSearchResult(query.lowercase(Locale.ROOT),sortState.value)
}
Fragment:
lifecycleScope.launch {
viewModel.flow.collectLatest { articles ->
binding.recyclerViewTop.layoutManager = LinearLayoutManager(context)
binding.recyclerViewTop.adapter = adapter.withLoadStateHeaderAndFooter(
header = ArticleLoadStateAdapter { adapter.retry() },
footer = ArticleLoadStateAdapter { adapter.retry() }
)
adapter.submitData(articles)
}
}
In fragment I have function: viewModel.searchNews(newText)
And in Main activity: viewModel.setSortState(SortOrderState.Ascending) (one menu item clicked) to see if MutableStateFlow.value is changed. I can see that in ViewModel i can change these values but if I do:
val flow=currentQuery.combine(sortState){
query,state ->
}
I never changes if I click on sort menu item, only if I type something to search.
Edit: sortState is not updating in flow variable, I checked setSortState and I can clearly see that state is changed but in flow I only send ascending all the time.
Main activity:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_sortAsc -> {
viewModel.setSortState(SortOrderState.Ascending)
}
R.id.menu_sortDesc -> {
viewModel.setSortState(SortOrderState.Descening)
}
}
return super.onOptionsItemSelected(item)
}
ViewModel:
fun setSortState(sortOrderState: SortOrderState) {
sortState.value = sortOrderState
}
SortOrderState:
sealed interface SortOrderState{
object Ascending : SortOrderState
object Descening : SortOrderState
}
Edit 2: Collecting in HomeFragment it always gives me Ascending value even if i click on menu item for descending sort
lifecycleScope.launch {
viewModel.sortState.collectLatest {
Log.d(TAG, "onCreateViewSort: $it")
}
In ViewModel I can see sortState is changed:
fun setSortState(sortOrderState: SortOrderState) {
sortState.value = sortOrderState
Log.d(TAG, "setSortState: ${sortState.value}")
}
You aren't using your sort state as a Flow. You're only passively using its value, so your output flow won't automatically update when the value changes.
Instead, you need to combine your flows.
Here, I also moved your lowercase transformation before the distinctUntilChanged because I think that makes more logical sense. Also, it makes sense to include the trim in the transformation and not just in the filter.
val flow = currentQuery
.debounce(2300)
.map { it.trim().lowercase(Locale.ROOT) }
.filter { it.isNotEmpty() }
.distinctUntilChanged()
.combine(sortState) { query, sort -> query to sort }
.flatMapLatest { (query, sort) ->
articleRepository.getSearchResult(query, sort)
}
You might also consider tagging this with shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 1) so the search doesn't have to restart on a screen rotation.

Kotlin combine lists based on common property

I have two functions (GetPodsOne and GetPodsTwo) that return me a big csv string. I then do some processing to discard the part of the string I don't want. See snippet below.
var podValues = execGetPodsOne()
val testPodValuesLst: List<String> = podValues.split(",").map { it -> it.substringAfterLast("/") }
testPodValuesLst.forEach { it ->
 println("value from testPodList=$it")
 }
podValues = execGetPodsTwo()
val sitPodValuesLst: List<String> = podValues.split(",").map { it -> it.substringAfterLast("/") }
sitPodValuesLst.forEach { it ->
 println("value from sitPodList=$it")
 }
This leaves me with two lists. See output of the above below:
value from testPodList=api-car-v1:0.0.118
value from testPodList=api-dog-v1:0.0.11
value from testPodList=api-plane-v1:0.0.36
value from sitPodList=api-car-v1:0.0.119
value from sitPodList=api-dog-v1:0.0.12
value from sitPodList=api-plane-v1:0.0.37
What i would like to do is end up with the objects inside a data class like below:
data class ImageVersions(val apiName: String, val testPodVersion: String, val sitPodVersion: String)
api-car-v1, 0.0.118, 0.0.119
api-dog-v1, 0.0.11, 0.0.12
api-plane-v1, 0.0.36, 0.0.37
I've used test and sit above but I'm going to have maybe another 5 environments eventually. Looking for a nice way to get the versions for each api and easily combine into that ImageVersions data class.
thanks
Considering that you're going to have maybe another 5 environments eventually, I tried to write something that will scale well:
enum class Env { Test, Sit }
data class ImageVersions(val apiName: String, val versions: Map<Env, String?>)
fun String.getNameAndVersion() = substringBefore(':') to substringAfter(':')
fun getVersions(envMap: Map<Env, List<String>>): List<ImageVersions> {
val envApiNameMap = envMap.mapValues { it.value.associate(String::getNameAndVersion) }
val allApiNames = envApiNameMap.flatMap { it.value.keys }.distinct()
return allApiNames.map { apiName ->
ImageVersions(apiName, envApiNameMap.mapValues { it.value[apiName] })
}
}
Playground example
So instead of separate val testPodVersion: String, val sitPodVersion: String, here you have a map. Now the structure of ImageVersions always remains the same irrespective of how many environments you have.
getNameAndVersion is a helper function to extract apiName and version from the original string.
getVersions accepts a list of versions corresponding to each environment and returns a list of ImageVersions
envApiNameMap is same as envMap just that the list is now a map of apiName and its version.
allApiNames contains all the available apiNames from all environments.
Then for every apiName, we take all the versions of that apiName from all the environments.
In future, if your have another environment, just add it in the Env enum and pass an extra map entry in the envMap of getVersions. You need not modify this function every time you have a new environment.
How about this:
val testPodValuesMap = testPodValuesLst.associate { it.split(':').zipWithNext().single() }
val sitPodValuesMap = sitPodValuesLst.associate { it.split(':').zipWithNext().single() }
val mergedMap = (testPodValuesMap.keys + sitPodValuesMap.keys).associateWith { key ->
testPodValuesMap.getValue(key) to sitPodValuesMap.getValue(key)
}
val imageVersions = mergedMap.map { (k, v) -> ImageVersions(k, v.first, v.second) }
println(imageVersions.joinToString("\n"))
which prints
ImageVersions(apiName=api-car-v1, testPodVersion=0.0.118, sitPodVersion=0.0.119)
ImageVersions(apiName=api-dog-v1, testPodVersion=0.0.11, sitPodVersion=0.0.12)
ImageVersions(apiName=api-plane-v1, testPodVersion=0.0.36, sitPodVersion=0.0.37)
As a first step I would extract the apiNames from both lists:
val apiNames = list1.map { it.replace("value from ", "").split("[=:]".toRegex())[1] }
.plus(list2.map { it.replace("value from ", "").split("[=:]".toRegex())[1] })
.distinct()
Then I'd create the ImageVersions instances by looping over apiNames:
val result = apiNames
.map { apiName ->
ImageVersions(
apiName,
(list1.firstOrNull { it.contains(apiName) } ?: "").split(":")[1],
(list2.firstOrNull { it.contains(apiName) } ?: "").split(":")[1]
)
}
.toList()
The reason to first extract the apiNames is, that apiNames missing in one of the two lists will still end up in the final result.
Kotlin Playground

Two Spinners Populated By Same ArrayList

So I am trying to populate two Spinners in the same Fragment, both using the same list, but to display different items.
I have the following data class:
data class ProductTypeObject (
//ProductType fields (2 fields)
var productType: String = "",
var productGroup: String = "",
#ServerTimestamp
var dateEditedTimestamp: Date? = null,
#Exclude #set:Exclude #get:Exclude
var productTypeID: String = ""
) : Serializable {
override fun toString(): String {
return productType
}
}
The Spinner is populated in the Fragment when the list is observed from the ViewModel as below:
// Observe ProductTypes and populate Spinner
businessViewModel.allAppDataProductTypes.observe(viewLifecycleOwner, Observer { productTypeArrayList ->
if (!productTypeArrayList.isNullOrEmpty()){
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, productTypeArrayList)
binding.inventoryAddEditProductGroupSpinner.adapter = adapter
}
})
This shows a list of product types as I have specified this in the toString()of the object, but is there a way to direct a second Spinner to show a list ofproduct group?
If you don't need to retrieve the values from the spinners, it's easiest to map the values to a new list:
businessViewModel.allAppDataProductTypes.observe(viewLifecycleOwner, Observer { productTypeArrayList ->
if (!productTypeArrayList.isNullOrEmpty()){
//...
val adapter2 = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item,
productTypeArrayList.map(ProductTypeObject::productGroup)
//...
}
})
If you need both Spinners to be able to retrieve the original item type, then you can't use the ArrayAdapter class as is, since it relies purely on the toString() of your class. You can subclass it like this for a more flexible version that lets you pass property or lambda that is used instead of toString(). I didn't test it, but I think it will do what you want. If you use this class, you don't need to override toString() in your original data class.
class CustomArrayAdapter<T : Any>(
context: Context,
items: List<T>,
val itemToCharSequence: T.() -> CharSequence = Any::toString
) : ArrayAdapter<T>(context, 0, items) {
private val inflater = LayoutInflater.from(context)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return (convertView ?: inflater.inflate(android.R.layout.simple_spinner_item, parent, false))
.apply {
val item = getItem(position)!! // will never be null inside getView()
(this as TextView).text = itemToCharSequence(item)
}
}
}
Usage:
val typeAdapter = CustomArrayAdapter(requireContext(), productTypeArrayList, ProductTypeObject::productType)
val groupAdapter = CustomArrayAdapter(requireContext(), productTypeArrayList, ProductTypeObject::productGroup)

How to retrieve data from Firestore that stored as an Array and set them as EditText values in Kotlin?

I have stored some data as an array in Firestore using the following code. Now, I want to get those values and put them one by one into the EditTexts. How can I do that?
private fun addZipToFirebase() {
val zipList = createListOfZipCodes()
mFireStore.collection(Constants.USERS)
.document(FirestoreClass().getCurrentUserID())
.update("zip_codes", zipList)
.addOnSuccessListener {
Toast.makeText(
this#AssignZIPCodeActivity,
"Zip Codes updates successfully",
Toast.LENGTH_SHORT
).show()
}
.addOnFailureListener { exception ->
Log.e(
javaClass.simpleName,
exception.message,
exception
)
}
}
Edit:
I am trying with the following code to get the data. I want each Zip Code under the field name zip_codes (in the screenshot), in each EditText (etPinCodeOne, etPinCodeTwo, etPinCodeThree and so on). But with following code what I am getting is all the zip codes together in the EditText. Exctely like, [123456, 789456, 132645,798654, 798654, 799865, 764997, 497646, 946529, 946585]. I want each codes in each EditText.
private fun getZipCodesFromFirebase() {
mFireStore.collection(Constants.USERS)
.document(FirestoreClass().getCurrentUserID())
.get()
.addOnSuccessListener { document ->
val list: ArrayList<String> = ArrayList()
list.add(document["zip_codes"].toString())
Toast.makeText(this#AssignZIPCodeActivity,list.toString(),Toast.LENGTH_SHORT).show()
binding.etZipCodeOne.setText(list[0])
}
}
Can someone help me with this please?
To be able to get the zip_codes array, you need to have inside your User class, a property called zip_codes that needs to be declared of type List:
val zip_codes: List<String>
Now, to get it accordingly, please use the following lines of code:
val uid = FirebaseAuth.getInstance().currentUser!!.uid
val rootRef = FirebaseFirestore.getInstance()
val usersRef = rootRef.collection("users")
val uidRef = usersRef.document(uid)
uidRef.get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val document = task.result
if (document.exists()) {
val zip_codes = document.toObject(User::class.java).zip_codes
//Do what you need to do with your list
} else {
Log.d(TAG, "No such document")
}
} else {
Log.d(TAG, "get failed with ", task.exception)
}
}
Since you are getting multiple zip codes, you should consider using a ListView, or even better a RecyclerView, rather than EditTexts.

How do i filter and copy values to a list of objects based on another list in Kotlin

I am trying to filter a list of objects based on a certain condition from a second list and then update/copy certain values from the second list to the already filtered list.
I tried this:
val filteredList = firstObjectList.stream()
.filter { first ->
secondObjectList.stream()
.anyMatch { second ->
second.sharedId == first.shareId
}
}.toList()
filteredList.map { filtered ->
secondObjectList.forEach { so ->
if(filtered.shareId == so.shareId){
val asset= Assets()
asset.address = so.address
asset.assetValue = so.assetValue
filtered.asset = asset
}
}
}
return filteredList
here are the objects:
Class firstObject(
val shareId: Int,
var asset : Asset? = null)
Class secondObject(
val shareId: Int,
var asset: Assets)
Class Assets(
val address: String,
val assetValue: Double)
This works but obviously not very efficient and Java based. How can I improve and write this in idiomatic kotlin? as i don’t seem to be able to chain operators correctly. Thanks in Advance.
val map = secondObjectList.associateBy { it.shareId }
val filteredList = firstObjectList
.filter { it.shareId in map }
.onEach { fo ->
fo.asset = map.getValue(fo.shareId).asset.let { Assets(it.address, it.assetValue) }
}