У меня проблема, что если при первом открытии я открываю INFO_SCREEN, а затем свайпаю, чтобы открыть BonusPointsHistoryScreen, то данные экрана с историей не прогружаются, просто пустой экран, при повторном свайпе всё нормально. package ru.maxi.mobile.feature.bonusPoints.ui.container

import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.viewpager2.widget.ViewPager2
import by.kirich1409.viewbindingdelegate.viewBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import ru.maxi.mobile.R
import ru.maxi.mobile.base.ui.MvvmBaseFragment
import ru.maxi.mobile.common.utils.android.screenArgs
import ru.maxi.mobile.databinding.FragmentBonusPointsContainerBinding
import ru.maxi.mobile.feature.bonusPoints.BonusPointsScreens
import ru.maxi.mobile.feature.bonusPoints.ui.container.model.BonusPointsTabPage
import ru.maxi.mobile.feature.bonusPoints.ui.container.model.BonusPointsViewState
import ru.maxi.mobile.uikit.utils.lifecycleAwareLazy

/**
 * [Fragment] контейнера для баллов
 */
@AndroidEntryPoint
class BonusPointsContainerFragment : MvvmBaseFragment(R.layout.fragment_bonus_points_container) {

    private val viewBinding by viewBinding(FragmentBonusPointsContainerBinding::bind)

    private val screenArgs by screenArgs(BonusPointsContainerArgs)

    private val viewModel by viewModels<BonusPointsContainerViewModel>()

    private val pageAdapter by lifecycleAwareLazy {
        BonusPointsTabAdapter(this)
    }

    override val analyticsName: String? = null

    private var isFirstOpen = true

    private val pageChangeListener = object : ViewPager2.OnPageChangeCallback() {
        override fun onPageSelected(position: Int) {
            if (isFirstOpen) {
                val tabPosition = when (screenArgs.tabId) {
                    BALANCE_TAB -> 0
                    INFO_TAB -> 1
                    else -> throw IllegalArgumentException("invalid tabId ${screenArgs.tabId}")
                }
                viewBinding.tabsButtonContainer.check(screenArgs.tabId)
                viewBinding.tabsContainer.setCurrentItem(tabPosition, false)
                isFirstOpen = false
            } else {
                val tab = when (position) {
                    0 -> BALANCE_TAB
                    1 -> INFO_TAB
                    else -> throw IllegalArgumentException("invalid tabId ${screenArgs.tabId}")
                }
                viewModel.updateCurrentTab(tab, position)
            }
        }
    }

    private val pageList: MutableList<BonusPointsTabPage> = mutableListOf(
        BonusPointsTabPage(BonusPointsScreens.BonusPointsHistoryScreen()),
        BonusPointsTabPage(BonusPointsScreens.InfoScreen()),
    )

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        actionBarManager.setTitle(R.string.bonus_points_screen_title)
        actionBarManager.showBackButton()

        with(viewBinding) {
            tabsContainer.registerOnPageChangeCallback(pageChangeListener)
            tabsContainer.adapter = pageAdapter
            pageAdapter.pages = pageList

            tabsButtonContainer.setOnButtonCheckedListener { buttonId ->
                when (buttonId) {
                    BALANCE_TAB -> {
                        viewModel.updateCurrentTab(BALANCE_TAB, 0)
                    }

                    INFO_TAB -> {
                        viewModel.updateCurrentTab(INFO_TAB, 1)
                    }
                }
            }
        }
        with(viewLifecycleOwner) {
            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.STARTED) {
                    launch { viewModel.viewState.collect(::onViewStateChange) }
                }
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        viewBinding.tabsContainer.unregisterOnPageChangeCallback(pageChangeListener)
    }

    private fun onViewStateChange(state: BonusPointsViewState) {
        with(viewBinding) {
            tabsButtonContainer.check(state.tabButtonId)
            tabsContainer.setCurrentItem(state.currentPosition, true)
        }
    }

    companion object {
        val BALANCE_TAB = R.id.balance_tab
        val INFO_TAB = R.id.info_tab
    }
}
package ru.maxi.mobile.feature.bonusPoints.ui

import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.ConcatAdapter
import by.kirich1409.viewbindingdelegate.viewBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import ru.maxi.mobile.R
import ru.maxi.mobile.app
import ru.maxi.mobile.base.ui.MvvmBaseFragment
import ru.maxi.mobile.base.ui.manager.showSnackbar
import ru.maxi.mobile.common.components.applyThemeColors
import ru.maxi.mobile.common.components.itemlist.ListElement
import ru.maxi.mobile.common.components.itemlist.ListItemAdapter
import ru.maxi.mobile.common.components.itemlist.isHeader
import ru.maxi.mobile.common.utils.android.MarginItemDecoration
import ru.maxi.mobile.core.data.ReloadableData
import ru.maxi.mobile.core.navigation.parentRouter
import ru.maxi.mobile.databinding.FragmentBonusPointsHistoryBinding
import ru.maxi.mobile.feature.bonusPoints.model.BonusPointsHistoryRecord
import ru.maxi.mobile.feature.bonusPoints.model.BonusPointsHistoryRecordType
import ru.maxi.mobile.feature.cardtab.view.entity.BonusPointsHistoryNavigationEvent
import ru.maxi.mobile.presentation.cashback.CashbackScreens
import ru.maxi.mobile.uikit.utils.lifecycleAwareLazy
import ru.maxi.mobile.utils.formatBonusPoints
import ru.maxi.mobile.utils.formatToMonthDate
import ru.maxi.mobile.utils.formatToMonthYearDate
import java.time.LocalDate
import ru.maxi.mobile.uikit.R as UIKitR

/**
 * Экран истории начислений баллов
 */
@AndroidEntryPoint
class BonusPointsHistoryFragment : MvvmBaseFragment(R.layout.fragment_bonus_points_history) {

    private val viewBinding by viewBinding(FragmentBonusPointsHistoryBinding::bind)

    private val viewModel by viewModels<BonusPointsHistoryViewModel>()

    private val balanceAdapter by lifecycleAwareLazy {
        BonusPointsBalanceAdapter()
    }

    private val historyAdapter by lifecycleAwareLazy {
        ListItemAdapter()
    }

    override val analyticsName: String = "_BonusPointsHistory"

    override fun onAttach(context: Context) {
        super.onAttach(context)
        app.cardTabComponent.inject(this)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Log.d("TESTING", "HistoryOnViewCreated")

        with(viewBinding) {
            listView.adapter = ConcatAdapter(balanceAdapter, historyAdapter)
            noDataView.setOnActionButtonClickListener(viewModel::onNoDataButtonClick)
            listView.addItemDecoration(
                MarginItemDecoration { itemPosition ->
                    if (itemPosition < balanceAdapter.itemCount) {
                        return@MarginItemDecoration 0
                    }

                    val position = itemPosition - balanceAdapter.itemCount
                    val currItem = historyAdapter.currentList.getOrNull(position)
                    val prevItem = historyAdapter.currentList.getOrNull(position - 1)

                    return@MarginItemDecoration when {
                        currItem.isHeader -> resources.getDimensionPixelOffset(UIKitR.dimen.std_margin)
                        prevItem.isHeader -> resources.getDimensionPixelOffset(
                            UIKitR.dimen.std_half_margin,
                        )
                        else -> 0
                    }
                },
            )
            noConnectionView.setOnActionButtonClickListener(viewModel::loadData)
            contentContainer.apply {
                applyThemeColors()
                setOnRefreshListener(viewModel::loadData)
            }
        }

        setupNoDataView(viewModel.isCashbackAvailable)
        Log.d("TESTING", "HistoryOnViewCreated2")
        with(viewLifecycleOwner) {
            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.STARTED) {
                    launch { viewModel.viewState.collect(::onViewStateChange) }
                    launch { viewModel.navigationEvent.collect(::onNavigationEvent) }
                    launch { viewModel.messageEvent.collect(notificationManager::showSnackbar) }
                    launch { Log.d("Testing", "coroutines") }
                }
            }
        }
    }

    private fun onViewStateChange(state: ReloadableData<List<BonusPointsBalanceViewItem>>) {
        Log.d("TESTING","OnViewStateChange")
        when (state) {
            is ReloadableData.Error -> with(viewBinding) {
                contentContainer.isRefreshing = false
                contentContainer.isVisible = false
                noConnectionView.isVisible = true
                loaderView.hide()
            }

            is ReloadableData.Loading -> with(viewBinding) {
                contentContainer.isRefreshing = false
                contentContainer.isVisible = false
                noConnectionView.isVisible = false
                loaderView.show()
            }

            is ReloadableData.Refreshing -> with(viewBinding) {
                contentContainer.isRefreshing = true
                contentContainer.isVisible = true
                noConnectionView.isVisible = false
                loaderView.hide()
            }

            is ReloadableData.Success -> with(viewBinding) {
                contentContainer.isRefreshing = false
                contentContainer.isVisible = true
                noConnectionView.isVisible = false
                loaderView.hide()

                val balance = state.value.filterIsInstance<BonusPointsBalanceViewItem.Balance>()
                val history = state.value.filterIsInstance<BonusPointsBalanceViewItem.History>()
                balanceAdapter.balance = balance.first()
                historyAdapter.submitList(history.toListElements())

                if (history.isEmpty()) {
                    noDataView.isVisible = true
                    listView.updateLayoutParams {
                        height = ViewGroup.LayoutParams.WRAP_CONTENT
                    }
                } else {
                    noDataView.isVisible = false
                    listView.updateLayoutParams {
                        height = ViewGroup.LayoutParams.MATCH_PARENT
                    }
                }
            }
        }
    }

    private fun List<BonusPointsBalanceViewItem.History>.toListElements(): List<ListElement> {
        return buildList {
            this@toListElements
                .groupBy { it.record.date }
                .forEach { (date, records) ->
                    add(
                        ListElement.Header(
                            text = if (date.year < LocalDate.now().year) {
                                date.formatToMonthYearDate()
                            } else {
                                date.formatToMonthDate()
                            },
                        ),
                    )
                    addAll(records.map { it.toListItem() })
                }
        }
    }

    private fun setupNoDataView(isCashbackAvailable: Boolean) {
        viewBinding.noDataView.apply {
            setDescriptionText(
                if (isCashbackAvailable) {
                    getString(R.string.bonus_points_empty_history_description_with_cashback)
                } else {
                    getString(R.string.bonus_points_empty_history_description_no_cashback)
                },
            )
            setActionButtonTitle(
                if (isCashbackAvailable) {
                    getString(R.string.bonus_points_empty_history_button_title)
                } else {
                    null
                },
            )
        }
    }

    private fun onNavigationEvent(event: BonusPointsHistoryNavigationEvent) {
        val screen = when (event) {
            BonusPointsHistoryNavigationEvent.OPEN_CASHBACK -> {
                CashbackScreens.ContainerScreen()
            }
        }
        parentRouter.navigateTo(screen)
    }

    private fun BonusPointsBalanceViewItem.History.toListItem() = ListElement.Item(
        title = getString(record.type.titleResId),
        caption = record.formattedCount,
        captionStyleId = record.type.styleResId,
        leftIcon = ListElement.Item.Icon.FromResource(record.type.iconResId),
        leftIconTintColor = ListElement.Item.IconTint.Custom(null),
        hasDivider = false,
    )

    private val BonusPointsHistoryRecordType.titleResId: Int
        get() = when (this) {
            BonusPointsHistoryRecordType.BURNT -> R.string.bonus_points_record_burnt_title
            BonusPointsHistoryRecordType.ACCRUED -> R.string.bonus_points_record_accrued_title
            BonusPointsHistoryRecordType.SPENT -> R.string.bonus_points_record_spent_title
        }

    private val BonusPointsHistoryRecordType.iconResId: Int
        get() = when (this) {
            BonusPointsHistoryRecordType.BURNT -> R.drawable.ic_bonus_points_flame
            else -> R.drawable.ic_bonus_points_history
        }

    private val BonusPointsHistoryRecordType.styleResId: Int
        get() = when (this) {
            BonusPointsHistoryRecordType.BURNT -> R.style.TextAppearance_AppTheme_BonusPoints_Burnt
            BonusPointsHistoryRecordType.ACCRUED -> R.style.TextAppearance_AppTheme_BonusPoints_Accrued
            BonusPointsHistoryRecordType.SPENT -> R.style.TextAppearance_AppTheme_BonusPoints_Spent
        }

    private val BonusPointsHistoryRecord.formattedCount: String
        get() {
            val prefix =
                if (this.type == BonusPointsHistoryRecordType.ACCRUED) "+" else "-"
            val pointsPlural = resources.getQuantityString(R.plurals.points_plurals, this.count)
            return "$prefix${this.count.formatBonusPoints} $pointsPlural"
        }
}
package ru.maxi.mobile.feature.bonusPoints.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import ru.maxi.mobile.common.features.business.FeatureProvider
import ru.maxi.mobile.common.features.core.FeatureType
import ru.maxi.mobile.core.data.ReloadableData
import ru.maxi.mobile.core.data.dataValue
import ru.maxi.mobile.core.data.emitError
import ru.maxi.mobile.core.data.emitLoading
import ru.maxi.mobile.core.data.emitRefreshing
import ru.maxi.mobile.core.data.emitSuccess
import ru.maxi.mobile.core.data.exception.BusinessLogicException
import ru.maxi.mobile.core.data.isRefreshing
import ru.maxi.mobile.core.data.isSuccess
import ru.maxi.mobile.core.data.mapped
import ru.maxi.mobile.feature.bonusPoints.domain.BonusPointsUseCase
import ru.maxi.mobile.feature.bonusPoints.model.BonusPointsData
import ru.maxi.mobile.feature.cardtab.view.entity.BonusPointsHistoryNavigationEvent
import javax.inject.Inject

/**
 * [ViewModel] истории начислений баллов
 */
@HiltViewModel
class BonusPointsHistoryViewModel @Inject constructor(
    private val useCase: BonusPointsUseCase,
    private val featureProvider: FeatureProvider,
) : ViewModel() {

    private val _navigationEvent = Channel<BonusPointsHistoryNavigationEvent>(Channel.BUFFERED)
    private val _messageEvent = Channel<String>(Channel.BUFFERED)
    private val _viewState = MutableStateFlow<ReloadableData<BonusPointsData>>(ReloadableData.Loading())

    /** Состояние доступности кешбэка */
    val isCashbackAvailable: Boolean
        get() = useCase.isCashbackAvailable()

    /** Состояние списка истории начислений баллов */
    val viewState: Flow<ReloadableData<List<BonusPointsBalanceViewItem>>> = _viewState.asStateFlow()
        .map {
            it.mapped { data ->
                buildList {
                    add(
                        BonusPointsBalanceViewItem.Balance(
                            data.balance,
                        ),
                    )
                    addAll(
                        data.records.map { record ->
                            BonusPointsBalanceViewItem.History(record)
                        },
                    )
                }
            }
        }
        .onStart { loadData() }
        .stateIn(viewModelScope, SharingStarted.Lazily, ReloadableData.Loading())

    /** События навигации */
    val navigationEvent = _navigationEvent.receiveAsFlow()

    /** Событие сообщений **/
    val messageEvent = _messageEvent.receiveAsFlow()

    /**
     * Загрузить историю начислений баллов
     */
    fun loadData() {
        when {
            _viewState.isRefreshing -> return
            _viewState.isSuccess -> _viewState.emitRefreshing()
            else -> _viewState.emitLoading()
        }

        viewModelScope.launch {
            try {
                val data = useCase.getBonusPointsHistory()
                _viewState.emitSuccess(data)
            } catch (exception: Exception) {
                _viewState.dataValue?.let {
                    val errorMessage = (exception as? BusinessLogicException)?.message
                        ?: exception.localizedMessage
                    _messageEvent.send(errorMessage)
                    _viewState.emitSuccess(it)
                } ?: _viewState.emitError(exception)
            }
        }
    }

    /**
     * Обработать клик по кнопке при отсутствии данных
     */
    fun onNoDataButtonClick() {
        when {
            featureProvider.isFeatureAvailable(FeatureType.CASHBACK) -> {
                sendNavigationEvent(BonusPointsHistoryNavigationEvent.OPEN_CASHBACK)
            }
        }
    }

    private fun sendNavigationEvent(event: BonusPointsHistoryNavigationEvent) {
        viewModelScope.launch {
            _navigationEvent.send(event)
        }
    }
}