I have the below working code which uses a dropdown to update the satusFilterFlow to allow for the filtering of characters through the getCharacterList call. The getCharacterList call uses the jetpack paging and returns Flow<PagerData<Character>>.
private val statusFilterFlow = MutableStateFlow<StatusFilter>(NoStatusFilter)
// private val searchFilterFlow = MutableStateFlow<SearchFilter>(NoSearchFilter)
val listData: LiveData<PagingData<Character>> =
statusFilterFlow.flatMapLatest{ statusFilter ->
characterRepository.getCharacterList(null, statusFilter.status)
.cachedIn(viewModelScope)
.flowOn(Dispatchers.IO)
}.asLiveData()
Given the above working solution, what is the correct flow extension to allow for me to add multiple StateFlows as I build out additional filters (e.g. SearchFilter).
I have tried combineTransorm as follows:
private val statusFilterFlow = MutableStateFlow<StatusFilter>(NoStatusFilter)
private val searchFilterFlow = MutableStateFlow<SearchFilter>(NoSearchFilter)
val listData: LiveData<PagingData<Character>> =
statusFilterFlow.combineTransform(searchFilterFlow) { statusFilter, searchFilter ->
characterRepository.getCharacterList(searchFilter.search, statusFilter.status)
.flowOn(Dispatchers.IO)
.cachedIn(viewModelScope)
}.asLiveData()
However, this gives me a "Not enough information to infer type variable R" error.
The usual way to understand and/or fix those errors is to specify types explicitly in the function call:
statusFilterFlow.combineTransform<StatusFilter, SearchFilter, PagingData<Character>>(searchFilterFlow) { ... }
This is orthogonal to the problem at hand, but I'd also suggest using the top-level combineTransform overload that takes all source flows as argument (instead of having the first one as receiver), so there is a better symmetry. Since I believe there is no reason one of the filters is more special than the other.
All in all, this gives:
val listData: LiveData<PagingData<Character>> =
combineTransform<StatusFilter, SearchFilter, PagingData<Character>>(statusFilterFlow, searchFilterFlow) { statusFilter, searchFilter ->
characterRepository.getCharacterList(searchFilter.search, statusFilter.status)
.flowOn(Dispatchers.IO)
.cachedIn(viewModelScope)
}.asLiveData()
For anymore else, this is too complex or doesn't work out for you ... Use Combine then flatMap latest on the top of that.
private val _selectionLocation: MutableStateFlow<Location?> = MutableStateFlow(null)
val searchKeyword: MutableStateFlow<String> = MutableStateFlow("")
val unassignedJobs: LiveData<List<Job>> =
combine(_selectionLocation, searchKeyword) { location: Location?, keyword: String ->
Log.e("HomeViewModel", "$location -- $keyword")
location to keyword
}.flatMapLatest { pair ->
_repo.getJob(Status.UNASSIGNED, pair.first).map {
Log.e("HomeViewModel", "size ${it.size}")
it.filter { it.desc.contains(pair.second) }
}
}.flowOn(Dispatchers.IO).asLiveData(Dispatchers.Main)
Related
I'm trying to get specific behavior with focus and so use something like this :
val (focusA, focusB) = remember { FocusRequester.createRefs() }
And since i didn't get the correct behavior, start to investigate and the destructuring pattern with remember is the problem.
If you try this (this is what is it done under the hood of FocusRequester.createRefs()):
` class MyClass
object MyClassFactory{
operator fun component1() = MyClass()
operator fun component2() = MyClass()
}
fun createRefs() = MyClassFactory
#Composable
private fun ContentBody() {
val (a, b) = remember {
createRefs()
}
Log.d(">>:a", "${a.hashCode()}")
Log.d(">>:b", "${b.hashCode()}")
}
`
You will realise that a and b are new instance each time there is a recomposition.
Does any one have some information about that? Why remember fail with destructuring pattern. We can see many time this pattern (i use it with constraint layout for example), and according to that, it is a complete failure because each time a new instance are created...
What I'm doing wrong? I solved all my problem by using a remember without destructuring.
Thank.
In my application I want update data with SharedFlow and my application architecture is MVI .
I write below code, but just update one of data!
I have 2 spinners and this spinners data fill in viewmodel.
ViewModel code :
class MyViewModel #Inject constructor(private val repository: DetailRepository) : ViewModel() {
private val _state = MutableStateFlow<MyState>(MyState.Idle)
val state: StateFlow<MyState> get() = _state
fun handleIntent(intent: MyIntent) {
when (intent) {
is MyIntent.CategoriesList -> fetchingCategoriesList()
is MyIntent.PriorityList -> fetchingPrioritiesList()
}
}
private fun fetchingCategoriesList() {
val data = mutableListOf(Car, Animal, Color, Food)
_state.value = DetailState.CategoriesData(data)
}
private fun fetchingPrioritiesList() {
val data = mutableListOf(Low, Normal, High)
_state.value = DetailState.PriorityData(data)
}
}
With below codes I filled spinners in fragment :
lifecycleScope.launch {
//Send
viewModel.handleIntent(MyIntent.CategoriesList)
viewModel.handleIntent(MyIntent.PriorityList)
//Get
viewModel.state.collect { state ->
when (state) {
is DetailState.Idle -> {}
is DetailState.CategoriesData -> {
categoriesList.addAll(state.categoriesData)
categorySpinner.setupListWithAdapter(state.categoriesData) { itItem ->
category = itItem
}
Log.e("DetailLog","1")
}
is DetailState.PriorityData -> {
prioritiesList.addAll(state.prioritiesData)
prioritySpinner.setupListWithAdapter(state.prioritiesData) { itItem ->
priority = itItem
}
Log.e("DetailLog","2")
}
}
When run application not show me number 1 in logcat, just show number 2.
Not call this line : is DetailState.CategoriesData
But when comment this line viewModel.handleIntent(MyIntent.PriorityList) show me number 1 in logcat!
Why when use this code viewModel.handleIntent(MyIntent.CategoriesList) viewModel.handleIntent(MyIntent.PriorityList) not show number 1 and 2 in logcat ?
The problem is that a StateFlow is conflated, meaning if you rapidly change its value faster than collectors can collect it, old values are dropped without ever being collected. Therefore, StateFlow is not suited for an event-like system like this. After all, it’s in the name that it is for states rather than events.
It’s hard to suggest an alternative because your current code looks like you shouldn’t be using Flows at all. You could simply call a function that synchronously returns data that you use synchronously. I don’t know if your current code is a stepping stone towards something more complicated that really would be suitable for flows.
There is function collectAsState() applicable to a StateFlow property in order to observe it in a Composable.
A composable requires a StateFlow because StateFlow guarantees an initial value. A Flow doesn't come with that guarantee.
Now, what is the way to go if I have a StateFlow property but I want to apply an operator (like map) before collecting the Flow in the Composable?
Here an example:
Let's say a repository exposes a StateFlow<MyClass>
val myClassStateFlow: StateFlow<MyClass>
data class MyClass(val a: String)
... and a view model has a dependency on the repository and wants to expose only the property a to its Composable...
val aFlow = myClassState.Flow.map { it.a } // <- this is of type Flow<String>
The map operator changes the type from StateFlow<MyClass> to Flow<String>.
Is it semantically justified that aFlow has no initial value anymore? After all its first emission is derived from the initial value of myClassStateFlow.
It's required to convert Flow back into StateFlow at some point. Which is the more idiomatic place for this?
In the view model using stateIn()? How would the code look like?
In the composable using collectAsState(initial: MyClass) and come up with an initial value (although myClassStateFlow had an initial value)?
See this issue on GitHub
Currently there is no built-in way to transform StateFlows, only Flows. But you can write your own.
Way I ended up solving was to use the example in that post.
First create a notion of a DerivedStateFlow.
class DerivedStateFlow<T>(
private val getValue: () -> T,
private val flow: Flow<T>
) : StateFlow<T> {
override val replayCache: List<T>
get () = listOf(value)
override val value: T
get () = getValue()
#InternalCoroutinesApi
override suspend fun collect(collector: FlowCollector<T>) {
flow.collect(collector)
}
}
Then have an extension on StateFlow like the current map extension on Flow
fun <T1, R> StateFlow<T1>.mapState(transform: (a: T1) -> R): StateFlow<R> {
return DerivedStateFlow(
getValue = { transform(this.value) },
flow = this.map { a -> transform(a) }
)
}
Now in your Repository or ViewModel, you can use it as below.
class MyViewModel( ... ) {
private val originalStateFlow:StateFlow<SomeT> = ...
val someStateFlowtoExposeToCompose =
originalStateFlow
.mapState { item ->
yourTransform(item)
}
}
Now you can consume it as you expect in Compose without any special work, since it returns a StateFlow.
I have an array of feedback channels because (outside of question scope) in my ViewModel.
Now, I don't want to expose my MutableLiveData to outside my Viewmodel.
So, i make a private list of LiveData objects, but compiler complains of "Useless Cast"
private val _feedbackChannels = Array(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels
get() = _feedbackChannels.map{
#Suppress("USELESS_CAST") // it is not useless as it no longer exposes the mutableLiveData
it as LiveData<*>
}
Why do I get USELESS_CAST warning?
Compiler doesn't realize you're doing it only to force implication of property type.
Just specify type explicitly and you'll be able to drop the cast entirely. You won't even have to use map, a simple toList() will do:
private val _feedbackChannels = Array(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels : List<LiveData<FeedbackEvent>>
get() = _feedbackChannels.toList()
Clearly the compiler doesn't understand the point of the cast. In order to do this in a more explicit way and remove the costly map function, you can just upcast it like this:
private val _feedbackChannels = Array(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels: Array<out LiveData<FeedbackEvent>>
get() = _feedbackChannels
Edit
If you wanted to expose a List specifically (avoid exposing a mutable array) then you should probably just create one in the first place:
private val _feedbackChannels = List(10) { MutableLiveData<FeedbackEvent>() }
val feedbackChannels: List<out LiveData<FeedbackEvent>>
get() = _feedbackChannels
I'm trying to genericise the boilerplate around a very common pattern, and Kotlin brings me tantalisingly close.
I've built a class that serves as a listener manager, as follows:
class GenericListenerSupport <EventArgumentType, ListenerFunction: (EventArgumentType) -> Unit> {
private val listeners = mutableListOf<ListenerFunction>()
fun addListener(listener: ListenerFunction) {
listeners.add(listener)
}
fun removeListener(listener: ListenerFunction) {
listeners.remove(listener)
}
fun fireListeners(argument: EventArgumentType) {
listeners.forEach { it.invoke(argument) }
}
}
and it can be used as follows:
class ExampleWithArgument {
private val listenerSupport = GenericListenerSupport<String, (String)->Unit>()
fun exampleAdd() {
listenerSupport.addListener({ value -> System.out.println("My string: "+value)})
}
fun exampleFire() {
listenerSupport.fireListeners("Hello")
}
}
So far, so good. But what if the listener has no arguments? Or stretching even further, multiple parameters.
I can scrape through with this:
class ExampleWithNoArgument {
private val listenerSupport = GenericListenerSupport<Nothing?, (Nothing?)->Unit>()
fun exampleAdd() {
listenerSupport.addListener({ System.out.println("I've got no argument")})
}
fun exampleFiring() {
listenerSupport.fireListeners(null)
}
}
but it smells, and obviously it's no use for multiple parameters.
Is there a better way to pull this off? e.g. something supporting this concept:
private val listenerSupport = GenericListenerSupport<???, (String, Double)->Unit>()
Since your GenericListenerSupport declares a type parameter EventArgumentType and expects an instance of it in fun fireListeners(argument: EventArgumentType), I doubt you can support multiple arguments in a clean way. Instead, I'd suggest using a data class (which is not so much extra code), as a clean and type-safe way to wrap multiple values:
data class MyEvent(val id: String, val value: Double)
private val listenerSupport = GenericListenerSupport<MyEvent, (MyEvent) -> Unit>()
As to passing no value, you can also use Unit, the type that has exactly one value Unit:
listenerSupport.fireListeners(Unit)
The type system and resolution won't allow you to pass no argument where a single one is expected, but, as #Ruckus T-Boom suggested, you can make an extension to fire listeners with no value where Unit is expected:
fun GenericListenerSupport<Unit>.fireListeners() = fireListeners(Unit)
A bit off-topic, but I think you can simplify the type if you don't need custom function types and (EventArgumentType) -> Unit is sufficient:
class GenericListenerSupport<EventArgumentType> {
/* Just use `(EventArgumentType) -> Unit` inside. */
}