Простая архитектура с использованием MVVM и делегатов в Android. Оптимальное решение для малых проектов

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В этой статье мы рассмотрим, как создать простую архитектуру Android, используя паттерн MVVM (Model-View-ViewModel) и делегаты для эффективного управления состоянием.

Как работать с UiStateDelegate можно почитать в предыдущей статье.

Спойлер:

Важно отметить, что рассматриваемый подход и его реализация рекомендуются для применения в небольших проектах, где акцент делается на оперативной разработке. В контексте более крупных и сложных проектов, возможно, потребуется более глубокое погружение в архитектурные детали и оптимизацию, однако в небольших масштабах данное решение представляет собой эффективный вариант.

Когда вы разрабатываете Android-приложение, управление состоянием играет важную роль. Оно позволяет вам эффективно обрабатывать данные и события в вашем приложении. Однако, вместо того чтобы заполнять вашу ViewModel большим количеством кода, вы можете воспользоваться простой архитектурой, основанной на MVVM и делегатах.

Использование делегатов с простой реализацией может значительно улучшить процесс интеграции новых сотрудников в проект. Вот как это может быть полезно:

  1. Быстрая адаптация новых разработчиков: Простая реализация делегатов делает код более понятным и доступным для новых разработчиков. Они могут быстрее освоиться с проектом и начать вносить вклад.

  2. Меньше шансов на ошибки: Простая реализация методов делегатов уменьшает вероятность появления ошибок в коде, так как она делает код более надежным и понятным.

  3. Увеличение производительности команды: Благодаря простой реализации методов, разработчики могут более эффективно работать с делегатами, что приводит к увеличению производительности команды и более быстрой разработке новых функций.

  4. Сокращение времени на документацию: Простой и интуитивно понятный код делегатов может уменьшить необходимость в объемной документации, так как большая часть логики уже является самодокументируемой.

  5. Легкость тестирования: Простые реализации делегатов упрощают процесс тестирования, что позволяет быстрее выявлять и устранять ошибки.

  6. Более четкое разделение ответственности: Применение делегатов с простой реализацией способствует четкому разделению ответственности в коде, что делает его более структурированным и понятным.

  7. Поддерживаемость: Простые реализации делегатов делают код более поддерживаемым. Новые разработчики могут быстро разобраться в существующем коде и вносить необходимые изменения.

  8. Повторное использование кода на разных экранах: Повторное использование кода с делегатами помогает уменьшить объем работы

Пример реализации

MVVM: Основа нашей архитектуры

MVVM — это паттерн архитектуры, который разделяет приложение на три ключевых компонента: Model, View и ViewModel.

  • Model: Это ваш бизнес-логика и данные. Здесь вы обрабатываете данные, выполняете операции и взаимодействуете с источниками данных.

  • View: Это пользовательский интерфейс (UI) вашего приложения. View ответственен за отображение данных и реагирование на взаимодействие пользователя.

  • ViewModel: ViewModel является посредником между Model и View. Он содержит логику, связанную с UI, и управляет состоянием приложения. ViewModel предоставляет данные, которые View отображает, и обрабатывает действия пользователя.

Delegates: Инструмент для управления состоянием

Для управления состоянием внутри нашей архитектуры мы будем использовать делегаты. Давайте определим несколько ключевых делегатов, которые помогут нам сделать это более эффективно.

UiStateDelegate

UiStateDelegate
/**
 * UiState - must be Data class, immutable
 */
interface UiStateDelegate<UiState, Event> {

    /**
     * Declarative description of the UI based on the current state.
     */
    val uiStateFlow: StateFlow<UiState>

    val singleEvents: Flow<Event>

    /**
     * State is read-only
     * The only way to change the state is to emit[reduce] an action,
     * an object describing what happened.
     */
    val UiStateDelegate<UiState, Event>.uiState: UiState

    /**
     * Transforms UI state using the specified transformation.
     *
     * @param transform  - function to transform UI state.
     */
    suspend fun UiStateDelegate<UiState, Event>.updateUiState(
        transform: (uiState: UiState) -> UiState,
    )

    /**
     * Changing the state without blocking the coroutine.
     */
    fun UiStateDelegate<UiState, Event>.asyncUpdateUiState(
        coroutineScope: CoroutineScope,
        transform: (state: UiState) -> UiState,
    ): Job

    suspend fun UiStateDelegate<UiState, Event>.sendEvent(event: Event)
}

UiStateDelegate - это интерфейс, который позволяет вам управлять UI-состоянием в вашей ViewModel. С его помощью вы можете легко обновлять UI-состояние и отправлять события, связанные с UI. Ваш код позволяет вам декларативно описывать состояние UI и асинхронно обновлять его.

Методы updateUiState и asyncUpdateUiState в интерфейсе UiStateDelegate используются для обновления состояния UI (UiState) в приложении, но они выполняются асинхронно, чтобы не блокировать основной поток выполнения.

updateUiState(transform: (uiState: UiState) -> UiState):

Этот метод принимает функцию transform, которая берет текущее состояние UI (UiState) и возвращает новое состояние после применения какой-либо логики или преобразования. Метод updateUiState блокирует доступ к изменению состояния UI, чтобы избежать гонок за ресурсы. После обновления состояния, он уведомляет всех наблюдателей о новом состоянии, что может привести к обновлению пользовательского интерфейса.

Пример использования:

in ViewModel:
updateUiState { currentState ->
    // update state
    currentState.copy()
}

asyncUpdateUiState(coroutineScope: CoroutineScope, transform: (state: UiState) -> UiState): Job:

Этот метод также обновляет состояние UI, но он выполняется асинхронно в рамках указанной области coroutine (coroutineScope). Он принимает функцию transform, которая выполняет аналогичное преобразование состояния, как и в методе updateUiState. Возвращаемый объект Job представляет выполнение асинхронной операции обновления состояния.

Пример использования:

in ViewModel:
asyncUpdateUiState(viewModelScope) { currentState ->
    // async update state
    currentState.copy()
}

Использование этих методов позволяет эффективно обновлять состояние пользовательского интерфейса в приложении, обеспечивая реакцию на действия пользователя или изменения внутренних данных, при этом не блокируя главный поток выполнения и обеспечивая отзывчивость вашего приложения.

val singleEvents: Flow<Event> в интерфейсе UiStateDelegate используется для передачи events, которые могут возникать в ходе взаимодействия пользователя с интерфейсом. Это может включать в себя события, такие как навигация, успешное завершение какой то операции, показ ошибки и т. д.

InternalStateDelegate

InternalStateDelegate
interface InternalStateDelegate<State> {

    /**
     * Get the internal state as data flow.
     */
    val InternalStateDelegate<State>.internalStateFlow: Flow<State>

    /**
     * Get the current internal state.
     */
    val InternalStateDelegate<State>.internalState: State

    /**
     * Transforms internal state using the specified transformation.
     *
     * @param transform  - function to transform internal state.
     */
    suspend fun InternalStateDelegate<State>.updateInternalState(
        transform: (state: State) -> State,
    )

    /**
     * Changing the state without blocking the coroutine.
     */
    fun InternalStateDelegate<State>.asyncUpdateInternalState(
        coroutineScope: CoroutineScope, transform: (state: State) -> State
    ): Job
}

InternalStateDelegate - это еще один интерфейс, который предоставляет возможность управлять внутренним состоянием вашей ViewModel. Это полезно, когда вы хотите хранить и обрабатывать данные, которые не должны напрямую отображаться на UI, но влияют на него.

Метод updateInternalState в интерфейсе InternalStateDelegate используется для обновления внутреннего состояния (internalState) в приложении. Этот метод позволяет асинхронно изменять состояние, что может быть полезным, например, при выполнении асинхронных операций или обновлении данных внутри вашей ViewModel.

Метод updateInternalState выполняет следующие действия:

  1. Он блокирует доступ к изменению внутреннего состояния с использованием мьютекса (или иного механизма синхронизации), чтобы избежать гонок за ресурсы и обеспечить потокобезопасность.

  2. Затем он вызывает функцию transform, передавая текущее внутреннее состояние в качестве аргумента.

  3. Функция transform выполняет необходимую логику для обновления внутреннего состояния. Это может включать в себя добавление, удаление, изменение данных и т. д.

  4. После успешного обновления состояния метод уведомляет все наблюдатели о новом состоянии, что может привести к дальнейшим действиям или обновлению пользовательского интерфейса, если внутреннее состояние влияет на отображение данных в UI.

CombinedStateDelegate

CombinedStateDelegate
interface CombinedStateDelegate<UiState, State, Event> :
    UiStateDelegate<UiState, Event>,
    InternalStateDelegate<State> {

    /**
     * Transforms UI state using the specified transformation.
     *
     * @param transform  - function to transform UI state.
     */
    suspend fun CombinedStateDelegate<UiState, State, Event>.updateUiState(
        transform: (uiState: UiState, state: State) -> UiState
    )

    fun CombinedStateDelegate<UiState, State, Event>.collectUpdateUiState(
        coroutineScope: CoroutineScope,
        transform: (state: State, uiState: UiState) -> UiState,
    ): Job

    fun <T> CombinedStateDelegate<UiState, State, Event>.combineCollectUpdateUiState(
        coroutineScope: CoroutineScope,
        flow: Flow<T>,
        transform: suspend (state: State, uiState: UiState, value: T) -> UiState,
    ): Job

    fun <T1, T2> CombinedStateDelegate<UiState, State, Event>.combineCollectUpdateUiState(
        coroutineScope: CoroutineScope,
        flow1: Flow<T1>,
        flow2: Flow<T2>,
        transform: suspend (state: State, uiState: UiState, value1: T1, value2: T2) -> UiState,
    ): Job

    fun <T1, T2, T3> CombinedStateDelegate<UiState, State, Event>.combineCollectUpdateUiState(
        coroutineScope: CoroutineScope,
        flow1: Flow<T1>,
        flow2: Flow<T2>,
        flow3: Flow<T3>,
        transform: suspend (state: State, uiState: UiState, value1: T1, value2: T2, value3: T3) -> UiState,
    ): Job

    fun <T1, T2, T3, T4> CombinedStateDelegate<UiState, State, Event>.combineCollectUpdateUiState(
        coroutineScope: CoroutineScope,
        flow1: Flow<T1>,
        flow2: Flow<T2>,
        flow3: Flow<T3>,
        flow4: Flow<T4>,
        transform: suspend (state: State, uiState: UiState, value1: T1, value2: T2, value3: T3, value4: T4) -> UiState,
    ): Job
}

CombinedStateDelegate объединяет UiStateDelegate и InternalStateDelegate, позволяя связать логику этих двух состояний. Это особенно полезно, когда вам нужно обновить UI-состояние, зависящее от внутреннего состояния.

Реализация делегатов

Методы updateUiState и updateInternalState в соответствующих делегатах принимают функцию transform:

Реализация
transform: (state: State) -> State


override suspend fun InternalStateDelegate<State>.updateInternalState(
        transform: (state: State) -> State,
    ) {
        mutexState.withLock { internalMutableState.update(transform) }
    }


override suspend fun UiStateDelegate<UiState, Event>.updateUiState(
        transform: (uiState: UiState) -> UiState,
    ) {
        mutexState.withLock { uiMutableStateFlow.emit(transform(uiState))}
    }

Функция принимает текущее состояние как параметр и возвращает новое состояние.

Внутри функции используется Mutex что бы избежать гонки за ресурсы.

Такие простые реализации позволяют асинхронно обновлять состояния, и они могут быть дополнены более сложной логикой в зависимости от требований вашего приложения.

Пример использования CombinedStateDelegate

CombinedStateDelegate c Viewmodel
class HomeViewModel(
    private val dashboardRepository: DashboardRepository,
) : ViewModel(),
    CombinedStateDelegate<UiState, State, Event> by CombinedStateDelegateImpl(
    initialState = State(),
    initialUiState = UiState(),
) {

В этой части кода мы видим, что HomeViewModel расширяет ViewModel и делегирует свои обязанности по управлению состоянием и событиями делегату CombinedStateDelegate, который представляет собой сочетание UiStateDelegate и InternalStateDelegate. Это делает код более модульным и читаемым, так как логика управления состоянием и событиями вынесена в отдельный компонент.

data class UiState(
    val isLoading: Boolean = false,
    val items: List<DashboardUi> = emptyList(),
    val filter: String = "",
)

Здесь определена структура UiState, которая представляет состояние пользовательского интерфейса экрана. Этот объект содержит информацию о том, загружаются ли данные, какие элементы отображаются на экране, и текущий фильтр.

data class State(
    val fullItems: List<DashboardUi> = emptyList(),
)

State представляет внутреннее состояние, в данном случае - список элементов, полученных из репозитория.

sealed interface Event {
    object StartForgotPasswordFeature : Event
}

Здесь определен интерфейс Event, который может быть использован для обработки событий, таких как переход на другой экран.

init {
    collectUpdateUiState(viewModelScope) { state, uiState ->
        val newItems = if (uiState.filter.isBlank()) {
            state.fullItems
        } else {
            state.fullItems.filter { item -> item.title.contains(uiState.filter) }
        }
        uiState.copy(items = newItems)
    }

В этой части кода происходит инициализация HomeViewModel. Мы используем collectUpdateUiState, чтобы отслеживать изменения в состоянии пользователя (UiState). В данном случае, мы фильтруем элементы в соответствии с значением filter и обновляем items в UiState.

viewModelScope.launch(CoroutineExceptionHandler { _, throwable ->
    throwable.printStackTrace()
}) {
    updateUiState { uiState, _ -> uiState.copy(isLoading = true) }
    val items = runCatching { dashboardRepository.getHomeItems() }
        .getOrDefault(emptyList())

    val uiItems = items.map { item -> item.toDashboardUi() }

    updateInternalState { state -> state.copy(fullItems = uiItems) }
}.invokeOnCompletion {
    asyncUpdateUiState(viewModelScope) { uiState -> uiState.copy(isLoading = false) }
}

В этой части кода мы видим асинхронную работу с состоянием. Сначала мы обновляем UiState, устанавливая isLoading в true, чтобы указать о начале загрузки. Затем мы получаем данные из репозитория, преобразуем их в uiItems и обновляем внутреннее состояние. Наконец, в invokeOnCompletion, после завершения операции, мы асинхронно обновляем UiState, устанавливая isLoading в false.

Код всей ViewModel:
class HomeViewModel(
    private val dashboardRepository: DashboardRepository,
) : ViewModel(),
    CombinedStateDelegate<UiState, State, Event> by CombinedStateDelegateImpl(
    initialState = State(),
    initialUiState = UiState(),
) {

    data class UiState(
        val isLoading: Boolean = false,
        val items: List<DashboardUi> = emptyList(),

        val filter: String = "",
    )

    data class State(
        val fullItems: List<DashboardUi> = emptyList(),
    )

    sealed interface Event {
        object StartForgotPasswordFeature : Event
    }

    init {
        collectUpdateUiState(viewModelScope) { state, uiState ->
            val newItems = if (uiState.filter.isBlank()) {
                state.fullItems
            } else {
                state.fullItems.filter { item -> item.title.contains(uiState.filter) }
            }
            uiState.copy(items = newItems)
        }

        viewModelScope.launch(CoroutineExceptionHandler { _, throwable ->
            throwable.printStackTrace()
        }) {
            updateUiState { uiState, _ -> uiState.copy(isLoading = true) }
            val items = runCatching { dashboardRepository.getHomeItems() }
                .getOrDefault(emptyList())

            val uiItems = items.map { item -> item.toDashboardUi() }

            updateInternalState { state -> state.copy(fullItems = uiItems) }
        }.invokeOnCompletion {
            asyncUpdateUiState(viewModelScope) { uiState -> uiState.copy(isLoading = false) }
        }
    }
}

Этот код демонстрирует, как использовать делегат CombinedStateDelegate для управления состоянием и событиями на экране. Он делает код более модульным и понятным, упрощая управление состоянием экрана в архитектуре MVVM.

Повторное использование Delegates на разных экранах

Один из ключевых аспектов использования делегатов — это их возможность легко переиспользовать на разных экранах и в разных частях вашего приложения. В этом разделе мы рассмотрим пример, где делегаты играют важную роль в создании модульных и переиспользуемых компонентов.

Пример: ToolbarDelegate

Представьте себе ситуацию, когда в вашем приложении есть экраны, каждый из которых имеет свой собственный toolbar. Эти верхние бары могут иметь разное содержание, такое как заголовок, уровень прогресса и метку уровня. Для управления этими верхними барами можно было бы создать общий компонент, который будет отвечать за отображение и обновление данных в toolbar.

Пример
interface ToolbarDelegate {
    val toolbarUiState: StateFlow<ToolbarUiState>
}

data class ToolbarUiState(
    val title: String = "",
    @FloatRange(from = 0.0, to = 100.0)
    val levelProgress: Float = 0f,
    val levelLabel: String = "",
)

class ToolbarDelegateImpl @Inject constructor(
    coroutineScope: CoroutineScope,
    userRepository: UserRepository,
) : ToolbarDelegate,
    UiStateDelegate<ToolbarUiState, Unit> by UiStateDelegateImpl(ToolbarUiState()) {

    override val toolbarUiState: StateFlow<ToolbarUiState>
        get() = uiStateFlow

    init {
        coroutineScope.launch {
            delay(500L)
            updateUiState { state ->
                state.copy(
                    title = "Title",
                    levelLabel = "Level:",
                )
            }
        }

        userRepository.getUserLevelFlow()
            .flowOn(Dispatchers.IO)
            .onEach { levelProgress ->
                updateUiState { state -> state.copy(levelProgress = levelProgress) }
            }
            .catch { throwable -> throwable.printStackTrace() }
            .launchIn(coroutineScope)
    }
}

В этом коде определен интерфейс ToolbarDelegate, который описывает, что должен предоставлять компонент для управления верхним баром. ToolbarDelegate включает в себя StateFlow, предоставляющий информацию о текущем состоянии TopBar.

ToolbarDelegateImpl реализует ToolbarDelegate и использует делегат UiStateDelegate, чтобы управлять состоянием TopBar. Он обновляет заголовок, уровень прогресса и метку уровня, а также слушает изменения уровня пользователя и обновляет соответствующие данные в TopBar.

Теперь, когда у нас есть переиспользуемый компонент ToolbarDelegate, мы можем его использовать на разных экранах. Вот как это может выглядеть:

class UserViewModel(
    private val toolbarDelegate: ToolbarDelegate,
) : ToolbarDelegate by toolbarDelegate, ViewModel()

UserViewModel использует ToolbarDelegate, чтобы управлять верхним баром на экране пользователя. Это означает, что тот же самый компонент ToolbarDelegate, который был использован на других экранах, может быть легко переиспользован здесь.

UI
@Composable
fun UserScreen(
    viewModel: UserViewModel,
) {
    val toolbarUiState by viewModel.toolbarUiState.collectAsState()

    AppTopBar(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp),
        state = toolbarUiState,
    )
}

В Composable-функции UserScreen мы используем toolbarUiState, предоставленный UserViewModel, для отображения TopBar. Это создает согласованный дизайн на экране пользователя и гарантирует, что TopBarна этом экране будет выглядеть и вести себя так же, как и на других экранах.

Таким образом, мы видим, как применение делегатов позволяет создавать переиспользуемые компоненты, что упрощает разработку и поддержку приложений, особенно в случае, когда несколько экранов могут иметь общие элементы интерфейса.

Переиспользование PaymentDelegate для оплаты на разных экранах

Давайте рассмотрим другой пример, который демонстрирует, как делегаты могут быть переиспользованы на разных экранах в приложении. В этом примере мы создадим PaymentDelegate, который будет использоваться для выполнения оплаты на разных экранах.

Пример: PaymentDelegate

PaymentDelegate
interface PaymentDelegate {

    context(ViewModel)
    fun pay(productId: String)

    suspend fun purchaseProduct(productId: String)
}

class PaymentDelegateImpl @Inject constructor(
    private val paymentRepository: PaymentRepository,
    private val paymentHistoryRepository: PaymentHistoryRepository,
    private val paymentAnalytics: PaymentAnalytics,
) : PaymentDelegate {

    context(ViewModel)
    override fun pay(productId: String) {
        viewModelScope.launch {
            purchaseProduct(productId)
        }
    }

    override suspend fun purchaseProduct(productId: String) {
        paymentAnalytics.trackStartPaymentEvent()

        paymentRepository.pay(productId)

        paymentAnalytics.trackFinishPaymentEvent()

        paymentHistoryRepository.refresh()
    }
}

PaymentDelegate определяет один метод pay(productId: String), который выполняет процесс оплаты.

PaymentDelegateImpl реализует этот интерфейс и предоставляет реализацию метода pay. Внутри этого метода выполняются следующие шаги:

  1. Отслеживание события начала оплаты с помощью аналитики (paymentAnalytics.trackStartPaymentEvent()).

  2. Выполнение оплаты продукта с использованием paymentRepository.

  3. Отслеживание события завершения оплаты (paymentAnalytics.trackFinishPaymentEvent()).

  4. Обновление истории платежей через paymentHistoryRepository.

Теперь мы можем использовать этот PaymentDelegate на разных экранах в нашем приложении, где требуется процесс оплаты.

Например, предположим, у нас есть экран Shop и экран Subscription. Оба экрана могут использовать PaymentDelegate для обработки оплаты без необходимости дублировать логику оплаты на каждом экране.

В обоих ViewModel (ShopViewModel и SubscriptionViewModel) мы используем PaymentDelegate для выполнения оплаты продукта или подписки. Это значительно упрощает код и позволяет переиспользовать логику оплаты на разных экранах.

Таким образом, применение делегатов в данном контексте позволяет создавать переиспользуемые компоненты для обработки общих задач, таких как оплата, и повышает читаемость и обслуживаемость кода в приложении.

Опишем примеры реализации ShopViewModel и SubscriptionViewModel, где оба ViewModel будут использовать PaymentDelegate для выполнения оплаты.

ShopViewModel
class ShopViewModel(
    paymentDelegate: PaymentDelegate
) : ViewModel(),
    UiStateDelegate<UiState, Any> by UiStateDelegateImpl(UiState()),
    PaymentDelegate by paymentDelegate {

    data class UiState(
        val isLoading: Boolean = false,
    )

    fun purchase(productId: String) {
        viewModelScope.launch(CoroutineExceptionHandler { _, throwable ->
            throwable.printStackTrace()
        }) {
            updateUiState { state -> state.copy(isLoading = true) }
            purchaseProduct(productId)
        }.invokeOnCompletion { asyncUpdateUiState(viewModelScope) { state -> state.copy(isLoading = false) } }
    }
}

В некоторых случаях PaymentDelegate может также иметь свое собственное состояние, которое описывает текущее состояние процесса оплаты. Давайте обновим PaymentDelegate для включения состояния оплаты.

PaymentDelegate
interface PaymentDelegate {

    val paymentState: StateFlow<PaymentState>

    context(ViewModel)
    fun pay(productId: String)
}

data class PaymentState(
    val isPaymentInProgress: Boolean = false,
    val paymentResult: PaymentResult? = null
)

sealed class PaymentResult {
    data class Success(val transactionId: String) : PaymentResult()
    data class Failure(val error: String) : PaymentResult()
}

class PaymentDelegateImpl @Inject constructor(
    private val paymentRepository: PaymentRepository,
    private val paymentHistoryRepository: PaymentHistoryRepository,
    private val paymentAnalytics: PaymentAnalytics,
) : PaymentDelegate {

    override val paymentState = MutableStateFlow(PaymentState())
    
    context(ViewModel) 
    override fun pay(productId: String) {
        viewModelScope.launch {
            paymentAnalytics.trackStartPaymentEvent()
            paymentState.value = PaymentState(isPaymentInProgress = true, paymentResult = null)

            val result = runCatching { paymentRepository.pay(productId) }

            if (result.isSuccess) {
                paymentAnalytics.trackFinishPaymentEvent()
                paymentHistoryRepository.refresh()
                paymentState.value = PaymentState(isPaymentInProgress = false, paymentResult = PaymentResult.Success("transactionId"))
            } else {
                val errorMessage = result.exceptionOrNull()?.message ?: "Payment failed"
                paymentState.value = PaymentState(isPaymentInProgress = false, paymentResult = PaymentResult.Failure(errorMessage))
            }
        }
    }
}

В этой обновленной версии PaymentDelegate, мы добавили свойство paymentState, представляющее текущее состояние оплаты. PaymentState содержит информацию о том, идет ли в данный момент процесс оплаты (isPaymentInProgress) и результат оплаты (paymentResult). paymentResult может быть либо Success (успешная оплата с идентификатором транзакции), либо Failure (неудачная оплата с сообщением об ошибке).

В методе pay(productId: String), мы теперь обновляем paymentState в соответствии с текущим состоянием оплаты. Мы устанавливаем isPaymentInProgress в true в начале оплаты, а затем обновляем его в зависимости от результата оплаты. В случае успешной оплаты, мы также передаем информацию о транзакции.

Таким образом, PaymentDelegate теперь не только выполняет оплату, но и предоставляет информацию о текущем состоянии оплаты, что может быть полезно для отображения информации об оплате на пользовательском интерфейсе или для обработки ошибок оплаты на разных экранах в приложении.

Пример реализации

Выводы

Использование делегатов в мобильных приложениях предоставляет множество преимуществ и сделает ваши ViewModel более ясными, лаконичными и легко переиспользуемыми. Вот несколько ключевых выводов:

1. Переиспользование логики: Делегаты позволяют вынести часто используемую логику из ViewModel и использовать ее на разных экранах и в разных частях приложения. Это способствует уменьшению дублирования кода и поддерживает принцип DRY (Don’t Repeat Yourself).

2. Ясность и упрощение кода: ViewModel становятся более чистыми и понятными. Логика, связанная с определенными функциональными блоками, разделяется на отдельные делегаты, что делает код более структурированным и управляемым.

3. Улучшение сопровождаемости: При использовании делегатов легче поддерживать код, так как каждый делегат отвечает только за свой конкретный функциональный блок. Это облегчает поиск и исправление ошибок.

4. Возможность замены и расширения: Делегаты могут быть легко заменены или расширены без изменения основной логики ViewModel. Это позволяет легко вносить изменения и добавлять новый функционал.

5. Повышение читаемости кода: Код становится более читаемым и понятным благодаря четкому разделению функциональности на отдельные делегаты. Это упрощает командную работу и обучение новых разработчиков.

6. Инкапсуляция состояния и логики: Делегаты позволяют инкапсулировать состояние и логику внутри себя, что способствует более гибкой архитектуре приложения.

В итоге использование делегатов в мобильных приложениях является мощным инструментом для упрощения разработки, улучшения качества кода и обеспечения более высокой производительности разработчиков. Этот подход помогает создавать более модульные и переиспользуемые компоненты, что существенно облегчает разработку и поддержку приложений.

Источник: https://habr.com/ru/articles/776344/


Интересные статьи

Интересные статьи

В этом эпизоде подкаста «Сушите вёсла» поговорили с архитектором «Альфа-Банка» Максимом Чернухиным про то, как банки оказываются в облаках и защищаются от шумных соседей, и&nb...
Сегодня я расскажу, про базовое решение задачи рекомендации текстового контента на конкретном примере — ленте одной российской социальной сети. Посмотрим, что под капотом у сервиса рекомендаций, какие...
В статье расскажу о том, как тестировал микросервис, с помощью мок-данных или моков (mock data). Объясню, что это такое, почему их использовал, как создавал и к каким выводам пришел.
Я каждый день пишу код на сишарпе, и натыкаюсь на одну проблему: я трачу кучу времени на то, чтобы решить, как быть, если что-то идёт не по плану. У меня есть приличный опыт работы...
NedoOS – многозадачная операционная система для «русского ZX Spectrum» со средами программирования на ассемблере, Basic, Pascal, C, NedoLang. Работает на TR-DOS, FAT16 и FAT32 с длинн...