migrate Lists from RxJava to Kotlin coroutines (#2537)
* migrate Lists from RxJava to Kotlin coroutines * use DROP_OLDEST when creating MutableSharedFlow
This commit is contained in:
parent
51f3794e78
commit
5abb82004a
6 changed files with 168 additions and 150 deletions
|
@ -24,12 +24,11 @@ import android.widget.LinearLayout
|
|||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
|
@ -45,7 +44,7 @@ import com.keylesspalace.tusky.util.show
|
|||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
|
||||
import com.keylesspalace.tusky.viewmodel.State
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -98,10 +97,8 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
binding.accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.accountsSearchRecycler.adapter = searchAdapter
|
||||
|
||||
viewModel.state
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this))
|
||||
.subscribe { state ->
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.state.collect { state ->
|
||||
adapter.submitList(state.accounts.asRightOrNull() ?: listOf())
|
||||
|
||||
when (state.accounts) {
|
||||
|
@ -111,6 +108,7 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
|
||||
setupSearchView(state)
|
||||
}
|
||||
}
|
||||
|
||||
binding.searchView.isSubmitButtonEnabled = true
|
||||
binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
|
|
|
@ -31,14 +31,13 @@ import android.widget.TextView
|
|||
import androidx.activity.viewModels
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import at.connyduck.sparkbutton.helpers.Utils
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.databinding.ActivityListsBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
|
@ -63,7 +62,7 @@ import com.mikepenz.iconics.utils.colorInt
|
|||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -102,19 +101,18 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
|
||||
)
|
||||
|
||||
viewModel.state
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this))
|
||||
.subscribe(this::update)
|
||||
lifecycleScope.launch {
|
||||
viewModel.state.collect(this@ListsActivity::update)
|
||||
}
|
||||
|
||||
viewModel.retryLoading()
|
||||
|
||||
binding.addListButton.setOnClickListener {
|
||||
showlistNameDialog(null)
|
||||
}
|
||||
|
||||
viewModel.events.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this))
|
||||
.subscribe { event ->
|
||||
lifecycleScope.launch {
|
||||
viewModel.events.collect { event ->
|
||||
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
|
||||
when (event) {
|
||||
Event.CREATE_ERROR -> showMessage(R.string.error_create_list)
|
||||
|
@ -122,6 +120,7 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
Event.DELETE_ERROR -> showMessage(R.string.error_delete_list)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showlistNameDialog(list: MastoList?) {
|
||||
|
|
|
@ -25,6 +25,7 @@ import androidx.appcompat.widget.AppCompatEditText
|
|||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -46,9 +47,9 @@ import com.keylesspalace.tusky.network.MastodonApi
|
|||
import com.keylesspalace.tusky.util.onTextChanged
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -253,10 +254,8 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
|
||||
private fun showSelectListDialog() {
|
||||
val adapter = ListSelectionAdapter(this)
|
||||
mastodonApi.getLists()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe(
|
||||
lifecycleScope.launch {
|
||||
mastodonApi.getLists().fold(
|
||||
{ lists ->
|
||||
adapter.addAll(lists)
|
||||
},
|
||||
|
@ -264,6 +263,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
Log.e("TabPreferenceActivity", "failed to load lists", throwable)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.select_list_title)
|
||||
|
|
|
@ -38,7 +38,6 @@ import com.keylesspalace.tusky.entity.SearchResult
|
|||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.entity.StatusContext
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
|
@ -74,9 +73,6 @@ interface MastodonApi {
|
|||
const val PLACEHOLDER_DOMAIN = "dummy.placeholder"
|
||||
}
|
||||
|
||||
@GET("/api/v1/lists")
|
||||
fun getLists(): Single<List<MastoList>>
|
||||
|
||||
@GET("/api/v1/custom_emojis")
|
||||
suspend fun getCustomEmojis(): Result<List<Emoji>>
|
||||
|
||||
|
@ -281,12 +277,12 @@ interface MastodonApi {
|
|||
): Result<Account>
|
||||
|
||||
@GET("api/v1/accounts/search")
|
||||
fun searchAccounts(
|
||||
suspend fun searchAccounts(
|
||||
@Query("q") query: String,
|
||||
@Query("resolve") resolve: Boolean? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("following") following: Boolean? = null
|
||||
): Single<List<TimelineAccount>>
|
||||
): Result<List<TimelineAccount>>
|
||||
|
||||
@GET("api/v1/accounts/search")
|
||||
fun searchAccountsCall(
|
||||
|
@ -462,44 +458,47 @@ interface MastodonApi {
|
|||
@Field("grant_type") grantType: String
|
||||
): Result<AccessToken>
|
||||
|
||||
@GET("/api/v1/lists")
|
||||
suspend fun getLists(): Result<List<MastoList>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/lists")
|
||||
fun createList(
|
||||
suspend fun createList(
|
||||
@Field("title") title: String
|
||||
): Single<MastoList>
|
||||
): Result<MastoList>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("api/v1/lists/{listId}")
|
||||
fun updateList(
|
||||
suspend fun updateList(
|
||||
@Path("listId") listId: String,
|
||||
@Field("title") title: String
|
||||
): Single<MastoList>
|
||||
): Result<MastoList>
|
||||
|
||||
@DELETE("api/v1/lists/{listId}")
|
||||
fun deleteList(
|
||||
suspend fun deleteList(
|
||||
@Path("listId") listId: String
|
||||
): Completable
|
||||
): Result<Unit>
|
||||
|
||||
@GET("api/v1/lists/{listId}/accounts")
|
||||
fun getAccountsInList(
|
||||
suspend fun getAccountsInList(
|
||||
@Path("listId") listId: String,
|
||||
@Query("limit") limit: Int
|
||||
): Single<List<TimelineAccount>>
|
||||
): Result<List<TimelineAccount>>
|
||||
|
||||
@FormUrlEncoded
|
||||
// @DELETE doesn't support fields
|
||||
@HTTP(method = "DELETE", path = "api/v1/lists/{listId}/accounts", hasBody = true)
|
||||
fun deleteAccountFromList(
|
||||
suspend fun deleteAccountFromList(
|
||||
@Path("listId") listId: String,
|
||||
@Field("account_ids[]") accountIds: List<String>
|
||||
): Completable
|
||||
): Result<Unit>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/lists/{listId}/accounts")
|
||||
fun addCountToList(
|
||||
suspend fun addAccountToList(
|
||||
@Path("listId") listId: String,
|
||||
@Field("account_ids[]") accountIds: List<String>
|
||||
): Completable
|
||||
): Result<Unit>
|
||||
|
||||
@GET("/api/v1/conversations")
|
||||
suspend fun getConversations(
|
||||
|
|
|
@ -17,92 +17,103 @@
|
|||
package com.keylesspalace.tusky.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.Either.Left
|
||||
import com.keylesspalace.tusky.util.Either.Right
|
||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||
import com.keylesspalace.tusky.util.withoutFirstWhich
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
data class State(val accounts: Either<Throwable, List<TimelineAccount>>, val searchResult: List<TimelineAccount>?)
|
||||
|
||||
class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) : RxAwareViewModel() {
|
||||
class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() {
|
||||
|
||||
val state: Observable<State> get() = _state
|
||||
private val _state = BehaviorSubject.createDefault(State(Right(listOf()), null))
|
||||
val state: Flow<State> get() = _state
|
||||
private val _state = MutableStateFlow(State(Right(listOf()), null))
|
||||
|
||||
fun load(listId: String) {
|
||||
val state = _state.value!!
|
||||
val state = _state.value
|
||||
if (state.accounts.isLeft() || state.accounts.asRight().isEmpty()) {
|
||||
api.getAccountsInList(listId, 0).subscribe(
|
||||
{ accounts ->
|
||||
updateState { copy(accounts = Right(accounts)) }
|
||||
},
|
||||
{ e ->
|
||||
updateState { copy(accounts = Left(e)) }
|
||||
}
|
||||
).autoDispose()
|
||||
viewModelScope.launch {
|
||||
api.getAccountsInList(listId, 0).fold(
|
||||
{ accounts ->
|
||||
updateState { copy(accounts = Right(accounts)) }
|
||||
},
|
||||
{ e ->
|
||||
updateState { copy(accounts = Left(e)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addAccountToList(listId: String, account: TimelineAccount) {
|
||||
api.addCountToList(listId, listOf(account.id))
|
||||
.subscribe(
|
||||
{
|
||||
updateState {
|
||||
copy(accounts = accounts.map { it + account })
|
||||
viewModelScope.launch {
|
||||
api.addAccountToList(listId, listOf(account.id))
|
||||
.fold(
|
||||
{
|
||||
updateState {
|
||||
copy(accounts = accounts.map { it + account })
|
||||
}
|
||||
},
|
||||
{
|
||||
Log.i(
|
||||
javaClass.simpleName,
|
||||
"Failed to add account to list: ${account.username}"
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
Log.i(
|
||||
javaClass.simpleName,
|
||||
"Failed to add account to the list: ${account.username}"
|
||||
)
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAccountFromList(listId: String, accountId: String) {
|
||||
api.deleteAccountFromList(listId, listOf(accountId))
|
||||
.subscribe(
|
||||
{
|
||||
updateState {
|
||||
copy(
|
||||
accounts = accounts.map { accounts ->
|
||||
accounts.withoutFirstWhich { it.id == accountId }
|
||||
}
|
||||
viewModelScope.launch {
|
||||
api.deleteAccountFromList(listId, listOf(accountId))
|
||||
.fold(
|
||||
{
|
||||
updateState {
|
||||
copy(
|
||||
accounts = accounts.map { accounts ->
|
||||
accounts.withoutFirstWhich { it.id == accountId }
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
Log.i(
|
||||
javaClass.simpleName,
|
||||
"Failed to remove account from list: $accountId"
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
Log.i(javaClass.simpleName, "Failed to remove account from thelist: $accountId")
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String) {
|
||||
when {
|
||||
query.isEmpty() -> updateState { copy(searchResult = null) }
|
||||
query.isBlank() -> updateState { copy(searchResult = listOf()) }
|
||||
else -> api.searchAccounts(query, null, 10, true)
|
||||
.subscribe(
|
||||
{ result ->
|
||||
updateState { copy(searchResult = result) }
|
||||
},
|
||||
{
|
||||
updateState { copy(searchResult = listOf()) }
|
||||
}
|
||||
).autoDispose()
|
||||
else -> viewModelScope.launch {
|
||||
api.searchAccounts(query, null, 10, true)
|
||||
.fold(
|
||||
{ result ->
|
||||
updateState { copy(searchResult = result) }
|
||||
},
|
||||
{
|
||||
updateState { copy(searchResult = listOf()) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun updateState(crossinline fn: State.() -> State) {
|
||||
_state.onNext(fn(_state.value!!))
|
||||
_state.value = fn(_state.value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,19 +16,22 @@
|
|||
|
||||
package com.keylesspalace.tusky.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.keylesspalace.tusky.entity.MastoList
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||
import com.keylesspalace.tusky.util.replacedFirstWhich
|
||||
import com.keylesspalace.tusky.util.withoutFirstWhich
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
import java.net.ConnectException
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ListsViewModel @Inject constructor(private val api: MastodonApi) : RxAwareViewModel() {
|
||||
internal class ListsViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() {
|
||||
enum class LoadingState {
|
||||
INITIAL, LOADING, LOADED, ERROR_NETWORK, ERROR_OTHER
|
||||
}
|
||||
|
@ -39,86 +42,94 @@ internal class ListsViewModel @Inject constructor(private val api: MastodonApi)
|
|||
|
||||
data class State(val lists: List<MastoList>, val loadingState: LoadingState)
|
||||
|
||||
val state: Observable<State> get() = _state
|
||||
val events: Observable<Event> get() = _events
|
||||
private val _state = BehaviorSubject.createDefault(State(listOf(), LoadingState.INITIAL))
|
||||
private val _events = PublishSubject.create<Event>()
|
||||
val state: Flow<State> get() = _state
|
||||
val events: Flow<Event> get() = _events
|
||||
private val _state = MutableStateFlow(State(listOf(), LoadingState.INITIAL))
|
||||
private val _events = MutableSharedFlow<Event>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
fun retryLoading() {
|
||||
loadIfNeeded()
|
||||
}
|
||||
|
||||
private fun loadIfNeeded() {
|
||||
val state = _state.value!!
|
||||
val state = _state.value
|
||||
if (state.loadingState == LoadingState.LOADING || state.lists.isNotEmpty()) return
|
||||
updateState {
|
||||
copy(loadingState = LoadingState.LOADING)
|
||||
}
|
||||
|
||||
api.getLists().subscribe(
|
||||
{ lists ->
|
||||
updateState {
|
||||
copy(
|
||||
lists = lists,
|
||||
loadingState = LoadingState.LOADED
|
||||
)
|
||||
viewModelScope.launch {
|
||||
api.getLists().fold(
|
||||
{ lists ->
|
||||
updateState {
|
||||
copy(
|
||||
lists = lists,
|
||||
loadingState = LoadingState.LOADED
|
||||
)
|
||||
}
|
||||
},
|
||||
{ err ->
|
||||
updateState {
|
||||
copy(
|
||||
loadingState = if (err is IOException || err is ConnectException)
|
||||
LoadingState.ERROR_NETWORK else LoadingState.ERROR_OTHER
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ err ->
|
||||
updateState {
|
||||
copy(
|
||||
loadingState = if (err is IOException || err is ConnectException)
|
||||
LoadingState.ERROR_NETWORK else LoadingState.ERROR_OTHER
|
||||
)
|
||||
}
|
||||
}
|
||||
).autoDispose()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createNewList(listName: String) {
|
||||
api.createList(listName).subscribe(
|
||||
{ list ->
|
||||
updateState {
|
||||
copy(lists = lists + list)
|
||||
viewModelScope.launch {
|
||||
api.createList(listName).fold(
|
||||
{ list ->
|
||||
updateState {
|
||||
copy(lists = lists + list)
|
||||
}
|
||||
},
|
||||
{
|
||||
sendEvent(Event.CREATE_ERROR)
|
||||
}
|
||||
},
|
||||
{
|
||||
sendEvent(Event.CREATE_ERROR)
|
||||
}
|
||||
).autoDispose()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun renameList(listId: String, listName: String) {
|
||||
api.updateList(listId, listName).subscribe(
|
||||
{ list ->
|
||||
updateState {
|
||||
copy(lists = lists.replacedFirstWhich(list) { it.id == listId })
|
||||
viewModelScope.launch {
|
||||
api.updateList(listId, listName).fold(
|
||||
{ list ->
|
||||
updateState {
|
||||
copy(lists = lists.replacedFirstWhich(list) { it.id == listId })
|
||||
}
|
||||
},
|
||||
{
|
||||
sendEvent(Event.RENAME_ERROR)
|
||||
}
|
||||
},
|
||||
{
|
||||
sendEvent(Event.RENAME_ERROR)
|
||||
}
|
||||
).autoDispose()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteList(listId: String) {
|
||||
api.deleteList(listId).subscribe(
|
||||
{
|
||||
updateState {
|
||||
copy(lists = lists.withoutFirstWhich { it.id == listId })
|
||||
viewModelScope.launch {
|
||||
api.deleteList(listId).fold(
|
||||
{
|
||||
updateState {
|
||||
copy(lists = lists.withoutFirstWhich { it.id == listId })
|
||||
}
|
||||
},
|
||||
{
|
||||
sendEvent(Event.DELETE_ERROR)
|
||||
}
|
||||
},
|
||||
{
|
||||
sendEvent(Event.DELETE_ERROR)
|
||||
}
|
||||
).autoDispose()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun updateState(crossinline fn: State.() -> State) {
|
||||
_state.onNext(fn(_state.value!!))
|
||||
_state.value = fn(_state.value)
|
||||
}
|
||||
|
||||
private fun sendEvent(event: Event) {
|
||||
_events.onNext(event)
|
||||
private suspend fun sendEvent(event: Event) {
|
||||
_events.emit(event)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue