samedi 14 janvier 2023

How to split complicated ViewModel into reusable parts? Using MVVM, android jetpack compose

I have an application with MVVM pattern with jetpack compose, the code style is similar with NowInAndroid app structure. Now I faced the issue, please help with examples, that I can investigate and move further. I have complicated screen, for example this is the entity (it came from room as flow) and I should provide editing feature for the user. The entity contain data including some list. For editing this list I open a dialog above the screen, each item is a Card with text fields, etc. Also, I need this dialog to be opened from other screens of this application with the same goal to edit the same type of list.

Now, each change of each field triggers action in viewmodel like other actions that comes from other screen components.

All I did : I separated dialog Composable and each Card and use them. I united all dialog actions in one interface that my viewmodel implemented. So, now for reuse this dialog I should other viewmodel implement this actions interface, but the implementation will be almost the same! it is 12 actions now.

Now I cannot understand how to separate this implementation of actions from viewmodel.

The same question I have not only about dialog, but about any part of the screen (composable) that have complicated logic with actions and should be reusable.

Structure of code for example that I have now.

@Composable
 fun ScreenRoute(
  viewModel: ExampleEntityEditViewModel = hiltViewModel(),){
//...
val exampleDialogUIStateby viewModel.exampleDialogUIState.collectAsStateWithLifecycle()
val exampleActions: ExampleDialogActions = viewModel

if (exampleDialogUIState.visible) {
        ExampleDialog(
            uiState = exampleDialogUIState,
            actions = exampleActions,
//...
        )
    }
}

@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
fun ExampleDialog(
    uiState: ExampleDialogUIState,
    actions: ExampleDialogActions,
    currencies: List<Currency>,
    //...
    showInfoDialog: Boolean,
    onDismiss: () -> Unit,
    modifier: Modifier = Modifier,
    messages: List<Message>,
) {
    if (uiState.visible) {
        ModalBottomSheetLayout(){
            Scaffold(
                modifier = modifier,
                topBar = {
                    CustomTopAppBar(
                        titleRes = R.string.title,
                        navigationIcon = CustomIcons.Close,
                        onNavigationClick = onDismiss,
                        actionIcon = CustomIcons.Info,
                        //...
                        onActionClick = actions::onInfoClicked,
                    )
                },
                floatingActionButton = {
                    FloatingActionButton(
                        onClick = actions::onExampleAddClicked,
                        //...
                    )
                },
            ) { innerPadding ->
                if (uiState.items.isEmpty()) {
                    EmptyScreen(
                        modifier = Modifier.padding(innerPadding),
                        messageHeader = //..,
                        messageText = //..,
                    )
                } else {
                    ExampleDialogEditContent(
                        modifier = Modifier.padding(innerPadding),
                        uiState = uiState,
                        actions = actions,
                        //...
                    )
                }
            }
            if (showInfoDialog) {
                MessageDialog(
                    //...
                )
            }
        }
    }
}

interface ExampleDialogActions {
    fun onExampleAmountChanged(exampleItem: ExampleItem, value: String)
    fun onExamplePeriodCountChanged(exampleItem: ExampleItem, value: String)
    fun onExampleTypeSelected(exampleItem: ExampleItem, value: String)
    fun onExampleSelectedClicked(id: Long)
    fun onExampleDeleteClicked(exampleItem: ExampleItem)
    fun onExampleAddClicked()
    fun onDismissExampleDialog()
    //...
    fun onInfoClicked()
    fun onDismissInfoDialog()
    fun onExampleMessageShown(errorId: Long)
    fun onCurrencySelected(currency: Currency)
}

data class ExampleDialogUIState(
    val visible: Boolean,
    val showInfoDialog: Boolean,
    val exampleItems: List<ExampleDialogUIState>,
    val currency: Currency,
) {
    companion object {
        val initialState = ExampleDialogUIState(
            visible = false,
            exampleItems = listOf(),
            showInfoDialog = false,
            currency = DefaultCurrency
        )
    }
}

@HiltViewModel
class ExampleViewModel @Inject constructor(
    private val exampleRepository: ExampleRepository,
    private val currencyRepository: CurrencyRepository,
    private val preferencesManager: DefaultPreferencesManager,
    savedStateHandle: SavedStateHandle,
) : ViewModel(), ExampleDialogActions {
    //...
    private val _exampleDialogUIState: MutableStateFlow<ExampleDialogUIState> =
        MutableStateFlow(ExampleDialogUIState.initialState)
    val exampleDialogUIState: StateFlow<ExampleDialogUIState> get() = _exampleDialogUIState
    //...
    override fun onShowExampleDialog() {
        _exampleDialogUIState.update {
            _exampleDialogUIState.value.copy(
                visible = true,
                exampleItems = _mainEntity.value.someList.map { item ->
                    ExampleItem(
                        item = item,
                        amount = item.amount.toString(),
                        amountInputIsError = false,
                        title = "",
                        //...
                    )
                },
                currency = _mainEntity.value.currency
            )
        }
    }
//...
override fun onExampleAmountChanged(exampleItem: ExampleItem, value: String) {
    _exampleDialogUIState.update {
        val exampleItems = _exampleDialogUIState.value.exampleItems
        val itemToChange = exampleItems.indexOfFirst {
            it.id == exampleItem.id
        }
        _exampleDialogUIState.value.copy(
            exampleItems = exampleItems.copy {
                this[itemToChange] = this[itemToChange].copy(
                    amount = value,
                    amountInputIsError = !validateExampleAmount(value).status,
                )
            }
        )
    }
}
}

Aucun commentaire:

Enregistrer un commentaire