I've been working on how to filter my data based on a certain condition.
I worked around the issue by creating a viewModel with a assisted injection for my tag. But I think if I need to do more filtering, this solution is going to be inefficient.
Here's the sample code of what I did
sealed interface TasksState {
data class Success(val tasks: Map<TimeType, RelatedTasksMetaDataResult>): TasksState
object Loading: TasksState
object Error: TasksState
}
class TasksViewModel #AssistedInject constructor(
getTasksFlowUseCase: GetTasksFlowUseCase,
relatedTasksUseCase: RelatedTasksUseCase,
#Assisted
private val selectedTag: Tag
): ViewModel() {
private val _state = MutableStateFlow<TasksState>(TasksState.Loading)
val state: StateFlow<TasksState> = _state.asStateFlow()
init {
viewModelScope.launch {
tasksUiStream(
getTasksFlowUseCase,
relatedTasksUseCase,
Filter.TagFilter(selectedTag, Grouping.DateGrouping)
).collect { _state.value = it }
}
}
/**
* Get all tasks as stream and convert them to UI state
*/
private fun tasksUiStream(
getTasksFlowUseCase: GetTasksFlowUseCase,
relatedTasksUseCase: RelatedTasksUseCase,
filter: Filter,
completed: Boolean = false
): Flow<TasksState> {
return getTasksFlowUseCase(
TasksRetrievalParameters(filter, completed)
)
.map { tasksResult ->
when(tasksResult) {
is Success -> {
val groupedTasks = tasksResult.data
val tasks = mutableMapOf<TimeType, RelatedTasksMetaDataResult>()
groupedTasks?.forEach{ groupedTask ->
/* Make sure the keys of the grouped tasks are instance of TimeType */
val dueDate = groupedTask.key as? TimeType
dueDate?.let {
tasks[dueDate] = relatedTasksUseCase(groupedTask.value)
}
}
TasksState.Success(tasks.toSortedMap())
}
is Loading -> {
TasksState.Loading
}
is Error -> {
TasksState.Error
}
}
}
}
#AssistedFactory
interface Factory {
fun create(selectedTag: Tag): TasksViewModel
}
#Suppress("UNCHECKED_CAST")
companion object {
fun provideFactory(
assistedFactory: Factory,
selectedTag: Tag
): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return assistedFactory.create(selectedTag) as T
}
}
}
}
So what I tried to do instead now, is to use a mutable state flow and filter once the value change. Here's the sample code:
data class UiState(
val filterQuery: FilterQuery = FilterQuery(),
val projectsState: ProjectsState = ProjectsState.Loading
)
sealed interface ProjectsState {
object Loading: ProjectsState
data class Success(val projects: List<ProjectResult>): ProjectsState
object Error: ProjectsState
}
#OptIn(ExperimentalCoroutinesApi::class)
#HiltViewModel
class ProjectsViewModel #Inject constructor(
getAllProjectsFlowUseCase: GetAllProjectsFlowUseCase,
getProjectsFlowUseCase: GetProjectsFlowUseCase,
private val upsertProjectUseCase: UpsertProjectUseCase
): ViewModel() {
private val _filterQuery = MutableStateFlow(FilterQuery())
val state: StateFlow<UiState> = _filterQuery
.flatMapLatest {
getProjectsFlowUseCase(it)
.mapLatest { result ->
UiState(
filterQuery = it,
projectsState =
when(result) {
is Result.Success -> ProjectsState.Success(result.data ?: emptyList())
is Result.Loading -> ProjectsState.Loading
is Result.Error -> ProjectsState.Error
}
)
}
}
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
UiState()
)
}
Unfortunately with this approach, once I move to the screen, the screen keeps loading with the initial state without emitting the data(But also sometimes the date are emitted directly). In order for the value to be emitted if that occurs, I have to change the filter query on the screen and change it again back to the initial state.
Is there any better approach to filter data based on certain value with kotlin Flow ?
Related
I would like to combine two livedata / flow values conditionally. Here is my problem: Currently, I have two livedata / flow values. One LiveData value always emits type Status<Unit> while the second LiveData value emits T. When the first LiveData value emits Status.Success I manually set View to visible and now know that the second LiveData value will emit T.
What I now want is, to get the second Livedata value T inside my first LiveData value onSucess block
Current approach
class MyViewModel() : ViewModel() {
val myDownloadState: LiveData<Status<Unit>> = ...
val myDownloadData: LiveData<T> = ...
}
class MyFragment : Fragment() {
val myViewModel = ...
myViewModel.myDownloadState.observeStatus(
viewLifecycleOwner,
onSuccess = { it: Unit
binding.myRecyclerView.isVisible = true
},
onLoading = {
binding.myRecyclerView.isVisible = false
},
onError = { it: String?
toast(it.toString())
}
)
myViewModel.myDownloadData.observe(viewLifecycleOwner) { data: T
binding.myRecylerView.submitList(data)
}
}
What I want
class MyViewModel() : ViewModel() {
val myCombinedState: LiveData<Status<T>> = ...
}
class MyFragment : Fragment() {
val myViewModel = ...
myViewModel.myCombinedState.observeStatus(
viewLifecycleOwner,
onSuccess = { it: T
binding.myRecyclerView.isVisible = true
binding.myRecylerView.submitList(data)
},
onLoading = {
binding.myRecyclerView.isVisible = false
},
onError = { it: String?
toast(it.toString())
}
)
}
Here is where the two livedata values are coming from:
interface IWorkerContract<T, R> {
// this is "myDownloadData"
val appDatabaseData: LiveData<R>
// this is "myDownloadState"
val workInfo: LiveData<Status<Unit>>
}
#Singleton
class DocumentWorkerContract #Inject constructor(
#ApplicationContext private val context: Context,
private val documentDao: DocumentDao,
) : IWorkerContract<Unit, List<DocumentCacheEntity>> {
// this is "myDownloadData"
override val appDatabaseData: LiveData<List<DocumentCacheEntity>>
get() = documentDao.getListLiveData()
// this is "myDownloadState"
override val workInfo: LiveData<Status<Unit>>
get() = WorkManager
.getInstance(context)
.getWorkInfoByIdLiveData(worker.id)
.mapToState()
}
State Class
sealed class Status<out T> {
data class Success<out T>(val data: T) : Status<T>()
class Loading<out T>(val message: String? = null) : Status<T>()
data class Failure<out T>(val message: String?) : Status<T>()
companion object {
fun <T> success(data: T) = Success(data)
fun <T> loading(message: String? = null) = Loading<T>(message)
fun <T> failed(message: String?) = Failure<T>(message)
}
}
I think you should try using switchMap in combination with map in this case.
Try it this way:
class MyViewModel() : ViewModel() {
val myCombineState: LiveData<List<DocumentCacheEntity>> = myDownloadState.switchMap { state ->
myDownloadData.map { data ->
when (state) {
is Status.Success -> {
Status.Success(data)
}
is Status.Loading -> {
Status.Loading()
}
is Status.Error -> {
Status.Error()
}
}
}
}
}
I'd love to observe changes of a shared preference. Here is how I Use Kotlin Flow to do it:
Data source.
interface DataSource {
fun bestTime(): Flow<Long>
fun setBestTime(time: Long)
}
class LocalDataSource #Inject constructor(
#ActivityContext context: Context
) : DataSource {
private val preferences = context.getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE)
#ExperimentalCoroutinesApi
override fun bestTime() = callbackFlow {
trySendBlocking(preferences, PREF_KEY_BEST_TIME)
val listener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == PREF_KEY_BEST_TIME) {
trySendBlocking(sharedPreferences, key)
}
}
preferences.registerOnSharedPreferenceChangeListener(listener)
awaitClose { // NEVER CALLED
preferences.unregisterOnSharedPreferenceChangeListener(listener)
}
}
#ExperimentalCoroutinesApi
private fun ProducerScope<Long>.trySendBlocking(
sharedPreferences: SharedPreferences,
key: String?
) {
trySendBlocking(sharedPreferences.getLong(key, 0L))
.onSuccess { }
.onFailure {
Log.e(TAG, "", it)
}
}
override fun setBestTime(time: Long) = preferences.edit {
putLong(PREF_KEY_BEST_TIME, time)
}
companion object {
private const val TAG = "LocalDataSource"
private const val PREFS_FILE_NAME = "PREFS_FILE_NAME"
private const val PREF_KEY_BEST_TIME = "PREF_KEY_BEST_TIME"
}
}
Repository
interface Repository {
fun observeBestTime(): Flow<Long>
fun setBestTime(bestTime: Long)
}
class RepositoryImpl #Inject constructor(
private val dataSource: DataSource
) : Repository {
override fun observeBestTime() = dataSource.bestTime()
override fun setBestTime(bestTime: Long) = dataSource.setBestTime(bestTime)
}
ViewModel
class BestTimeViewModel #Inject constructor(
private val repository: Repository
) : ViewModel() {
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(0L)
val uiState: StateFlow<Long> = _uiState
init {
viewModelScope.launch {
repository.observeBestTime()
.onCompletion { // CALLED WHEN THE SCREEN IS ROTATED OR HOME BUTTON PRESSED
Log.d("myTag", "viewModelScope onCompletion")
}
.collect { bestTime ->
_uiState.value = bestTime
}
}
}
fun setBestTime(time: Long) = repository.setBestTime(time)
}
Fragment.
#AndroidEntryPoint
class MetaDataFragment : Fragment(R.layout.fragment_meta_data) {
#Inject
lateinit var timeFormatter: TimeFormatter
#Inject
lateinit var bestTimeViewModel: BestTimeViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val bestTimeView = view.findViewById<TextView>(R.id.best_time_value)
// Create a new coroutine in the lifecycleScope
viewLifecycleOwner.lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Trigger the flow and start listening for values.
// This happens when lifecycle is STARTED and stops
// collecting when the lifecycle is STOPPED
bestTimeViewModel.uiState
.map { millis ->
timeFormatter.format(millis)
}
.onCompletion { // CALLED WHEN THE SCREEN IS ROTATED OR HOME BUTTON PRESSED
Log.d("MyApp", "onCompletion")
}
.collect {
bestTimeView.text = it
}
}
}
}
}
I've noticed that awaitClose is never called. But this is where my clean-up code is. Please advise. If it's not a good idea to use callbackFlow in the first place, please let me know (as you can see some functions are ExperimentalCoroutinesApi meaning their behaviour can change)
I found a solution that allows me to save a simple dataset such as a preference and observe its changes using Kotlin Flow. It's Preferences DataStore.
This is the code lab and guide I used:
https://developer.android.com/codelabs/android-preferences-datastore#0
https://developer.android.com/topic/libraries/architecture/datastore
and this is my code:
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException
data class UserPreferences(val bestTime: Long)
private const val USER_PREFERENCES_NAME = "user_preferences"
private val Context.dataStore by preferencesDataStore(
name = USER_PREFERENCES_NAME
)
interface DataSource {
fun userPreferencesFlow(): Flow<UserPreferences>
suspend fun updateBestTime(newBestTime: Long)
}
class LocalDataSource(
#ApplicationContext private val context: Context,
) : DataSource {
override fun userPreferencesFlow(): Flow<UserPreferences> =
context.dataStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val bestTime = preferences[PreferencesKeys.BEST_TIME] ?: 0L
UserPreferences(bestTime)
}
override suspend fun updateBestTime(newBestTime: Long) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.BEST_TIME] = newBestTime
}
}
}
private object PreferencesKeys {
val BEST_TIME = longPreferencesKey("BEST_TIME")
}
and the dependency to add to build.gradle:
implementation "androidx.datastore:datastore-preferences:1.0.0"
The problem is, that you are injecting your ViewModel as if it was just a regular class, by using
#Inject
lateinit var bestTimeViewModel: BestTimeViewModel
Because of this, the ViewModel's viewModelScope is never cancelled, and therefor the Flow is collected forever.
Per Documentation, you should use
privat val bestTimeViewModel: BestTimeViewModel by viewModels()
This ensures that the ViewModel's onCleared method, which in turn will cancel the viewModelScope, is called when your Fragment is destroyed.
Also make sure your ViewModel is annotated with #HiltViewModel:
#HiltViewModel
class BestTimeViewModel #Inject constructor(...) : ViewModel()
LeakCanary is telling me that one of my ViewModels is leaking but after playing around for 2 days I can't get the leak to go away.
Here is why LeakCanary shows
Here is the Fragment getting the ViewModel
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java).apply {
getStrains(arguments?.getString(BREEDER_ID_KEY, "")!!)
}
Here is the ViewModel
class ViewBreederViewModel(application: Application) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
val strainList = MutableLiveData<List<MinimalStrain>>()
fun getStrains(breederId: String) {
viewModelScope.launch {
breederRepository.getMinimalStrains(breederId).observeForever {
strainList.value = it
}
}
}
}
Here is the BreederRepository:
class BreederRepository(context: Context) {
private val dao: BreederDao
private val breederApi = RetrofitClientInstance.getInstance(context).breederAndStrainIdsApi
init {
val database: Db = Db.getInstance(
context
)!!
dao = database.breederDao()
}
suspend fun getMinimalStrains(breederId: String): LiveData<List<MinimalStrain>> =
withContext(Dispatchers.IO) {
dao.getMinimalStrains(breederId)
}
}
Here is the Db class
#Database(
entities = [Breeder::class, Strain::class],
version = 1,
exportSchema = true)
#TypeConverters(RoomDateConverter::class)
abstract class Db : RoomDatabase() {
abstract fun breederDao(): BreederDao
companion object {
private var instance: Db? = null
#JvmStatic
fun getInstance(context: Context): Db? {
if (instance == null) {
synchronized(Db::class) {
instance = Room.databaseBuilder(
context.applicationContext,
Db::class.java, "seedfinder_db"
)
.build()
}
}
return instance
}
}
}
You're using observeForever, which, as the name suggest, will keep observing forever, even after your ViewModel is cleared. Room does not require using a suspend method for DAO methods that return a LiveData and that is never the right approach in any case - LiveData is already asynchronous.
Instead, you should be transforming your LiveData, using your breederId as the input to your strainList LiveData:
class ViewBreederViewModel(application: Application) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
private val currentBreederId = MutableLiveData<String>()
// Here we use the switchMap method from the lifecycle-livedata-ktx artifact
val strainList: LiveData<String> = currentBreederId.switchMap {
breederId -> breederRepository.getMinimalStrains(breederId)
}
private fun setBreederId(breederId: String) {
currentBreederId.value = breederId
}
}
Where your getMinimalStrains becomes:
fun getMinimalStrains(breederId: String): LiveData<List<MinimalStrain>> =
dao.getMinimalStrains(breederId)
And you use it by setting your breederId in your UI and observing your strainList as before:
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java).apply {
setBreederId(arguments?.getString(BREEDER_ID_KEY, "")!!)
}
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
If you're using Saved State module for ViewModels (which is the default if you're using the latest stable Fragments / Activity libraries), then you can use SavedStateHandle, which is automatically populated from your Fragment's arguments and skip the setBreederId() entirely:
class ViewBreederViewModel(
application: Application,
savedStateHandle: SavedStateHandle
) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
// Here we use the switchMap method from the lifecycle-livedata-ktx artifact
val strainList: LiveData<String> = savedStateHandle
.getLiveData(BREEDER_ID_KEY) // Automatically populated from arguments
.switchMap {
breederId -> breederRepository.getMinimalStrains(breederId)
}
}
Which means your code can simply become:
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java)
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
And if you use the fragment-ktx artifact, you can simplify this further to:
// Move this to where you declare viewModel
val viewModel: ViewBreederViewModel by viewModels()
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
In my viewmodel class
class ViewModel(application: Application) : AndroidViewModel(application) {
private val repository: Repository by lazy {
Repository.getInstance(getApplication<BaseApplication>().retrofitFactory)
}
private var _liveData = MutableLiveData<ItemState>()
val liveData: LiveData<ItemState> = _liveData
init {
fetchData()
}
private fun fetchData() {
repository.getLiveData().observeForever(liveDataObserver)
}
override fun onCleared() {
super.onCleared()
repository.getLiveData().removeObserver(liveDataObserver)
}
private val liveDataObserver = Observer<User> {
if (it != null) {
setData(it)
}
}
private fun setData(it: User) =viewModelScope.launch {
val list1 = mutableListOf<something1>()
val list2 = mutableListOf<something2>()
list1.add(it.data)
list2.add(it.data)
}
_liveData.value = ItemState.State1(list1)
delay(1)
_liveData.value = ItemState.State2(list2)
}
The ItemState is a sealed class with two data members
sealed class ItemState {
data class State1(val list: List<something1>) : ItemState()
data class State2(val list: List<something2>) : ItemState()
}
Activity Observer Code
viewModel.liveData.observe(this, Observer {
loadDataIntoUi(it)
})
private fun loadDataIntoUi(data: ItemState) {
when (data) {
is ItemState.State1 -> adaptr1.addItems(data.list)
is ItemState.State2 -> adaptr2.addItems(data.list)
}
Now if i don't use delay in my viewModel here like above the livedata first value that is Office doesn't get observed but it works fine with delay
I have done a lot of research didn't understand why this happening also I have many alternate solutions to this but my question is why delay make's it working
I am trying out Kotlin Coroutines and Flow for the first time and I am trying to reproduce a certain flow I use on Android with RxJava with an MVI-ish approach, but I am having difficulties getting it right and I am essentially stuck at this point.
The RxJava app looks essentially like this:
MainActivityView.kt
object MainActivityView {
sealed class Event {
object OnViewInitialised : Event()
}
data class State(
val renderEvent: RenderEvent = RenderEvent.None
)
sealed class RenderEvent {
object None : RenderEvent()
class DisplayText(val text: String) : RenderEvent()
}
}
MainActivity.kt
MainActivity has an instance of a PublishSubject with a Event type. Ie MainActivityView.Event.OnViewInitialised, MainActivityView.Event.OnError etc. The initial Event is sent in onCreate() via the subjects's .onNext(Event) call.
#MainActivityScope
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var subscriptions: CompositeDisposable
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit var onViewInitialisedSubject: PublishSubject<MainActivityView.Event.OnViewInitialised>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupEvents()
}
override fun onDestroy() {
super.onDestroy()
subscriptions.clear()
}
private fun setupEvents() {
if (subscriptions.size() == 0) {
Observable.mergeArray(
onViewInitialisedSubject
.toFlowable(BackpressureStrategy.BUFFER)
.toObservable()
).observeOn(
Schedulers.io()
).compose(
viewModel()
).observeOn(
AndroidSchedulers.mainThread()
).subscribe(
::render
).addTo(
subscriptions
)
onViewInitialisedSubject
.onNext(
MainActivityView
.Event
.OnViewInitialised
)
}
}
private fun render(state: MainActivityView.State) {
when (state.renderEvent) {
MainActivityView.RenderEvent.None -> Unit
is MainActivityView.RenderEvent.DisplayText -> {
mainActivityTextField.text = state.renderEvent.text
}
}
}
}
MainActivityViewModel.kt
These Event's are then picked up by a MainActivityViewModel class which is invoked by .compose(viewModel()) which then transform the received Event into a sort of a new State via ObservableTransformer<Event, State>. The viewmodel returns a new state with a renderEvent in it, which can then be acted upon in the MainActivity again via render(state: MainActivityView.State)function.
#MainActivityScope
class MainActivityViewModel #Inject constructor(
private var state: MainActivityView.State
) {
operator fun invoke(): ObservableTransformer<MainActivityView.Event, MainActivityView.State> = onEvent
private val onEvent = ObservableTransformer<MainActivityView.Event,
MainActivityView.State> { upstream: Observable<MainActivityView.Event> ->
upstream.publish { shared: Observable<MainActivityView.Event> ->
Observable.mergeArray(
shared.ofType(MainActivityView.Event.OnViewInitialised::class.java)
).compose(
eventToViewState
)
}
}
private val eventToViewState = ObservableTransformer<MainActivityView.Event, MainActivityView.State> { upstream ->
upstream.flatMap { event ->
when (event) {
MainActivityView.Event.OnViewInitialised -> onViewInitialisedEvent()
}
}
}
private fun onViewInitialisedEvent(): Observable<MainActivityView.State> {
val renderEvent = MainActivityView.RenderEvent.DisplayText(text = "hello world")
state = state.copy(renderEvent = renderEvent)
return state.asObservable()
}
}
Could I achieve sort of the same flow with coroutines/Flow/Channels? Possibly a bit simplified even?
EDIT:
I have since found a solution that works for me, I haven't found any issues thus far. However this solution uses ConflatedBroadcastChannel<T> which eventually will be deprecated, it will likely be possible to replace it with (at the time of writing) not yet released SharedFlow api (more on that here.
The way it works is that the Activity and viewmodel shares
a ConflatedBroadcastChannel<MainActivity.Event> which is used to send or offer events from the Activity (or an adapter). The viewmodel reduce the event to a new State which is then emitted. The Activity is collecting on the Flow<State> returned by viewModel.invoke(), and ultimately renders the emitted State.
MainActivityView.kt
object MainActivityView {
sealed class Event {
object OnViewInitialised : Event()
data class OnButtonClicked(val idOfItemClicked: Int) : Event()
}
data class State(
val renderEvent: RenderEvent = RenderEvent.Idle
)
sealed class RenderEvent {
object Idle : RenderEvent()
data class DisplayText(val text: String) : RenderEvent()
}
}
MainActivity.kt
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit eventChannel: ConflatedBroadcastChannel<MainActivityView.Event>
private var isInitialised: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
init()
}
private fun init() {
if (!isInitialised) {
lifecycleScope.launch {
viewModel()
.flowOn(
Dispatchers.IO
).collect(::render)
}
eventChannel
.offer(
MainActivityView.Event.OnViewInitialised
)
isInitialised = true
}
}
private suspend fun render(state: MainActivityView.State): Unit =
when (state.renderEvent) {
MainActivityView.RenderEvent.Idle -> Unit
is MainActivityView.RenderEvent.DisplayText ->
renderDisplayText(text = state.renderEvent.text)
}
private val renderDisplayText(text: String) {
// render text
}
}
MainActivityViewModel.kt
class MainActivityViewModel constructor(
private var state: MainActivityView.State = MainActivityView.State(),
private val eventChannel: ConflatedBroadcastChannel<MainActivityView.Event>,
) {
suspend fun invoke(): Flow<MainActivityView.State> =
eventChannel
.asFlow()
.flatMapLatest { event: MainActivityView.Event ->
reduce(event)
}
private fun reduce(event: MainActivityView.Event): Flow<MainActivityView.State> =
when (event) {
MainActivityView.Event.OnViewInitialised -> onViewInitialisedEvent()
MainActivityView.Event.OnButtonClicked -> onButtonClickedEvent(event.idOfItemClicked)
}
private fun onViewInitialisedEvent(): Flow<MainActivityView.State> = flow
val renderEvent = MainActivityView.RenderEvent.DisplayText(text = "hello world")
state = state.copy(renderEvent = renderEvent)
emit(state)
}
private fun onButtonClickedEvent(idOfItemClicked: Int): Flow<MainActivityView.State> = flow
// do something to handle click
println("item clicked: $idOfItemClicked")
emit(state)
}
}
Similiar questions:
publishsubject-with-kotlin-coroutines-flow
Your MainActivity can look something like this.
#MainActivityScope
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var subscriptions: CompositeDisposable
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit var onViewInitialisedChannel: BroadcastChannel<MainActivityView.Event.OnViewInitialised>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupEvents()
}
override fun onDestroy() {
super.onDestroy()
subscriptions.clear()
}
private fun setupEvents() {
if (subscriptions.size() == 0) {
onViewInitialisedChannel.asFlow()
.buffer()
.flowOn(Dispatchers.IO)
.onEach(::render)
.launchIn(GlobalScope)
onViewInitialisedChannel
.offer(
MainActivityView
.Event
.OnViewInitialised
)
}
}
private fun render(state: MainActivityView.State) {
when (state.renderEvent) {
MainActivityView.RenderEvent.None -> Unit
is MainActivityView.RenderEvent.DisplayText -> {
mainActivityTextField.text = state.renderEvent.text
}
}
}
}
I think what you're looking for is the Flow version of compose and ObservableTransformer and as far as I can tell there isn't one. What you can use instead is the let operator and do something like this:
MainActivity:
yourFlow
.let(viewModel::invoke)
.onEach(::render)
.launchIn(lifecycleScope) // or viewLifecycleOwner.lifecycleScope if you're in a fragment
ViewModel:
operator fun invoke(viewEventFlow: Flow<Event>): Flow<State> = viewEventFlow.flatMapLatest { event ->
when (event) {
Event.OnViewInitialised -> flowOf(onViewInitialisedEvent())
}
}
As far as sharing a flow I would watch these issues:
https://github.com/Kotlin/kotlinx.coroutines/issues/2034
https://github.com/Kotlin/kotlinx.coroutines/issues/2047
Dominic's answer might work for replacing the publish subjects but I think the coroutines team is moving away from BroadcastChannel and intends to deprecate it in the near future.
kotlinx-coroutines-core provides a transform function.
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/transform.html
it isn't quite the same as what we are used to in RxJava but should be usable for achieving the same result.