I have a very complex composable that is about payment. It can have multiple states (NoPaymentMethodAvailable
and Ready
) and have different actions (OnSubTitleClicked
, OnBuyClicked
, OnPaymentMethodChangedClicked
).
Here are a few screens that demonstrate that:
PaymentMethods available
Change PaymentMethods (Dialog open when click on the PaymentMethod itself)
No PaymentMethods available + a SubTitle-Action
To construct this composable, some business classes are involved. Like the StreamPaymentMethods
(API call(s)), GetPreferredPaymentMethod
(local DB), FormatPaymentMethod
(local), you name it, class.
My question is basically, where do I hoist the state for this composable?
Note: The app architecture is right now based on each screen is a
Fragment
that creates aViewModel
and construct aComposableView
to display things.
If I would used this composable only once I would inject all the needed classes into my ViewModel
, construct a State
out of it, and lets that obvserve via viewModel.state.collectAsStateWithLifecycle()
(assuming the State
is a Flow<State>
).
But since the composable is used in 6 different screens, I obviously don’t want to add all of the needed classes in each of those 6 ViewModel
and basically dublicate code everywhere.
As a first step, I extracted a new PaymentBoxManager
that encapsulated all of the needed classes and produces its own state (PaymentBoxState
).
But now I struggling where to add this class.
To keep the “ViewModel
is the source of truth and hoisting the state” argument, I could put in each ViewModel
the PaymentBoxManager
and using Kotlins delegate feature to observe the PaymentBoxState
like that:
// ViewModel
class AViewModel(pmb: PaymentBoxManager) : ViewModel(), IPaymentBoxManager by pmb
// Fragment/Composable
val state by viewModel.paymentBoxState.collectAsStateWithLifecycle()
PaymentBox(state)
While having the benefit of hoisting it in a longer-loving Lifecycle (the Fragment
) plus “having busniess logic in the ViewModel rather than the UI”, I would have the downside of a bit more (annoying) code. Not showed here in the example, but there is also a bit of Dagger stuff involved.
Comparing to another solution a colleague suggested.
Instead of putting the PaymentBoxState
into the PaymentBox
composable, I could also put the PaymentBoxManager
into it. The PaymentBox
would basically work on their own and there is no need to put anything into the ViewModel
anymore. Screens that uses the PaymentBox
can simply put it to the composable hierarchie and are ready to use it.
@Composable
fun PaymentBox(pbm: PaymentBoxManager = rememberPaymentBoxManager()) {
val state by pbm.state.collectAsStateWithLifecycle()
// Use the state to construct the UI
}
@Composable
fun rememberPaymentBoxManager() = remember { SomeDaggerComponent().paymentBoxManager }
The benfit is cleary that I can just add the PaymentBox
composable and “it just works”.
Also, since the PaymentBoxManager
is a plain Kotlin object, I can also test it Android-dependency free.
However, where I’m unsure with that is the following:
- The
PaymentBoxManager
is now scoped to the composable lifecycle. It lifes shorter than theFragment
lifecycle. Being said, if I would switch the composable hierarchie (removingPaymentBox
and add it again (for whatever reasons)), I would get a newPaymentBoxManager
. - I moved some domain logic to the UI. (Did I?) Because the
PaymentBoxManager
uses the domain classes likeStreamPaymentMethods
that doing an API call or theGetPreferredPaymentMethod
that doing some DB operations, but using it inside a composable, I feel a bit of “I doing it wrong”.
So back to my question:
My question is basically, where do I hoist the state for this composable?
In the ViewModel
(s) and dublicate (a bit of) code or in the composables?
Benefits and downsides are mention in this question.
Maybe there are even more I haven’t discovered yet, but I guess these are the “main issues”.
I also read the official documentation about Where to hoist state, Stateholders#business-logic, and Stateholders#ui-logic.
If I get it right, they basically recommend to put these kind of things to the ViewModel
(what I second), however, there is no single word about reusing composables that requires a lot of domain/business logic.
There might even not “the correct answer”, but just a bit of brainstorming from the community would be great.
Thank you and sorry for this wall of text :).