A sample project using the Marvel API to show a list of superheroes and some stats about them.
Superheroes List | Superhero Details | Error & Retry |
---|---|---|
The project need marvel_public_api_key
and marvel_private_api_key
to build. You can add them to your home level gradle.properties
file (located in ~/.gradle
on Unix based systems):
marvel_public_api_key=<PUBLIC API KEY HERE>
marvel_private_api_key=<PRIVATE API KEY HERE>
or using -Pmarvel_public_api_key=<PUBLIC API KEY HERE> -Pmarvel_private_api_key=<PRIVATE API KEY HERE>
to each gradle command e.g.:
./gradlew assembleDebug -Pmarvel_public_api_key=<PUBLIC API KEY HERE> -Pmarvel_private_api_key=<PRIVATE API KEY HERE>
Check out the Marvel Developer portal for more info.
The app uses a reactive architecture built atop Flow. The app follows a layered architecture with data, domain and presentation layer. The package structure is an attempt to package by feature, however both screens share the data and domain layers. The app uses a single activity + fragments and the Jetpack Navigation Component.
The API call is modeled using Retrofit, KotlinX Serialization as the converter. The data layer converts the DTO objects to Domain objects. Any expected errors that happen up to this point are mapped to a sealed class SuperheroError
. A custom exception SuperheroException
that contains a property of the error is delivered as an Flow
error in the stream.
The main class here is Superhero
. It has a static create
function that converts the string that comes from the API (as a thumbnail) into a HttpUrl
also making sure it's https (so it works on Android).
Each screen is represented by a Fragment
which plays the role of glue code. It's responsible for DI, forwarding actions from the view (Compose) to the ViewModel
and forwarding state from the ViewModel
to the view. It also handles any side effect emitted by the ViewModel
e.g. navigation events.
Each fragment has a Jetpack ViewModel that:
- exposes a single
Flow<ViewState>
backed by aMutableStateFlow
(caching the last item) describing the state of the view at a given time - exposes a single
Flow<Effect>
backed by aChannel
for side effects like navigation, Snackbar or similar. Event that happen when no-one is subscribed are cached. All events are delivered when subscribed - exposes a
CoroutineScope
with operations tied to it's lifecycle
The Fragment observes the Flow<ViewState>
between onStart
and onStop
and updates the Sceen
. The Fragment observes Flow<Effect>
between onStart
and onStop
making sure fragment transactions are executed only when the view is active.
The Fragment observes the Flow<Action>
from onStart
until onStop
. However any network calls that result from those interactions are de-coupled from this lifecycle. The operations triggered by the view actions are coupled to the ViewModel
lifecycle and are only disposed in the ViewMode.onDispose()
function. Check the fork() function for more details.
The logic is written as extension functions on top of a module (collection of dependencies).
The sample uses the DI approach from Simple Kotlin DI. The dependencies with Singleton scope live in the app as AppModule
. Each fragment uses the AppModule
dependencies and can add it's own (e.g. the ViewModel
) that are un-scoped or use Jetpack for scoping (e.g. ViewModel
).
The logic is written as extension functions on top of a module (collection of dependencies).
This sample uses kotest as a testing library. The presentation logic is tested by mocking the Retrofit Service and using a TestViewModel
that uses MutableSharedFlow
instead of MutableStateFlow
and remembers all events. Tests use the real schedulers and Turbine for testing Flow
.
The view is tested in isolation using Espresso, by setting a ViewState and verifying the correct elements are displayed/hidden and the text matches the expected.
There is also one E2E (black box) test using Maestro that tests both fragments + activity together.
Approaches is this sample are heavily inspired by open source code I have read. It is impossible to list them all, but two samples that were key are: