Since I've been using compose for a while now and ...
# compose
c
Since I've been using compose for a while now and I've gotten pretty comfortable with delivering features for my team, one thing that I still keep running up against is the fact that I'm using compose-navigation with AAC ViewModels + Hilt... but my ViewModels just end up getting pretty darn big. I understand that I can refactor into some helper methods and pull networking code into different classes and stuff, but is it just me or do VMs esp with compose seem to just build up into god classes themselves? Or is that basically how it work (the work has to eventually happen somewhere... right?)
I'm just getting a huge amount of imposter syndrome here and curious to hear what others are doing. I guess repository pattern and "use cases" pattern? I don't do either. Repository pattern I think actually isn't all that great (remote and local data sources are very different things IMO) and most of the apps I work on are remote data only to make sure we show fresh data. Use-cases is something I've seen around, but never implemented. Are use-cases the solution to my god-like VM classes?
e
Repository pattern and "use cases" pattern
Doesn't have to be those two specifically, but some kind of arch like that is the solution here
a
This doesn't sound Compose-specific, TBH. These architectural concerns have also happened with classic UI toolkit — actually the view models should have stayed relatively similar, if not the same.
f
This is too generic a question and depends a lot on the app. If your screen is split into fairly independent sections then you could have, using MVI, a fairly small viewmodel that delegates the responsibilities of each section to a middleware. You can have a single reducer for the state, or follow a similar approach and have a reducer that delegates to child reducers, one per screen section.
c
Also, as someone that frequents this channel often I understand that this isn't necessarily compose specific, but since compose pushes you towards UDF and state down, events up, I can't help but think I'm the only one with this so I figured I'd just get it off my chest.
a
I was just writing something similar, @Francesc. If those sections are independent, you could even consider separate view models / UI components, maybe with a an shared one if you need to handle interactions between these sections (e.g. hide one if the other is visible).
f
We did this, multiple viewmodels, in a project and it can become messy if you need to observe lifecycle because only your main viewmodel receives the events and then needs to broadcast to all the children viewmodels (this is a single viewmodel in the view with child viewmodels). It's not an easy problem to solve and, again, depends a lot on your specific requirements.
And if you then need the pieces to talk to each other it's even messier, so this may only be viable with fairly independent blocks
a
Oh yeah, I agree that it can happen with a parent coordinator — I'm actually working with such an approach right now, but with the parent being responsible for a flow of screens, holding its shared state, etc. The class got huge, as you can imagine.
You're definitely not the only one @Colton Idle
p
Everybody is in the same boat. Projects grow exponentially and there is no one size fits all kind of solution. There are principles but just that, whatever the term involves. Coordinators arch or the App Coordinator pattern I use very often. Especially because some iOS colleagues love it, they kinda push me into it. In fact that last thing I mentioned is important too, iOS code alignment, on top of the crazyness 🤣
t
Are your ViewModels directly holding all the state of your screens @Colton Idle? Depends on what your app is like, but mixing-n-matching the state holder pattern from the docs can make it so that your AAC ViewModels aren’t really doing all the UI logic, they’re just caching things and helping you talk to your data sources (remote or storage).
j
You can create interface contracts for your use cases and implement them with the network layer so you move part of your logic from the vm to the use case
j
@Francesc what do you mean by “middleware” in this context?
c
At my workplace we are using Unidirectional architecture which supports Compose really well. The VMs are lean and usually not more than a couple of lines, if you do not need any crazy mapping between Domain and UI models. High level description on how it looks like:
Copy code
Compose onClick() -> VM.doSomething() -> Model.fetchData() -> modify internal state (StateFlow)
Copy code
Compose dataState = VM.data.collectAsState() <- VM.data = Model.getDataFlow() <- Model.getDataFlow = internal state (StateFlow)
your internal state can be: • SharedPreferences wrapped in DataStore so you get Flows • a Room database so you get Flows from Room, • you can store data in Memory with StateFlows • you can create 1 off events with StateFlows/Channels • just query your backend every time with a flow{} builder
c
One of the things that has helped me keep the size of my ViewModels down, is to not think of an “API Layer”, or “Database Layer” with the Repository layer acting as a translator between the VMs and the API/DB. Instead, I’ve found it much more helpful to think of the Repository layer as the important piece of your app that holds and manages data, and the API/DB are simply data sources that it may be interacting with. I start with defining the models I want to work with in my app, and then build the Repository layer to fetch/cache/translate the API/DB models into that ideal form. Essentially, the VMs (one per screen) are the what of your application. They get data from the repository, and manage interactions with the UI, but ultimately only care about what they’ve been given and have relatively little “business logic” in them. The Repository, then, is the how of your app. The VMs need data, but don’t care how it’s fetched, but the Repository does care how it’s fetched, and it can work to re-fetch data if stale, cache it in memory, refresh oauth tokens, etc. This is the cleanest separation I’ve found, and the easiest to communicate to team members (compared to other more formalized patterns), since it’s really only 2 layers to your app.
f
@Francesc what do you mean by “middleware” in this context?
in MVI a middleware is a component that accepts some of the intents and executes async work
Instead, I’ve found it much more helpful to think of the Repository layer as the important piece of your app that holds and manages data, and the API/DB are simply data sources that it may be interacting with. I start with defining the models I want to work with in my app, and then build the Repository layer to fetch/cache/translate the API/DB models into that ideal form.
I have worked on projects that do this as well and has helped, however we do not do this at the repository layer, we do this at the interactors (also known as use cases). These aggregate data from 1 or multiple repositories, and map it to a data model that the viewmodel expects (so the viewmodel does not have a direct dependency on the repositories but only on the interactors), and then the viewmodel can pass this data to the UI with minimum manipulation
c
we do this at the interactors (also known as use cases)
Yeah, I’m moving toward that terminology as well. As a general principle, i still prefer to call it the “Repository Layer”, which can (optionally) be broken down into smaller “use-cases”. I haven’t quite figured out the architecture I’m happy with yet for how to manage many interrelated use cases in large apps, though.