Объясни, что здесь отвечает за порядок расположения элементов? package ru.maxi.mobile.feature.maxiPay.ui.about

import android.util.Log
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import ru.maxi.mobile.common.components.databinding.HolderListItemBinding
import ru.maxi.mobile.common.components.databinding.HolderListMessageBinding
import ru.maxi.mobile.common.components.itemlist.ListElement
import ru.maxi.mobile.common.components.itemlist.ListItemViewHolder
import ru.maxi.mobile.common.components.itemlist.ListMessageViewHolder
import ru.maxi.mobile.databinding.ItemMaxiPayAboutHeaderBinding
import ru.maxi.mobile.databinding.ItemMaxiPayAdditionalInfoBinding
import ru.maxi.mobile.databinding.ItemMaxiPayAdvantagesTitleBinding
import ru.maxi.mobile.feature.maxiPay.model.MaxiPayAdditionalInfo
import ru.maxi.mobile.feature.maxiPay.ui.about.model.MaxiPayAboutViewItem
import ru.maxi.mobile.feature.maxiPay.ui.viewItems.MaxiPayAdditionalInfoViewHolder
import ru.maxi.mobile.ui.common.utils.inflate
import ru.maxi.mobile.ui.common.utils.inflateAndGetRoot

/**
 * Адаптер экрана информации о функционале Макси Pay
 */
class MaxiPayAboutAdapter(
    private val onAdditionalInfoItemClick: (item: MaxiPayAdditionalInfo) -> Unit,
) : ListAdapter<MaxiPayAboutViewItem, RecyclerView.ViewHolder>(DiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        Log.d("TESTING", "onCreateViewHolder")
        return when (ViewType.entries[viewType]) {
            ViewType.ADVANTAGE -> {
                val viewBinding = parent.inflate(HolderListItemBinding::inflate)
                ListItemViewHolder(viewBinding)
            }

            ViewType.ADVANTAGES_TITLE -> {
                val view = parent.inflateAndGetRoot(ItemMaxiPayAdvantagesTitleBinding::inflate)
                MaxiPayAdvantagesTitleViewHolder(view)
            }

            ViewType.APPS_DISCLAIMER -> {
                val viewBinding = parent.inflate(HolderListMessageBinding::inflate)
                ListMessageViewHolder(viewBinding)
            }

            ViewType.HEADER -> {
                val view = parent.inflateAndGetRoot(ItemMaxiPayAboutHeaderBinding::inflate)
                MaxiPayAboutHeaderViewHolder(view)
            }

            ViewType.ADDITIONAL_INFO -> {
                val view = parent.inflateAndGetRoot(ItemMaxiPayAdditionalInfoBinding::inflate)
                MaxiPayAdditionalInfoViewHolder(view, onAdditionalInfoItemClick)
            }
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (val item = getItem(position)) {
            is MaxiPayAboutViewItem.Advantage ->
                (holder as? ListItemViewHolder)?.bind(item.toListItemElement())

            is MaxiPayAboutViewItem.AdvantagesTitle ->
                (holder as? MaxiPayAdvantagesTitleViewHolder)?.bind(item)

            is MaxiPayAboutViewItem.AppsDisclaimer ->
                (holder as? ListMessageViewHolder)?.bind(item.toListMessageElement())

            is MaxiPayAboutViewItem.Header ->
                (holder as? MaxiPayAboutHeaderViewHolder)?.bind(item)

            is MaxiPayAboutViewItem.AdditionalInfoType ->
                (holder as? MaxiPayAdditionalInfoViewHolder)?.bind(item.type)
        }
    }

    override fun getItemViewType(position: Int): Int = when (getItem(position)) {
        is MaxiPayAboutViewItem.Advantage -> ViewType.ADVANTAGE
        is MaxiPayAboutViewItem.AdvantagesTitle -> ViewType.ADVANTAGES_TITLE
        is MaxiPayAboutViewItem.AppsDisclaimer -> ViewType.APPS_DISCLAIMER
        is MaxiPayAboutViewItem.Header -> ViewType.HEADER
        is MaxiPayAboutViewItem.AdditionalInfoType -> ViewType.ADDITIONAL_INFO
    }.ordinal

    private companion object {

        enum class ViewType {
            ADVANTAGE,
            ADVANTAGES_TITLE,
            APPS_DISCLAIMER,
            HEADER,
            ADDITIONAL_INFO,
        }
    }
}

private fun MaxiPayAboutViewItem.Advantage.toListItemElement() = ListElement.Item(
    title = title,
    subtitle = description,
    trimTitle = false,
    trimSubtitle = false,
    hasDivider = false,
    leftIcon = ListElement.Item.Icon.FromUrl(imageUrl),
    leftIconTintColor = ListElement.Item.IconTint.Custom(null),
)

private fun MaxiPayAboutViewItem.AppsDisclaimer.toListMessageElement() = ListElement.Message(
    style = ListElement.Message.Style.WARNING,
    subtitle = text,
)

private val MaxiPayAboutViewItem.AdditionalInfoType.type
    get() = when (this) {
        MaxiPayAboutViewItem.AdditionalInfoType.FAQ -> MaxiPayAdditionalInfo.Faq
    }

private class DiffCallback : DiffUtil.ItemCallback<MaxiPayAboutViewItem>() {

    override fun areItemsTheSame(oldItem: MaxiPayAboutViewItem, newItem: MaxiPayAboutViewItem): Boolean {
        return when (oldItem) {
            is MaxiPayAboutViewItem.Advantage -> oldItem == newItem
            is MaxiPayAboutViewItem.AdvantagesTitle -> true
            is MaxiPayAboutViewItem.AppsDisclaimer -> true
            is MaxiPayAboutViewItem.Header -> true
            is MaxiPayAboutViewItem.AdditionalInfoType -> oldItem == newItem
        }
    }

    override fun areContentsTheSame(oldItem: MaxiPayAboutViewItem, newItem: MaxiPayAboutViewItem): Boolean {
        return oldItem == newItem
    }
}
package ru.maxi.mobile.feature.maxiPay.ui.about.model

/**
 * Элементы экрана информации о функционале Макси Pay
 */
sealed interface MaxiPayAboutViewItem {

    /**
     * Шапка экрана
     */
    data class Header(
        val title: String,
        val description: String,
    ) : MaxiPayAboutViewItem

    /**
     * Заголовок списка преимуществ
     */
    data class AdvantagesTitle(
        val text: String,
    ) : MaxiPayAboutViewItem

    /**
     * Преимущество
     */
    data class Advantage(
        val imageUrl: String,
        val title: String,
        val description: String,
    ) : MaxiPayAboutViewItem

    /**
     * Уведомление о банковских приложениях
     */
    data class AppsDisclaimer(
        val text: String,
    ) : MaxiPayAboutViewItem

    /**
     * Возможные типы для отображения элемента дополнительной информации
     */
    enum class AdditionalInfoType : MaxiPayAboutViewItem {
        FAQ,
    }
}
package ru.maxi.mobile.feature.maxiPay.ui.about

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.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import ru.maxi.mobile.core.data.LoadableData
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.emitSuccess
import ru.maxi.mobile.core.data.exception.BusinessLogicException
import ru.maxi.mobile.core.data.exception.BusinessLogicValidException
import ru.maxi.mobile.core.data.isLoading
import ru.maxi.mobile.core.data.isSuccess
import ru.maxi.mobile.core.data.mapped
import ru.maxi.mobile.feature.analytics.model.AnalyticType
import ru.maxi.mobile.feature.analytics.model.actions.AnalyticMaxiPayActions
import ru.maxi.mobile.feature.analytics.model.events.AnalyticsGeneralEvent
import ru.maxi.mobile.feature.analytics.usecase.AnalyticsUseCase
import ru.maxi.mobile.feature.maxiPay.domain.CreateMaxiPaySubscriptionUseCase
import ru.maxi.mobile.feature.maxiPay.domain.GetMaxiPayAboutDataUseCase
import ru.maxi.mobile.feature.maxiPay.domain.GetMaxiPayAvailableMerchantsListUseCase
import ru.maxi.mobile.feature.maxiPay.domain.MaxiPaySseUseCase
import ru.maxi.mobile.feature.maxiPay.model.MaxiPayAboutData
import ru.maxi.mobile.feature.maxiPay.model.MaxiPayAdditionalInfo
import ru.maxi.mobile.feature.maxiPay.model.MaxiPayAdvantageData
import ru.maxi.mobile.feature.maxiPay.model.MaxiPayMerchant
import ru.maxi.mobile.feature.maxiPay.ui.about.model.MaxiPayAboutNavigationEvent
import ru.maxi.mobile.feature.maxiPay.ui.about.model.MaxiPayAboutViewItem
import javax.inject.Inject

/**
 * [ViewModel] экрана информации о функционале Макси Pay
 */
@HiltViewModel
class MaxiPayAboutViewModel @Inject constructor(
    private val getAboutData: GetMaxiPayAboutDataUseCase,
    private val getAvailableMerchantsList: GetMaxiPayAvailableMerchantsListUseCase,
    private val createSubscription: CreateMaxiPaySubscriptionUseCase,
    private val sseUseCase: MaxiPaySseUseCase,
    private val analyticsUseCase: AnalyticsUseCase,
) : ViewModel() {

    private val _navigationEvent = Channel<MaxiPayAboutNavigationEvent>(Channel.BUFFERED)
    private val _messageEvent = Channel<String>(Channel.BUFFERED)

    private val dataState = MutableStateFlow<LoadableData<MaxiPayAboutData>>(LoadableData.Loading())

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

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

    /** Состояние экрана */
    val viewState: Flow<LoadableData<List<MaxiPayAboutViewItem>>> = dataState.asStateFlow()
        .onSubscription { loadData() }
        .stateIn(viewModelScope, SharingStarted.Lazily, LoadableData.Loading())
        .map { it.mapped(MaxiPayAboutData::toViewItems) }

    /**
     * Обновить данные экрана
     */
    fun refreshData() {
        if (dataState.isLoading) return

        dataState.emitLoading(saveValue = false)
        viewModelScope.launch {
            loadData()
        }
    }

    private suspend fun loadData() {
        try {
            val aboutData = getAboutData()
            dataState.emitSuccess(aboutData)
        } catch (exception: Exception) {
            dataState.emitError(exception)
        }
    }

    /**
     * Обработать клик по кнопке привязки счёта
     */
    fun onCreateSubscriptionButtonClick() {
        if (!dataState.isSuccess) return
        val data = dataState.dataValue ?: return

        analyticsUseCase log AnalyticsGeneralEvent.SelectContent(
            analyticType = AnalyticType.MAXI_PAY,
            analyticAction = AnalyticMaxiPayActions.ADD_ACC_BUTTON_CLICK,
        )

        var connectedToSse = false
        viewModelScope.launch {
            try {
                dataState.emitLoading()
                val mainMerchant = getMainMerchant()

                sseUseCase.connectToSse(MaxiPaySseUseCase.WAITING_SCREEN_SSE_CLIENT_TAG)
                connectedToSse = true

                createSubscription(mainMerchant.code)
                _navigationEvent.send(MaxiPayAboutNavigationEvent.OpenWaitingScreen(mainMerchant.name))
            } catch (exception: Exception) {
                val errorMessage = (exception as? BusinessLogicValidException)?.message
                    ?: (exception as? BusinessLogicException)?.message
                    ?: exception.localizedMessage
                _messageEvent.send(errorMessage)

                if (connectedToSse) {
                    sseUseCase.disconnectFromSse(MaxiPaySseUseCase.WAITING_SCREEN_SSE_CLIENT_TAG)
                }
            } finally {
                dataState.emitSuccess(data)
            }
        }
    }

    /**
     * Обработать клик по элементу [item] дополнительной информации
     */
    fun onAdditionalInfoItemClick(item: MaxiPayAdditionalInfo) {
        when (item) {
            is MaxiPayAdditionalInfo.Faq -> _navigationEvent.trySend(
                MaxiPayAboutNavigationEvent.OpenFaqScreen,
            )
        }
    }

    private suspend fun getMainMerchant(): MaxiPayMerchant {
        return getAvailableMerchantsList().firstOrNull()?.let { merchants ->
            merchants.firstOrNull { it.merchant.isMain }?.merchant
                ?: merchants.firstOrNull()?.merchant
        } ?: throw BusinessLogicException("Не найдено ни одного мерчанта")
    }
}

private fun MaxiPayAboutData.toViewItems(): List<MaxiPayAboutViewItem> = buildList {
    add(headerViewItem)
    advantagesList.map(MaxiPayAdvantageData::toViewItem)
        .takeIf(List<Any>::isNotEmpty)
        ?.let { advantagesItems ->
            add(advantagesTitleViewItem)
            addAll(advantagesItems)
        }
    add(appsDisclaimerViewItem)
    if (haveQuestions) {
        add(additionalInfoType)
    }
}

private val MaxiPayAboutData.headerViewItem: MaxiPayAboutViewItem.Header
    get() = MaxiPayAboutViewItem.Header(
        title = aboutTitle,
        description = aboutDescription,
    )

private fun MaxiPayAdvantageData.toViewItem() = MaxiPayAboutViewItem.Advantage(
    imageUrl = imageUrl,
    title = title,
    description = description,
)

private val MaxiPayAboutData.advantagesTitleViewItem: MaxiPayAboutViewItem.AdvantagesTitle
    get() = MaxiPayAboutViewItem.AdvantagesTitle(
        text = advantagesTitle,
    )

private val MaxiPayAboutData.appsDisclaimerViewItem: MaxiPayAboutViewItem.AppsDisclaimer
    get() = MaxiPayAboutViewItem.AppsDisclaimer(
        text = appsDisclaimer,
    )

private val MaxiPayAboutData.additionalInfoType: MaxiPayAboutViewItem.AdditionalInfoType
    get() = MaxiPayAboutViewItem.AdditionalInfoType.FAQ
package ru.maxi.mobile.feature.maxiPay.ui.about

import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
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.base.ui.manager.showSnackbar
import ru.maxi.mobile.common.utils.android.MarginItemDecoration
import ru.maxi.mobile.core.data.LoadableData
import ru.maxi.mobile.core.navigation.parentRouter
import ru.maxi.mobile.databinding.FragmentMaxiPayAboutBinding
import ru.maxi.mobile.feature.maxiPay.MaxiPayScreens
import ru.maxi.mobile.feature.maxiPay.ui.about.model.MaxiPayAboutNavigationEvent
import ru.maxi.mobile.feature.maxiPay.ui.about.model.MaxiPayAboutViewItem
import ru.maxi.mobile.uikit.utils.lifecycleAwareLazy
import ru.maxi.mobile.uikit.R as UIKitR

/**
 * [Fragment] экрана информации о функционале Макси Pay
 */
@AndroidEntryPoint
class MaxiPayAboutFragment : MvvmBaseFragment(R.layout.fragment_maxi_pay_about) {

    private val viewModel by viewModels<MaxiPayAboutViewModel>()

    private val viewBinding by viewBinding(FragmentMaxiPayAboutBinding::bind)

    private val adapter by lifecycleAwareLazy {
        MaxiPayAboutAdapter(onAdditionalInfoItemClick = viewModel::onAdditionalInfoItemClick)
    }

    override val analyticsName: String = "_MaxiPayAboutScreen"

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        with(viewBinding) {
            itemsView.adapter = adapter
            itemsView.addItemDecoration(
                MarginItemDecoration { itemPosition ->
                    val currItem = adapter.currentList.getOrNull(itemPosition)
                    val prevItem = adapter.currentList.getOrNull(itemPosition - 1)
                    val margin = when {
                        currItem is MaxiPayAboutViewItem.AppsDisclaimer ->
                            resources.getDimension(UIKitR.dimen.std_margin)

                        currItem is MaxiPayAboutViewItem.AdditionalInfoType &&
                            prevItem !is MaxiPayAboutViewItem.AdditionalInfoType ->
                            resources.getDimension(UIKitR.dimen.std_margin)

                        else -> 0
                    }
                    return@MarginItemDecoration margin.toInt()
                },
            )

            createSubscriptionButton.setOnClickListener {
                viewModel.onCreateSubscriptionButtonClick()
            }
            noConnectionView.setOnActionButtonClickListener(viewModel::refreshData)
        }

        with(viewLifecycleOwner) {
            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.STARTED) {
                    launch { viewModel.viewState.collect(::onViewStateChange) }
                    launch { viewModel.navigationEvent.collect(::onNavigationEvent) }
                    launch { viewModel.messageEvent.collect(notificationManager::showSnackbar) }
                }
            }
        }
    }

    private fun onViewStateChange(state: LoadableData<List<MaxiPayAboutViewItem>>) {
        state.value?.let(adapter::submitList)

        when (state) {
            is LoadableData.Error -> with(viewBinding) {
                loaderView.hide()
                blockingLoaderManager.isShowing = false
                contentContainer.isVisible = false
                noConnectionView.isVisible = true
            }

            is LoadableData.Loading -> with(viewBinding) {
                if (state.hasValue) {
                    loaderView.hide()
                    blockingLoaderManager.isShowing = true
                } else {
                    loaderView.show()
                    blockingLoaderManager.isShowing = false
                }
                contentContainer.isVisible = state.hasValue
                noConnectionView.isVisible = false
            }

            is LoadableData.Success -> with(viewBinding) {
                loaderView.hide()
                blockingLoaderManager.isShowing = false
                contentContainer.isVisible = true
                noConnectionView.isVisible = false
            }
        }
    }

    private fun onNavigationEvent(event: MaxiPayAboutNavigationEvent) {
        when (event) {
            is MaxiPayAboutNavigationEvent.OpenWaitingScreen ->
                parentRouter.navigateTo(MaxiPayScreens.WaitingScreen(event.merchantName))

            is MaxiPayAboutNavigationEvent.OpenFaqScreen ->
                parentRouter.navigateTo(MaxiPayScreens.FaqScreen())
        }
    }
}