Последнее значение StateFlow снова собирается в пользовательском интерфейсе
Итак, в последнее время я работал с API StateFlow, SharedFlow и Channels, но я борюсь с одним распространенным вариантом использования, пытаясь перенести свой код из LiveData в StateFlow на уровне представления.
Проблема, с которой я сталкиваюсь, заключается в том, что я отправляю свои данные и собираю их в viewModel, поэтому я могу установить значение mutableStateFlow, когда он, наконец, добирается до фрагмента, он показывает некоторые информативные сообщения с помощью Toast, чтобы пользователь знал, есть ли ошибка случилось или все прошло нормально. Затем есть кнопка, которая переходит к другому фрагменту, но если я вернусь к предыдущему экрану, на котором уже есть результат неудавшегося намерения, снова отобразится тост. И это именно то, что я пытаюсь понять. Если я уже собрал результат и показал сообщение пользователю, я не хочу продолжать это делать. Если я перейду на другой экран и вернусь (это также происходит, когда приложение возвращается из фона, оно снова собирает последнее значение). Этой проблемы не было с LiveData, где я сделал то же самое,выставить поток из репозитория и собрать через LiveData в ViewModel.
Код:
class SignInViewModel @Inject constructor(
private val doSignIn: SigninUseCase
) : ViewModel(){
private val _userResult = MutableStateFlow<Result<String>?>(null)
val userResult: StateFlow<Result<String>?> = _userResult.stateIn(viewModelScope, SharingStarted.Lazily, null) //Lazily since it's just one shot operation
fun authenticate(email: String, password: String) {
viewModelScope.launch {
doSignIn(LoginParams(email, password)).collect { result ->
Timber.e("I just received this $result in viewmodel")
_userResult.value = result
}
}
}
}
Затем в моем фрагменте:
override fun onViewCreated(...){
super.onViewCreated(...)
launchAndRepeatWithViewLifecycle {
viewModel.userResult.collect { result ->
when(result) {
is Result.Success -> {
Timber.e("user with code:${result.data} logged in")
shouldShowLoading(false)
findNavController().navigate(SignInFragmentDirections.toHome())
}
is Result.Loading -> {
shouldShowLoading(true)
}
is Result.Error -> {
Timber.e("error: ${result.exception}")
if(result.exception is Failure.ApiFailure.BadRequestError){
Timber.e(result.exception.message)
shortToast("credentials don't match")
} else {
shortToast(result.exception.toString())
}
shouldShowLoading(false)
}
}
}
}
Функция расширения launchAndRepeatWithViewLifecycle:
inline fun Fragment.launchAndRepeatWithViewLifecycle(
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
crossinline block: suspend CoroutineScope.() -> Unit
) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(minActiveState) {
block()
}
}
}
Есть мысли о том, почему это происходит и как решить эту проблему с помощью StateFlow? Я также пробовал использовать SharedFlow с replay = 0 и каналы с receiveAsFlow(), но тогда возникли другие проблемы.
5 ответов
Вы можете создать функцию расширения следующим образом:
fun <T> MutableStateFlow<T?>.set(value: T, default: T? = null) {
this.value = value
this.value = default
}
Он устанавливает желаемое значение по умолчанию после того, как новое значение было выдано. В моем случае я использую null как желаемое значение.
Внутри ViewModel вместо прямой установки значения вы можете использовать
set()
функция расширения.
fun signIn() = authRepository.signIn(phoneNumber.value).onEach {
_signInState.set(it)
}.launchIn(viewModelScope)
Похоже, вы ищете SingleLiveEvent с потоком Kotlin.
` класс MainViewModel : ViewModel() {
sealed class Event {
data class ShowSnackBar(val text: String): Event()
data class ShowToast(val text: String): Event()
}
private val eventChannel = Channel<Event>(Channel.BUFFERED)
val eventsFlow = eventChannel.receiveAsFlow()
init {
viewModelScope.launch {
eventChannel.send(Event.ShowSnackBar("Sample"))
eventChannel.send(Event.ShowToast("Toast"))
}
}
}
класс MainFragment : Фрагмент () {
companion object {
fun newInstance() = MainFragment()
}
private val viewModel by viewModels<MainViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.main_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Note that I've chosen to observe in the tighter view lifecycle here.
// This will potentially recreate an observer and cancel it as the
// fragment goes from onViewCreated through to onDestroyView and possibly
// back to onViewCreated. You may wish to use the "main" lifecycle owner
// instead. If that is the case you'll need to observe in onCreate with the
// correct lifecycle.
viewModel.eventsFlow
.onEach {
when (it) {
is MainViewModel.Event.ShowSnackBar -> {}
is MainViewModel.Event.ShowToast -> {}
}
}
.flowWithLifecycle(lifecycle = viewLifecycleOwner.lifecycle, minActiveState = Lifecycle.State.STARTED)
.onEach {
// Do things
}
.launchIn(viewLifecycleOwner.lifecycleScope)
}
}
`
Кредит: Майкл Фергюсон написал отличную статью с обновленным расширением библиотеки. Порекомендовал бы вам пройти его. Я скопировал выдержку из него.
https://proandroiddev.com/android-singleliveevent-redux-with-kotlin-flow-b755c70bb055
Вы можете установить значение вашего состояния в Idle(), что означает пустое состояние
lifecycleScope.launch {
viewModel.signInState.collect {
when (it) {
is ViewState.Success -> {
stopLoading()
viewModel._signInState.value = ViewState.Idle()
// do your tasks here
}
is ViewState.Error<*> -> requireActivity().onBackPressedDispatcher.onBackPressed()
is ViewState.BusinessException<*> -> requireActivity().onBackPressedDispatcher.onBackPressed()
is ViewState.Idle<*> -> stopLoading()
is ViewState.Loading<*> -> startLoading()
}
}
}
Простой ответ:SharedFlow
с параметромreplay = 0
.
Пример кода
val sharedFlow = MutableSharedFlow<Result<PromotionItem>>(replay = 0)
.
К вашему сведению: Но в случае, когда значение излучается, а подписчика нет, то после подписки значение НЕ будет получено .
В моем случае я использую событие клика, и после обработки данных нужно открыть новое действие. При возврате он не должен снова запускать метод сбора. Поэтому я использовал SharedFlow вместо StateFlow, проблема решена!
Вот поздний ответ. Недавно я начал использовать сопрограмму и поток для своего проекта и столкнулся с той же проблемой. Ниже приведен код, который я использую для решения проблемы:
Фрагмент кода
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUi()
subscribeUi()
}
private fun subscribeUi() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.pagingData.collectLatest { pagingData ->
categoryListAdapter.submitData(pagingData.map {
CategoryUiItem.DataItem(
it.itemNo,
it.itemName,
it.brandName,
it.imageUrl,
it.heartCount,
it.reviewCount,
it.reviewAveragePoint,
it.saleInfo.consumerPrice,
it.saleInfo.sellPrice,
it.saleInfo.saleRate,
it.saleInfo.couponSaleRate,
it.saleInfo.isCoupon,
it.saleInfo.totalSellPrice,
it.saleInfo.totalSaleRate,
it.isLiked // isLiked
)
})
}
}
}
}
Модель представления
private val _pagingData: MutableStateFlow<PagingData<CategoryItemDomain>> =
MutableStateFlow(PagingData.empty())
val pagingData = _pagingData.shareIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
replay = 0
)
init {
getCategoryList()
}
private fun getCategoryList() {
viewModelScope.launch(Dispatchers.IO) {
getCategoryListUseCase(category)
.cachedIn(viewModelScope)
.collectLatest {
_pagingData.value = it
}
}
}
Сначала я использовал StateFlow, а затем изменил его на SharedFlow, используя ShareIn. Я использую paging3, но в другом случае будет то же самое.
Надеюсь, поможет.