Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cb6520ef authored by cketti's avatar cketti
Browse files

Change ViewModel orchestration in account setup

Using more direct dependencies makes it easier to follow what's going on.
parent a233a63b
Loading
Loading
Loading
Loading
+12 −6
Original line number Diff line number Diff line
@@ -82,20 +82,26 @@ val featureAccountSetupModule: Module = module {
    viewModel {
        AccountSetupViewModel(
            createAccount = get(),
            autoDiscoveryViewModel = get(),
            incomingViewModel = get(),
            incomingValidationViewModel = get(named(NAME_INCOMING_VALIDATION)),
            outgoingViewModel = get(),
            outgoingValidationViewModel = get(named(NAME_OUTGOING_VALIDATION)),
            optionsViewModel = get(),
        )
    }
    viewModel {
    factory<AccountAutoDiscoveryContract.ViewModel> {
        AccountAutoDiscoveryViewModel(
            validator = get(),
            getAutoDiscovery = get(),
        )
    }
    viewModel {
    factory<AccountIncomingConfigContract.ViewModel> {
        AccountIncomingConfigViewModel(
            validator = get(),
        )
    }
    viewModel(named(NAME_INCOMING_VALIDATION)) {
    factory<AccountValidationContract.ViewModel>(named(NAME_INCOMING_VALIDATION)) {
        AccountValidationViewModel(
            validateServerSettings = get(),
            initialState = AccountValidationContract.State(
@@ -103,12 +109,12 @@ val featureAccountSetupModule: Module = module {
            ),
        )
    }
    viewModel {
    factory<AccountOutgoingConfigContract.ViewModel> {
        AccountOutgoingConfigViewModel(
            validator = get(),
        )
    }
    viewModel(named(NAME_OUTGOING_VALIDATION)) {
    factory<AccountValidationContract.ViewModel>(named(NAME_OUTGOING_VALIDATION)) {
        AccountValidationViewModel(
            validateServerSettings = get(),
            initialState = AccountValidationContract.State(
@@ -116,7 +122,7 @@ val featureAccountSetupModule: Module = module {
            ),
        )
    }
    viewModel {
    factory<AccountOptionsContract.ViewModel> {
        AccountOptionsViewModel(
            validator = get(),
        )
+9 −26
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@ import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryCon
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract

interface AccountSetupContract {

@@ -17,7 +18,14 @@ interface AccountSetupContract {
        OPTIONS,
    }

    interface ViewModel : UnidirectionalViewModel<State, Event, Effect>
    interface ViewModel : UnidirectionalViewModel<State, Event, Effect> {
        val autoDiscoveryViewModel: AccountAutoDiscoveryContract.ViewModel
        val incomingViewModel: AccountIncomingConfigContract.ViewModel
        val incomingValidationViewModel: AccountValidationContract.ViewModel
        val outgoingViewModel: AccountOutgoingConfigContract.ViewModel
        val outgoingValidationViewModel: AccountValidationContract.ViewModel
        val optionsViewModel: AccountOptionsContract.ViewModel
    }

    data class State(
        val setupStep: SetupStep = SetupStep.AUTO_CONFIG,
@@ -32,36 +40,11 @@ interface AccountSetupContract {
            val isAutomaticConfig: Boolean,
        ) : Event

        data class OnStateCollected(
            val autoDiscoveryState: AccountAutoDiscoveryContract.State,
            val incomingState: AccountIncomingConfigContract.State,
            val outgoingState: AccountOutgoingConfigContract.State,
            val optionsState: AccountOptionsContract.State,
        ) : Event

        object OnBack : Event
    }

    sealed interface Effect {

        data class UpdateIncomingConfig(
            val state: AccountIncomingConfigContract.State,
        ) : Effect

        object UpdateIncomingConfigValidation : Effect

        data class UpdateOutgoingConfig(
            val state: AccountOutgoingConfigContract.State,
        ) : Effect

        object UpdateOutgoingConfigValidation : Effect

        data class UpdateOptions(
            val state: AccountOptionsContract.State,
        ) : Effect

        object CollectExternalStates : Effect

        data class NavigateNext(
            val accountUuid: String,
        ) : Effect
+7 −56
Original line number Diff line number Diff line
@@ -3,75 +3,26 @@ package app.k9mail.feature.account.setup.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.common.mvi.observe
import app.k9mail.feature.account.setup.NAME_INCOMING_VALIDATION
import app.k9mail.feature.account.setup.NAME_OUTGOING_VALIDATION
import app.k9mail.feature.account.setup.ui.AccountSetupContract.Effect
import app.k9mail.feature.account.setup.ui.AccountSetupContract.Event
import app.k9mail.feature.account.setup.ui.AccountSetupContract.SetupStep
import app.k9mail.feature.account.setup.ui.AccountSetupContract.ViewModel
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryScreen
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryViewModel
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigScreen
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigViewModel
import app.k9mail.feature.account.setup.ui.incoming.toValidationState
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
import app.k9mail.feature.account.setup.ui.options.AccountOptionsScreen
import app.k9mail.feature.account.setup.ui.options.AccountOptionsViewModel
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigScreen
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigViewModel
import app.k9mail.feature.account.setup.ui.outgoing.toValidationState
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract
import app.k9mail.feature.account.setup.ui.validation.AccountValidationScreen
import app.k9mail.feature.account.setup.ui.validation.AccountValidationViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.core.qualifier.named

@Suppress("LongMethod", "CyclomaticComplexMethod")
@Suppress("LongMethod")
@Composable
fun AccountSetupScreen(
    onFinish: (String) -> Unit,
    onBack: () -> Unit,
    viewModel: ViewModel = koinViewModel<AccountSetupViewModel>(),
    autoDiscoveryViewModel: AccountAutoDiscoveryContract.ViewModel = koinViewModel<AccountAutoDiscoveryViewModel>(),
    incomingViewModel: AccountIncomingConfigContract.ViewModel = koinViewModel<AccountIncomingConfigViewModel>(),
    incomingValidationViewModel: AccountValidationContract.ViewModel = koinViewModel<AccountValidationViewModel>(
        named(
            NAME_INCOMING_VALIDATION,
        ),
    ),
    outgoingViewModel: AccountOutgoingConfigContract.ViewModel = koinViewModel<AccountOutgoingConfigViewModel>(),
    outgoingValidationViewModel: AccountValidationContract.ViewModel = koinViewModel<AccountValidationViewModel>(
        named(
            NAME_OUTGOING_VALIDATION,
        ),
    ),
    optionsViewModel: AccountOptionsContract.ViewModel = koinViewModel<AccountOptionsViewModel>(),
) {
    val (state, dispatch) = viewModel.observe { effect ->
        when (effect) {
            is Effect.UpdateIncomingConfig -> incomingViewModel.initState(effect.state)
            is Effect.UpdateIncomingConfigValidation -> {
                incomingValidationViewModel.initState(incomingViewModel.state.value.toValidationState())
            }

            is Effect.UpdateOutgoingConfig -> outgoingViewModel.initState(effect.state)
            is Effect.UpdateOutgoingConfigValidation -> {
                outgoingValidationViewModel.initState(outgoingViewModel.state.value.toValidationState())
            }

            is Effect.UpdateOptions -> optionsViewModel.initState(effect.state)
            is Effect.CollectExternalStates -> viewModel.event(
                Event.OnStateCollected(
                    autoDiscoveryState = autoDiscoveryViewModel.state.value,
                    incomingState = incomingViewModel.state.value,
                    outgoingState = outgoingViewModel.state.value,
                    optionsState = optionsViewModel.state.value,
                ),
            )

            is Effect.NavigateNext -> onFinish(effect.accountUuid)
            Effect.NavigateBack -> onBack()
        }
@@ -89,7 +40,7 @@ fun AccountSetupScreen(
                    )
                },
                onBack = { dispatch(Event.OnBack) },
                viewModel = autoDiscoveryViewModel,
                viewModel = viewModel.autoDiscoveryViewModel,
            )
        }

@@ -97,7 +48,7 @@ fun AccountSetupScreen(
            AccountIncomingConfigScreen(
                onNext = { dispatch(Event.OnNext) },
                onBack = { dispatch(Event.OnBack) },
                viewModel = incomingViewModel,
                viewModel = viewModel.incomingViewModel,
            )
        }

@@ -105,7 +56,7 @@ fun AccountSetupScreen(
            AccountValidationScreen(
                onNext = { dispatch(Event.OnNext) },
                onBack = { dispatch(Event.OnBack) },
                viewModel = incomingValidationViewModel,
                viewModel = viewModel.incomingValidationViewModel,
            )
        }

@@ -113,7 +64,7 @@ fun AccountSetupScreen(
            AccountOutgoingConfigScreen(
                onNext = { dispatch(Event.OnNext) },
                onBack = { dispatch(Event.OnBack) },
                viewModel = outgoingViewModel,
                viewModel = viewModel.outgoingViewModel,
            )
        }

@@ -121,7 +72,7 @@ fun AccountSetupScreen(
            AccountValidationScreen(
                onNext = { dispatch(Event.OnNext) },
                onBack = { dispatch(Event.OnBack) },
                viewModel = outgoingValidationViewModel,
                viewModel = viewModel.outgoingValidationViewModel,
            )
        }

@@ -129,7 +80,7 @@ fun AccountSetupScreen(
            AccountOptionsScreen(
                onNext = { dispatch(Event.OnNext) },
                onBack = { dispatch(Event.OnBack) },
                viewModel = optionsViewModel,
                viewModel = viewModel.optionsViewModel,
            )
        }
    }
+21 −24
Original line number Diff line number Diff line
@@ -7,23 +7,31 @@ import app.k9mail.feature.account.setup.ui.AccountSetupContract.Effect
import app.k9mail.feature.account.setup.ui.AccountSetupContract.Event
import app.k9mail.feature.account.setup.ui.AccountSetupContract.SetupStep
import app.k9mail.feature.account.setup.ui.AccountSetupContract.State
import app.k9mail.feature.account.setup.ui.AccountSetupContract.ViewModel
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract
import app.k9mail.feature.account.setup.ui.common.mapper.toIncomingConfigState
import app.k9mail.feature.account.setup.ui.common.mapper.toOptionsState
import app.k9mail.feature.account.setup.ui.common.mapper.toOutgoingConfigState
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract
import app.k9mail.feature.account.setup.ui.incoming.toServerSettings
import app.k9mail.feature.account.setup.ui.incoming.toValidationState
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
import app.k9mail.feature.account.setup.ui.options.toAccountOptions
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract
import app.k9mail.feature.account.setup.ui.outgoing.toServerSettings
import app.k9mail.feature.account.setup.ui.outgoing.toValidationState
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract
import kotlinx.coroutines.launch

class AccountSetupViewModel(
    private val createAccount: UseCase.CreateAccount,
    override val autoDiscoveryViewModel: AccountAutoDiscoveryContract.ViewModel,
    override val incomingViewModel: AccountIncomingConfigContract.ViewModel,
    override val incomingValidationViewModel: AccountValidationContract.ViewModel,
    override val outgoingViewModel: AccountOutgoingConfigContract.ViewModel,
    override val outgoingValidationViewModel: AccountValidationContract.ViewModel,
    override val optionsViewModel: AccountOptionsContract.ViewModel,
    initialState: State = State(),
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {
) : BaseViewModel<State, Event, Effect>(initialState), AccountSetupContract.ViewModel {

    override fun event(event: Event) {
        when (event) {
@@ -36,13 +44,6 @@ class AccountSetupViewModel(
                onAutoDiscoveryFinished(event.state)
            }

            is Event.OnStateCollected -> onStateCollected(
                autoDiscoveryState = event.autoDiscoveryState,
                incomingState = event.incomingState,
                outgoingState = event.outgoingState,
                optionsState = event.optionsState,
            )

            Event.OnBack -> onBack()
            Event.OnNext -> onNext()
        }
@@ -51,9 +52,9 @@ class AccountSetupViewModel(
    private fun onAutoDiscoveryFinished(
        autoDiscoveryState: AccountAutoDiscoveryContract.State,
    ) {
        emitEffect(Effect.UpdateIncomingConfig(autoDiscoveryState.toIncomingConfigState()))
        emitEffect(Effect.UpdateOutgoingConfig(autoDiscoveryState.toOutgoingConfigState()))
        emitEffect(Effect.UpdateOptions(autoDiscoveryState.toOptionsState()))
        incomingViewModel.initState(autoDiscoveryState.toIncomingConfigState())
        outgoingViewModel.initState(autoDiscoveryState.toOutgoingConfigState())
        optionsViewModel.initState(autoDiscoveryState.toOptionsState())
        onNext()
    }

@@ -86,8 +87,8 @@ class AccountSetupViewModel(
        when (state.value.setupStep) {
            SetupStep.AUTO_CONFIG -> {
                if (state.value.isAutomaticConfig) {
                    emitEffect(Effect.UpdateIncomingConfigValidation)
                    emitEffect(Effect.UpdateOutgoingConfigValidation)
                    incomingValidationViewModel.initState(incomingViewModel.state.value.toValidationState())
                    outgoingValidationViewModel.initState(outgoingViewModel.state.value.toValidationState())
                    changeToSetupStep(SetupStep.INCOMING_VALIDATION)
                } else {
                    changeToSetupStep(SetupStep.INCOMING_CONFIG)
@@ -95,7 +96,7 @@ class AccountSetupViewModel(
            }

            SetupStep.INCOMING_CONFIG -> {
                emitEffect(Effect.UpdateIncomingConfigValidation)
                incomingValidationViewModel.initState(incomingViewModel.state.value.toValidationState())
                changeToSetupStep(SetupStep.INCOMING_VALIDATION)
            }

@@ -108,7 +109,7 @@ class AccountSetupViewModel(
            }

            SetupStep.OUTGOING_CONFIG -> {
                emitEffect(Effect.UpdateOutgoingConfigValidation)
                outgoingValidationViewModel.initState(outgoingViewModel.state.value.toValidationState())
                changeToSetupStep(SetupStep.OUTGOING_VALIDATION)
            }

@@ -129,15 +130,11 @@ class AccountSetupViewModel(
    }

    private fun onFinish() {
        emitEffect(Effect.CollectExternalStates)
    }
        val autoDiscoveryState = autoDiscoveryViewModel.state.value
        val incomingState = incomingViewModel.state.value
        val outgoingState = outgoingViewModel.state.value
        val optionsState = optionsViewModel.state.value

    private fun onStateCollected(
        autoDiscoveryState: AccountAutoDiscoveryContract.State,
        incomingState: AccountIncomingConfigContract.State,
        outgoingState: AccountOutgoingConfigContract.State,
        optionsState: AccountOptionsContract.State,
    ) {
        viewModelScope.launch {
            val result = createAccount.execute(
                emailAddress = autoDiscoveryState.emailAddress.value,
+1 −30
Original line number Diff line number Diff line
@@ -7,11 +7,6 @@ import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
import app.k9mail.feature.account.setup.ui.AccountSetupContract.Effect
import app.k9mail.feature.account.setup.ui.AccountSetupContract.SetupStep
import app.k9mail.feature.account.setup.ui.AccountSetupContract.State
import app.k9mail.feature.account.setup.ui.autodiscovery.FakeAccountAutoDiscoveryViewModel
import app.k9mail.feature.account.setup.ui.incoming.FakeAccountIncomingConfigViewModel
import app.k9mail.feature.account.setup.ui.options.FakeAccountOptionsViewModel
import app.k9mail.feature.account.setup.ui.outgoing.FakeAccountOutgoingConfigViewModel
import app.k9mail.feature.account.setup.ui.validation.FakeAccountValidationViewModel
import assertk.assertThat
import assertk.assertions.isEqualTo
import kotlinx.coroutines.test.runTest
@@ -22,12 +17,6 @@ class AccountSetupScreenKtTest : ComposeTest() {
    @Test
    fun `should display correct screen for every setup step`() = runTest {
        val viewModel = FakeAccountSetupViewModel()
        val autoDiscoveryViewModel = FakeAccountAutoDiscoveryViewModel()
        val incomingViewModel = FakeAccountIncomingConfigViewModel()
        val incomingValidationViewModel = FakeAccountValidationViewModel()
        val outgoingViewModel = FakeAccountOutgoingConfigViewModel()
        val outgoingValidationViewModel = FakeAccountValidationViewModel()
        val optionsViewModel = FakeAccountOptionsViewModel()

        setContent {
            ThunderbirdTheme {
@@ -35,12 +24,6 @@ class AccountSetupScreenKtTest : ComposeTest() {
                    onFinish = { },
                    onBack = { },
                    viewModel = viewModel,
                    autoDiscoveryViewModel = autoDiscoveryViewModel,
                    incomingViewModel = incomingViewModel,
                    incomingValidationViewModel = incomingValidationViewModel,
                    outgoingViewModel = outgoingViewModel,
                    outgoingValidationViewModel = outgoingValidationViewModel,
                    optionsViewModel = optionsViewModel,
                )
            }
        }
@@ -54,13 +37,7 @@ class AccountSetupScreenKtTest : ComposeTest() {
    @Test
    fun `should delegate navigation effects`() = runTest {
        val initialState = State()
        val viewModel = FakeAccountSetupViewModel(initialState)
        val autoDiscoveryViewModel = FakeAccountAutoDiscoveryViewModel()
        val incomingViewModel = FakeAccountIncomingConfigViewModel()
        val incomingValidationViewModel = FakeAccountValidationViewModel()
        val outgoingViewModel = FakeAccountOutgoingConfigViewModel()
        val outgoingValidationViewModel = FakeAccountValidationViewModel()
        val optionsViewModel = FakeAccountOptionsViewModel()
        val viewModel = FakeAccountSetupViewModel(initialState = initialState)
        var onFinishCounter = 0
        var onBackCounter = 0

@@ -70,12 +47,6 @@ class AccountSetupScreenKtTest : ComposeTest() {
                    onFinish = { onFinishCounter++ },
                    onBack = { onBackCounter++ },
                    viewModel = viewModel,
                    autoDiscoveryViewModel = autoDiscoveryViewModel,
                    incomingViewModel = incomingViewModel,
                    incomingValidationViewModel = incomingValidationViewModel,
                    outgoingViewModel = outgoingViewModel,
                    outgoingValidationViewModel = outgoingValidationViewModel,
                    optionsViewModel = optionsViewModel,
                )
            }
        }
Loading