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