Home > Software design >  Best practise of using view model in jetpack compose
Best practise of using view model in jetpack compose

Time:08-09

I have few doubt using viewmodel in composable function. I am adding my activity code, I am passing my intent bundle.

  1. So I want to ask is this best practise to use viewmodel like this to create viewmodel global in the activity?

InputActivity.kt

class InputActivity : ComponentActivity() {

    private val viewModel by viewModel<InputViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setupViewModel()
        setContent {
            Theme {
                AppBarScaffold(
                    displayHomeAsUpEnabled = true,
                    titleId = R.string.personal_health
                ) {
                    viewModel.OptionData?.let {
                        Input(it)
                    }
                }
            }
        }
    }

    private fun setupViewModel() {
        viewModel.optionData = intent.getParcelableExtra("optiondata")
    }
}

I have so many composable function

Input

@Composable
fun Input(optionData: OptionData) {
    var value by rememberSaveable {
        mutableStateOf(false)
    }
    Column(
        modifier = Modifier
            .fillMaxHeight()
            .verticalScroll(rememberScrollState())
        verticalArrangement = Arrangement.SpaceBetween
    ) {
        InputItem()
        Spacer()
        OnSubmitPulse()
    }
}

InputItem

@Composable
fun InputItem() {
    Image()
    PulsePressure()
}

PulsePressure

@Composable
fun PulsePressure() {
    Column {
        InputWithUnitContainer()
        InputWithUnitContainer()
    }
}

InputWithUnitContainer

@Composable
fun InputWithUnitContainer() {
    Row() {
        Text()
        TextField(value = "")
        Text()
    }
}

Every function have logic which I want to store in viewmodel.

  1. So should I create viewmodel in constructors parameters or pass viewmodel instance every time ?

Scenario 1

fun Input(optionData: OptionData,viewModel: InputViewModel = viewModel())

and

fun InputItem(viewModel: InputViewModel = viewModel())

and

fun PulsePressure(viewModel: InputViewModel = viewModel())

Scenario 2

fun Input(optionData: OptionData,viewModel: InputViewModel = viewModel()) {
        InputItem(viewModel)
}

and

fun InputItem(viewModel: InputViewModel) {
      PulsePressure(viewModel)
}

and

fun PulsePressure(viewModel: InputViewModel) {
    // more function call
}

So what would you guys suggest in jetpack compose. Please ask me if you don't understand me question. Many Thanks

CodePudding user response:

I don't think there is not only one best practice but choosing approaches that suits your needs.

You should decide if your ViewModel needs to be in memory while your app is alive or scoped to navigation graph or a Composable.

Second thing to consider is if you will use same Composable in another screen or another app. If so, instead of passing ViewModel to Composable you might consider passing states as params and events to ViewModel via a callback.

Instead of this

fun Input(optionData: OptionData,viewModel: InputViewModel = viewModel()) {
        InputItem(viewModel)
}

I tend to go with this if i need to use, or think i will use Input in different sections or in another app in the future

fun Input(optionData: OptionData, someOtherData, onOptionDataChanged:()->Unit, onSomeOtherDataChanged: () -> Unit) {
        InputItem(viewModel)
}

State in jetpack Compose from codelabs is a good article to read about this subject.

@Composable
fun WellnessScreen(
    modifier: Modifier = Modifier, 
    wellnessViewModel: WellnessViewModel = viewModel()
) {
   Column(modifier = modifier) {
       StatefulCounter()

       WellnessTasksList(
           list = wellnessViewModel.tasks,
           onCheckedTask = { task, checked ->
               wellnessViewModel.changeTaskChecked(task, checked)
           },
           onCloseTask = { task ->
               wellnessViewModel.remove(task)
           }
       )
   }
}

@Composable
fun WellnessTasksList(
   list: List<WellnessTask>,
   onCheckedTask: (WellnessTask, Boolean) -> Unit,
   onCloseTask: (WellnessTask) -> Unit,
   modifier: Modifier = Modifier
) {
   LazyColumn(
       modifier = modifier
   ) {
       items(
           items = list,
           key = { task -> task.id }
       ) { task ->
           WellnessTaskItem(
               taskName = task.label,
               checked = task.checked,
               onCheckedChange = { checked -> onCheckedTask(task, checked) },
               onClose = { onCloseTask(task) }
           )
       }
   }
}

@Composable
fun WellnessTaskItem(
   taskName: String,
   checked: Boolean,
   onCheckedChange: (Boolean) -> Unit,
   onClose: () -> Unit,
   modifier: Modifier = Modifier
) {
   Row(
       modifier = modifier, verticalAlignment = Alignment.CenterVertically
   ) {
       Text(
           modifier = Modifier
               .weight(1f)
               .padding(start = 16.dp),
           text = taskName
       )
       Checkbox(
           checked = checked,
           onCheckedChange = onCheckedChange
       )
       IconButton(onClick = onClose) {
           Icon(Icons.Filled.Close, contentDescription = "Close")
       }
   }
}

Last but not least if it's UI logic that is not dependent of any business logic or if you are building a standalone custom Composables as counterpart of custom Views you can consider capturing UI logic in a class that is wrapped in remember instead of ViewModel. Examples for this approach are any rememberX functions we use with Lists, Scaffolds and other default Composables.

remmeberScrollState for instance

@Stable
class ScrollState(initial: Int) : ScrollableState {

    /**
     * current scroll position value in pixels
     */
    var value: Int by mutableStateOf(initial, structuralEqualityPolicy())
        private set

    /**
     * maximum bound for [value], or [Int.MAX_VALUE] if still unknown
     */
    var maxValue: Int
        get() = _maxValueState.value
        internal set(newMax) {
            _maxValueState.value = newMax
            if (value > newMax) {
                value = newMax
            }
        }

    /**
     * [InteractionSource] that will be used to dispatch drag events when this
     * list is being dragged. If you want to know whether the fling (or smooth scroll) is in
     * progress, use [isScrollInProgress].
     */
    val interactionSource: InteractionSource get() = internalInteractionSource

    internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource()

    private var _maxValueState = mutableStateOf(Int.MAX_VALUE, structuralEqualityPolicy())

    /**
     * We receive scroll events in floats but represent the scroll position in ints so we have to
     * manually accumulate the fractional part of the scroll to not completely ignore it.
     */
    private var accumulator: Float = 0f

    private val scrollableState = ScrollableState {
        val absolute = (value   it   accumulator)
        val newValue = absolute.coerceIn(0f, maxValue.toFloat())
        val changed = absolute != newValue
        val consumed = newValue - value
        val consumedInt = consumed.roundToInt()
        value  = consumedInt
        accumulator = consumed - consumedInt

        // Avoid floating-point rounding error
        if (changed) consumed else it
    }

    override suspend fun scroll(
        scrollPriority: MutatePriority,
        block: suspend ScrollScope.() -> Unit
    ): Unit = scrollableState.scroll(scrollPriority, block)

    override fun dispatchRawDelta(delta: Float): Float =
        scrollableState.dispatchRawDelta(delta)

    override val isScrollInProgress: Boolean
        get() = scrollableState.isScrollInProgress

    /**
     * Scroll to position in pixels with animation.
     *
     * @param value target value in pixels to smooth scroll to, value will be coerced to
     * 0..maxPosition
     * @param animationSpec animation curve for smooth scroll animation
     */
    suspend fun animateScrollTo(
        value: Int,
        animationSpec: AnimationSpec<Float> = SpringSpec()
    ) {
        this.animateScrollBy((value - this.value).toFloat(), animationSpec)
    }

    /**
     * Instantly jump to the given position in pixels.
     *
     * Cancels the currently running scroll, if any, and suspends until the cancellation is
     * complete.
     *
     * @see animateScrollTo for an animated version
     *
     * @param value number of pixels to scroll by
     * @return the amount of scroll consumed
     */
    suspend fun scrollTo(value: Int): Float = this.scrollBy((value - this.value).toFloat())

    companion object {
        /**
         * The default [Saver] implementation for [ScrollState].
         */
        val Saver: Saver<ScrollState, *> = Saver(
            save = { it.value },
            restore = { ScrollState(it) }
        )
    }
} 

Extra

Also depending on your needs or applicability favoring using states with Modifier over Composable might make it easy to use with other Composasbles.

For instance

class MyState(val color:Color)

@composable
fun rememberMyState(color:Color) = remember{MyState(color)}

Wrapping UI logic inside Modifier fun Modifier.myModifier(myState:State)= this.then( Modifier.color(myState.color) )

might have more reusability than in some scenarios

@Composable
fun MyComposable(myState: MyState) {
   Column(Modifier.background(color){...}
}

If we use a Composable in the example above we limit our layout to Column while you can use first one with any Composable you wish. Implementation depends on what's your preferences are mostly.

CodePudding user response:

1.So I want to ask is this best practise to use viewmodel like this to create viewmodel global in the activity?

It is more common to declare it inside of the onCreate function, as shown here:

class MyActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: MyViewModel by viewModels()
        exampleFunction(viewModel)
    }
}

and then pass the viewModel to the functions that require it, declaring the global viewModel seems a bit... anty-pattern, I've never seen it set-up like that but it should work

  1. So should I create viewmodel in constructors parameters or pass viewmodel instance every time?

passing the viewModel instance (so scenario 2) would be enough, there doesn't seem to be a reason to keep multiple instances of the same viewModel at once (especially if all of them contain some init functions)

  • Related