Building MVI loop with RxJava: how to replace BehaviorSubject with scan() - kotlin

I'm trying to figure out a way to eliminate mutable state and therefore possible race condition. But I can't seem to figure out how to somehow "intertwine" two Observables, while also using "scan".
Hopefully by showing more code I can give you the idea:
private val stateRelay: BehaviorRelay<State> = BehaviorRelay.createDefault(initialState ?: DEFAULT_STATE) // maybe this should be `Observable.startWith()` somehow?
fun bindIntents(intents: Observable<Actions>, stateRenderer: StateRenderer) {
compositeDisposable += intents.concatMap { action ->
when (action) {
is Actions.Increment -> {
Observable.create<Change> { emitter ->
// emit things
}
}
is Actions.Decrement -> {
Observable.create<Change> { emitter ->
// emit things
}
}
}
}.map { change ->
reducer(stateRelay.value, change) // TODO: figure out how to use scan() here, instead of stateRelay.value! :(
}.subscribeBy { newState ->
stateRelay.accept(newState) // there is a chance that the relay shouldn't be here if scan is used
}
compositeDisposable +=
stateRelay // TODO: figure out how to use scan() instead of a relay!
.distinctUntilChanged()
.subscribeBy { state ->
stateRenderer(state)
}
}
fun unbindIntents() {
compositeDisposable.clear()
}
So I'm receiving a Observable<Actions> in this method, which is technically a PublishRelay on the other side (this should be fine).
However, somehow I'm supposed to replace the BehaviorRelay with Observable.scan() (possibly with startWith) to eliminate the mutable state, but I can't seem to wrap my head around what I'm supposed to do for that to happen.
As for the types involved, in case they are needed:
private typealias Reducer = (state: State, change: Change) -> State
private typealias StateRenderer = (state: State) -> Unit
#Parcelize
data class State(val count: Int): Parcelable
How could I wrap intents.concatMap.map, as part of an Observable.scan() (with possibly startWith() and replay(1)), to eliminate my usage of the BehaviorSubject?

I'll elaborate on my comment above.
This is a simple rewrite of your code to do what you're asking for.
fun bindIntents(intents: Observable<Actions>, stateRenderer: StateRenderer) {
val stateObservable = intents.concatMap { action ->
when (action) {
is Actions.Increment -> {
Observable.create<Change> { emitter ->
// emit things
}
}
is Actions.Decrement -> {
Observable.create<Change> { emitter ->
// emit things
}
}
}
}.scan(initialState, { currentState, change -> reducer(currentState, change)})
compositeDisposable +=
stateObservable
.distinctUntilChanged()
.subscribeBy { state ->
stateRenderer(state)
}
}
note that this can be simplified further by inlining the observable I assign to stateObservable in the expression below and using a method reference as the second argument to scan like this
fun bindIntents(intents: Observable<Actions>, stateRenderer: StateRenderer) {
compositeDisposable +=
intents.concatMap { action ->
when (action) {
is Actions.Increment -> {
Observable.create<Change> { emitter ->
// emit things
}
}
is Actions.Decrement -> {
Observable.create<Change> { emitter ->
// emit things
}
}
}
}.scan(initialState, this::reducer)
.distinctUntilChanged()
.subscribeBy { state ->
stateRenderer(state)
}
}

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.

Should I get rid of big switch case?

I have a factory which includes many HTML attribute generators which returns one of them based on the type of attribute, so I wanted to see if there is a better way of doing this.
class AttributeHtmlGeneratorFactory {
fun create(property: String): AttributeHtmlGenerator {
when (property) {
"animation" -> {
return AnimationHtmlGenerator()
}
...
"left", "top" -> {
return PositionHtmlGenerator()
}
...
"scaleX" , "scaleY", ... , "direction" -> {
return UnusedAttributesHtmlGenerator()
}
this when switch has like 20 switch cases in it.
this is the interface which all these classes are using
interface AttributeHtmlGenerator {
fun generateHtml(member: KProperty1<HtmlComponentDataModel, *>, component: HtmlComponentDataModel ): String
}
and this is where and how I'm using all of these:
var result = ""
HtmlComponentDataModel::class.memberProperties.forEach { member ->
val generator = AttributeHtmlGeneratorFactory().create(member.name)
result = result.plus(generator.generateHtml(member, component))
}
return result
also, this is a simple implementation of the interface:
class ButtonFillHtmlGenerator : AttributeHtmlGenerator {
override fun generateHtml(member: KProperty1<HtmlComponentDataModel, *>, component: HtmlComponentDataModel): String {
var result = ""
member.get(component)?.let {
result = result.plus("background-color:${it};")
}
return result
}
}
is there anyway to make this better?
If you just want to reformat the when statement, I suggest you you do like this:
fun create(property: String): AttributeHtmlGenerator = when (property)
{
"animation" -> AnimationHtmlGenerator()
"left", "top" -> PositionHtmlGenerator()
"scaleX", "scaleY", "direction" -> UnusedAttributesHtmlGenerator()
else -> error("No generator found for property $property")
}
If you want to split this logic across modules, you would use a Map.
class AttributeHtmlGeneratorFactory {
private val generatorMap = mutableMapOf<String, () -> AttributeHtmlGenerator>()
init {
assignGeneratorToProperties("animation") { AnimationHtmlGenerator() }
assignGeneratorToProperties("left", "top") { PositionHtmlGenerator() }
}
fun create(property: String): AttributeHtmlGenerator {
return generatorMap[property]?.invoke() ?: error("No generator found for property $property")
}
fun assignGeneratorToProperties(vararg properties: String, provider: () -> AttributeHtmlGenerator) {
properties.forEach {
generatorMap[it] = provider
}
}
}
This way you can call assignGeneratorToProperties in parts of the code and thus split the initialization logic.
Performance-wise, when/if-else statements are really performant when you have a few cases but a HashMap outperforms them for a lot of elements. You decide what to use depending on your case.

Update State outside the composable function. (Jetpack compose)

I am trying to implement redux with Jetpack compose. The scenario looks like this:
I have a list view where I need to show data, in composable function.
#Composable
fun CreateListView(text: String) {
val listdata = state { store.state }
LazyColumn {
//some listview code here
}
}
above, I want to use the data that I got from the redux store. but the store. The subscription method is standalone, and outside the composable. where, though I am able to update the state through new data, but the changes are not reflecting back to composable listview:
// activity page outside composable
private fun storeSubscription(){
viewModel.storeSubscription = store.subscribe {
when (store.state) {
store.state = // list data from some source
}
}
}
Is it possible to update the composable, like above, from outside the function, and without sending any parameter? Since the redux store is a global one, so it should work I think.
You can use MutableLiveData outside of composable function. Use observeAsState() in composable to recompose when data changes.
private val myLive = MutableLiveData<String>()
fun nonComposableScope(){
myLive.postValue("hello")
}
#Composable
fun MyScreen(textLive:LiveData<String>){
val text: String? by textLive.observeAsState()
// use text here
}
Try something like,
#Composable
fun <T> Store<T>.asState(): State<T> {
val result = remember { mutableStateOf(store.state) }
DisposableEffect {
val unsubscribe = store.subscribe {
result.value = store.state
}
onDispose { unsubscribe() }
}
return result
}
#Composable
fun CreateListView(text: String) {
val listdata by store.asState()
LazyColumn {
//some listview code here
}
}
The exact code might differ as I don't know what redux implementation you are using.
This creates an observable state object that will be updated whenever the lambda passed to subscribe is called. Also, it will automatically unsubscribe when CreateListView is no longer part of the composition.
You have to follow the state hosting pattern
From Android Domcumentaiton
Key Term: State hoisting is a pattern of moving state up the tree to
make a component stateless.
When applied to composables, this often means introducing two
parameters to the composable:
value: T: the current value to display. onValueChange: (T) -> Unit: an
event that requests the value to change where T is the proposed new
value.
So in your case you will save the state in the upper Composable that needs to access it, and pass the value of the state and a lambda function to change it to the other Composable, you can learn more from the Official Documentation.
You could simply use a lambda like so:
(An example from an app I am working on.)
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun RumbleSearchResult(rumbleSearchResult: RumbleSearchResult, onClick: () -> Unit) {
ListItem(
headlineText = {
rumbleSearchResult.title?.let { title ->
Text(title)
}
},
supportingText = {
rumbleSearchResult.channel.let { creator ->
val text = when {
rumbleSearchResult.views > 0 -> {
"${creator.name}, ${rumbleSearchResult.views} views"
}
else -> {
creator.name ?: ""
}
}
Row {
Text(text)
if (creator.isVerified) {
Icon(
painter = painterResource(R.drawable.ic_baseline_verified_24),
tint = Color.Cyan,
contentDescription = stringResource(id = R.string.mainActivity_verified_content_description)
)
}
}
}
},
leadingContent = {
AsyncImage(
rumbleSearchResult.thumbnailSrc,
contentDescription = null,
modifier = Modifier.size(100.dp, 100.dp)
)
},
modifier = Modifier.clickable {
onClick.invoke()
}
)
Divider()
}
Main composable:
LazyColumn {
items(viewModel.searchResults) {
RumbleSearchResult(rumbleSearchResult = it) {
openDialog = true
}
}
}

Change source Flow for LiveData

I try to to use Flow instead of LiveData in repos.
In viewModel:
val state: LiveData<StateModel> = stateRepo
.getStateFlow("euro")
.catch {}
.asLiveData()
Repository:
override fun getStateFlow(currencyCode: String): Flow<StateModel> {
return serieDao.getStateFlow(currencyCode).map {with(stateMapper) { it.fromEntityToDomain() } }
}
It works fine if currCode if always the same during viewModel's lifetime, for example euro
but what to do if currCode is changed to dollar?
How to make state to show a Flow for another param?
You need to switchMap your repository call.
I imagine you could dosomething like this:
class SomeViewModel : ViewModel() {
private val currencyFlow = MutableStateFlow("euro");
val state = currencyFlow.switchMap { currentCurrency ->
// In case they return different types
when (currentCurrency) {
// Assuming all of these database calls return a Flow
"euro" -> someDao.euroCall()
"dollar" -> someDao.dollarCall()
else -> someDao.elseCall()
}
// OR in your case just call
serieDao.getStateFlow(currencyCode).map {
with(stateMapper) { it.fromEntityToDomain() }
}
}
.asLiveData(Dispatchers.IO); //Runs on IO coroutines
fun setCurrency(newCurrency: String) {
// Whenever the currency changes, then the state will emit
// a new value and call the database with the new operation
// based on the neww currency you have selected
currencyFlow.value = newCurrency
}
}

How can I write the transformation in a cleaner way

I have a method which takes a list of object (Widget) -- which contains some properties (header) and nested list(component). I want to flatten the list into a single list and have the below code for same:
#SuppressLint("CheckResult")
fun flatten(fatList: Single<List<Widget>>) {
val flatList: MutableList<IUiData> = mutableListOf()
fatList.map {
Observable.fromIterable(it).map { widget ->
if (widget.header.isNotEmpty()) {
flatList.add(ProductHeaderUi(widget.header))
}
widget.componentList.map { component ->
when (component.type) {
TILE_TEXT -> {
flatList.add(HeaderUi(component))
}
TILE_IMAGE -> {
flatList.add(ImageTileUi(component))
}
TILE_FOOTER -> {
flatList.add(FooterUi(component))
}
UNKNOWN -> {
//Do Nothing
}
}
}
}
}
}
I intend to return a Single of List: Single<MutableList<IUiData>> from this method, this purpose can be served right now, but I am looking for a cleaner way
You're using both Rx's Observable map and Kotlin's Iterable map in an unintended way. They are for converting one type to another, not for iterating something.
You've also nested an unnecessary Observable iterable inside the outer-most map function.
You only need to map the output of the Single. Inside the map function, you iterate (not map) the original List to pull out the data you need for the MutableList.
I'm an Rx novice and didn't check this, so sorry about any syntax errors.
fun flatten(fatList: Single<List<Widget>>): Single<MutableList<IUData>> = fatList.map { widgetList ->
val flatList: MutableList<IUiData> = mutableListOf()
for (widget in widgetList) {
if (widget.header.isNotEmpty()) {
flatList.add(ProductHeaderUi(widget.header))
}
for (component in widget.componentList) {
when (component.type) {
TILE_TEXT -> flatList.add(HeaderUi(component))
TILE_IMAGE -> flatList.add(ImageTileUi(component))
TILE_FOOTER -> flatList.add(FooterUi(component))
// Else do nothing
}
}
}
flatList
}
But in keeping with typical Rx chaining syntax, I would make it an extension function, so I'd have to first line like this. Then you can put it right in the middle of an Rx call chain:
fun Single<List<Widget>>.flatten(): Single<MutableList<IUData>> = map { widgetList ->
You can also do this in a more concise, functional, but less efficient way by using Kotlin's flatMap:
fun Single<List<Widget>>.flatten(): Single<MutableList<IUData>> = map {
it.flatMap { widget ->
listOfNotNull(widget.header.takeIf(Header::isNotEmpty)?.let(::ProductHeaderUi))
+
widget.componentList.mapNotNull { component ->
when (component.type) {
TILE_TEXT -> HeaderUi(component)
TILE_IMAGE -> ImageTileUi(component)
TILE_FOOTER -> FooterUi(component)
else -> null
}
}.toMutableList()
}
...where Header is whatever type widget.header uses.