Foodies - Modern Android Architecture
Foodies is a sample project that presents a modern 2021 approach to Android app development.
The project tries to combine popular Android tools and to demonstrate best developement practices by utilizing up to date tech-stack like Compose, Kotlin Flow and Hilt while also presenting modern Android application Architecture that is scalable and maintainable through a MVVM blend with MVI.
Description
-
UI
- Compose declarative UI framework
- Material design
-
Tech/Tools
- Kotlin 100% coverage
- Coroutines and Flow for async operations
- Hilt for dependency injection
- Jetpack
- Compose
- Navigation for navigation between composables
- ViewModel that stores, exposes and manages UI state
- Retrofit for networking
- Coil for image loading
-
Modern Architecture
- Single activity architecture (with Navigation component) that defines navigation graphs
- MVVM blend with MVI
- Android Architecture components (ViewModel, Navigation)
- Android KTX - Jetpack Kotlin extensions
Architecture
The project is layered traditionally with a View, Presentation, Model separation and presents a blend between MVVM and MVI inspired from Yusuf Ceylan's architecture but adapted to Compose.
Architecture layers:
- View - Composable screens that consume state, apply effects and delegate events.
- ViewModel - AAC ViewModel that manages and reduces the state of the corresponding screen. Additionally, it intercepts UI events and produces side-effects. The ViewModel lifecycle scope is tied to the corresponding screen composable.
- Model - Repository classes that retrieve data. In a clean architecture context, one should use use-cases that tap into repositories.
As the architecture blends MVVM with MVI, there are a three core components described:
-
State - data class that holds the state content of the corresponding screen e.g. list of
FoodItem
, loading status etc. The state is exposed as aMutableStateFlow
that perfectly matches the use-case of receiving continuos updates with initial values and that are broadcasted to an unknown number of subscribers. -
Event - plain object that is sent through callbacks from the UI to the presentation layer. Events should reflect UI events caused by the user. Event updates are exposed as a
MutableSharedFlow
type which is similar toStateFlow
and that behaves as in the absence of a subscriber, any posted event will be immediately dropped. -
Effect - plain object that signals one-time side-effect actions that should impact the UI e.g. triggering a navigation action, showing a Toast, SnackBar etc. Effects are exposed as
ChannelFlow
which behave as in each event is delivered to a single subscriber. An attempt to post an event without subscribers will suspend as soon as the channel buffer becomes full, waiting for a subscriber to appear.
Every screen/flow defines its own contract class that states all corresponding core components described above: state content, events and effects.
Dependency injection
Hilt is used for Dependency Injection as a wrapper on top of Dagger.
Apart from regular parameter/constructor injection, assisted injection is used in order to inject runtime categoryId
parameter to FoodCategoryDetailsViewModel
. While regular viewmodel injection was done through the HiltViewModel
annotation, FoodCategoryDetailsViewModel
was injected through the @AssistedInject
annotation.
The dynamic injection of the categoryId
was done through defining it as an @Assisted
parameter while also providing ViewModelProvider.Factory
class for FoodCategoryDetailsViewModel
. Additionally, an assisted factory ViewModelAssistedFactory
was defined through the @AssistedFactory
annotation.
Decoupling Compose
Since Compose is a standalone declarative UI framework, one must try to decouple it from the Android framework as much as possible. In order to achieve this, the project uses an EntryPointActivity
that defines a navigation graph where every screen is a composable.
The EntryPointActivity
also collects state
flows and passes them together with the Effect
flows to each Screen composable. This way, the Activity is coupled with the navigation component and only screen (root level) composables. This causes the screen composables to only receive and interact with plain objects and Kotlin flows, therefore being platform agnostic.