- Overview
- Caracteristics
- Work in Progress
- Setup for Contributors
- Threading Strategy
- Notes on UseCases
- Notes on ViewModels
- Modularization
- Credits
- CI/CD Workflows
- Git Hooks
- Gradle Tasks Reference
- Kover Code Coverage
- Ktlint Formatting Guide
- Module Dependencies
- Threading Strategy
- Accessibility
- TODO List
This is a simple HIIT Android App for Android TV and mobile devices.
- Sessions settings can be refined, like work and rest period duration, number of work period per cycle, and which exercise family to include in a session, or create and edit users.
- On the home screen one can select which user(s) are joining a session, and how many cycle(s) the session should contain.
- The session consists of an alternance of rest and exercise periods, showing an exercise as a looping gif image. A summary of the exercises done is shown at the end of the session.
- A statistics screen allows to see (and delete) basic statistics for each user.
- MVVM - Clean architecture
- multi-module
- Dagger-hilt for DI
- pure kotlin
- coroutines and flows / stateflows
- Kover for test coverage report generation (see docs/KOVER_CODE_COVERAGE.md)
- Full-Compose for UI
- handle device form-factor variation (TV / mobile) in the same project
- ktlint for code style enforcement (see docs/KTLINT_FORMATTING_GUIDE.md)
This is a WIP: see current TODO list
After cloning the repository, run the git hooks setup to enable automatic handling of CI-generated commits:
./.githooks/setup.shThis configures a pre-push hook that automatically merges remote changes (like dependency graph updates from CI) before pushing. See docs/GITHOOKS.md for details.
This project uses dispatcher injection for clean thread management across architecture layers:
- Presentation (ViewModels): Main thread
- Domain (Use Cases): Default thread
- Data (Repository): IO thread
All suspend methods are main-safe, enabling testability through TestDispatcher injection.
For complete details on threading choices and rationale, see docs/THREADING.md.
Usecases follow a common convention:
- They are named [action]+"UseCase"
- They expose a single public method named "execute", suspending or not, with various input
parameters, as needed
While I like the idea of replacing the execute method by an override of the
invoke operator, I found out that it has
2 main drawbacks:
- First, and that is a big issue in my mind, overriding the
invokeoperator is mostly interesting because it allows one to shorten the call site code, by simply invoking the usecase. Now that last point in my opinion comes with a steep decrease in discoverability, as it will break the IDE's ability to find usages of the invoke method, thus greatly hindering navigation around the code. - Removing the "Usecase" suffix makes the task of finding names harder, as the usecase is usually invoked from inside a viewmodel's method. Now this method could very well have the same name, and the "Usecase" suffix allows keeping the same name for both: UserManagementViewModel:createUser(user) can call createUserUseCase(user). If your usecase is also named CreateUser, troubles won't be long coming
These are the reasons why I'll keep using the "traditional" structure for usecases, as I value discoverability and clarity more than conciseness.
ViewModels are provided with a dedicated Interactor, which is a simple convenience wrapper around
all the usecases the viewmodel needs.
This is mostly to reduce the list of constructor parameters in the ViewModel, and simplify the
addition of any new usecase to it.
Since the group of usecases needed might vary between platforms and their dedicated presentation
layer, the interactor is part of the presentation layer too.
This project follows a matrix-like modularization structure with strict clean architecture principles:
- Features (columns): Home, Settings, Session, Statistics
- Layers (rows): Presentation (UI), Domain, Data
- Platforms: Mobile and TV with shared foundation modules
The dependency graph shows the current validated module structure. Dependencies are validated on every PR, and the graph is automatically updated when changes are merged to master. Dependencies flow top-to-bottom only, with lateral dependencies restricted to common modules.
For complete details on module architecture, dependency rules, and enforcement, see docs/MODULE_DEPENDENCIES.md.
Note on creating new modules in Android Studio: AS will add these plugins to the main build.gradle.kts - remove them as they'll fail the build:
id("com.android.library") version ...id("org.jetbrains.kotlin.android") version ...
All exercise pictures were made with the help of the awesome PoseMyArt free web app
- Ben Mannes' Gradle version plugin
- Kover - Kotlin code coverage by JetBrains
- Modules Graph Assert - Dependency validation and visualization
- Mockk library
- Glide Compose image loading library
- Ktlint Gradle by Jonathan Leitschuh
- The "Real" Modularization in Android by Better Programming Denis Brandi
- Effective testing with Android Test Only Modules by Shubham Garg
- Sharing build logic with Kotlin DSL by Chirag Kunder

