Последнее значение 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, но в другом случае будет то же самое.

Надеюсь, поможет.

Другие вопросы по тегам