Thanks for your interest in contributing! This guide will help you get started and understand our development workflow.
- Android Studio: Jellyfish (2023.3.1) or newer
- JDK: 11 or higher
- Kotlin: 1.9.x
- Gradle: 8.5+ (via wrapper)
- Clone the repository
- Open in Android Studio
- Let Gradle sync complete
- Build the project:
./gradlew build
This project follows Clean Architecture with a multi-module approach. Before contributing, familiarize yourself with:
- Multi-module structure: Features are isolated in separate modules
- Convention plugins: We use custom Gradle plugins in
build-logic/to maintain consistency - Navigation3: Type-safe navigation with sealed interfaces
- Hilt: Dependency injection throughout the app
- Jetpack Compose: All UI is built with Compose
Read CLAUDE.md for detailed architecture guidance.
We follow the Conventional Commits specification to keep our history clean and enable automated changelog generation.
<type>(<scope>): <subject>
[optional body]
[optional footer]
- feat: New feature for the user
- fix: Bug fix
- docs: Documentation changes only
- style: Code formatting, missing semicolons, etc. (no logic change)
- refactor: Code changes that neither fix bugs nor add features
- perf: Performance improvements
- test: Adding or updating tests
- build: Build system or dependency updates
- ci: CI/CD configuration changes
- chore: Maintenance tasks, tooling updates
- revert: Reverts a previous commit
The scope should reference the module or feature being changed:
app,ui,navigation,network,data,domainrecording,profilebuild-logic,gradle
Simple commits:
feat(recording): add pause/resume functionality
fix(auth): prevent token refresh loop on 401
docs(network): document TokenRefreshCallback pattern
refactor(ui): extract theme colors to sealed class
perf(data): cache user profile in memory
test(profile): add ViewModel state tests
build: upgrade Compose to 1.6.0
chore(deps): update Retrofit to 2.9.0
revert: "feat(auth): add Google login"Commit with body:
fix(auth): handle expired tokens gracefully
Previously, when a token expired, the user was logged out immediately.
Now, we attempt to refresh the token once before logging them out,
providing a better user experience.
Closes #123Commit with breaking change:
feat(api)!: migrate to new authentication flow
BREAKING CHANGE: Clients must now use OAuth2 tokens instead of API keys.
Update your TokenProvider implementation to return OAuth2 tokens.
Migration guide: docs/MIGRATION.md
Closes #456- Use present tense ("add feature" not "added feature")
- Keep the subject line under 72 characters
- Don't capitalize the first letter of the subject
- No period at the end of the subject
- Use the body to explain what and why, not how
- Reference issues with
Closes #123,Fixes #456, orResolves #789 - Mark breaking changes with
!after the scope orBREAKING CHANGE:in footer
- Automated changelogs: Tools can generate release notes from commits
- Semantic versioning: Automatically determine version bumps (major/minor/patch)
- Better history: Easy to scan and understand what changed
- Team consistency: Everyone writes commits the same way
- Follow Kotlin coding conventions
- Use 4 spaces for indentation (no tabs)
- Maximum line length: 120 characters
- Use meaningful variable names (avoid single letters except for iterators)
- Prefer immutability (
valovervar)
- Keep composables small and focused
- Extract reusable UI into
:core:ui - Use
rememberand state hoisting appropriately - Preview composables with
@Preview
When creating new files, follow these package conventions:
<base-package>.<module>
├── ui/ # Composables, screens
├── viewmodel/ # ViewModels
├── model/ # UI models, state classes
└── navigation/ # Navigation routes (if needed)
Use the scaffolding task:
./gradlew createFeature -PfeatureName=myfeatureThis creates:
:feature:myfeature(implementation):feature:myfeature:api(navigation routes)- Necessary build files and manifests
If you need to create a core module manually:
- Create the directory structure
- Add
build.gradle.ktsusing convention plugins:
plugins {
alias(libs.plugins.convention.android.library)
alias(libs.plugins.convention.android.compose) // if needed
alias(libs.plugins.convention.android.hilt) // if needed
}
android {
namespace = "${projectProperties.corePackagePrefix}.modulename"
}
dependencies {
// Module-specific dependencies
}- Create
src/main/AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>- Add to
settings.gradle.kts:
include(":category:modulename")- Create a
README.mddocumenting the module's purpose
# All unit tests
./gradlew test
# Specific module
./gradlew :feature:recording:test
# Instrumented tests (requires device/emulator)
./gradlew connectedAndroidTest- Place unit tests in
src/test/kotlin/ - Place instrumented tests in
src/androidTest/kotlin/ - Use meaningful test names:
should<ExpectedBehavior>When<Condition>() - Mock dependencies with Mockito or MockK
- Test ViewModels with
kotlinx-coroutines-test
- Create a branch: Use a descriptive name like
feat/recording-pauseorfix/profile-crash - Make your changes: Follow the code style and commit message guidelines
- Write tests: Add unit tests for new features
- Update docs: If you change APIs or architecture, update relevant documentation
- Test locally: Ensure
./gradlew buildpasses - Push and create PR: Provide a clear description of what changed and why
- Address review feedback: Be responsive to comments and suggestions
## What changed?
Brief description of the changes
## Why?
Explain the motivation behind this change
## Testing
How was this tested? What edge cases were considered?
## Screenshots (if applicable)
Add screenshots for UI changes
## Checklist
- [ ] Tests added/updated
- [ ] Documentation updated
- [ ] No new warnings
- [ ] Follows code style guidelinesAll dependencies are managed in gradle/libs.versions.toml. To add a new dependency:
- Add the version to the
[versions]section - Add the library to
[libraries]section - Use it in modules as
libs.library.name
Never hardcode versions in module build.gradle.kts files.
Convention plugins live in build-logic/. When editing:
- Make changes in
build-logic/src/main/kotlin/ - Test with a single module:
./gradlew :module:assemble - Ensure all modules build:
./gradlew build - Document significant changes in
build-logic/README.md
When adding cross-feature navigation:
- Define routes in the feature's
:apimodule - Create sealed interfaces implementing
NavKey - Add
@Serializableto route classes - Provide extension functions on
Navigatorfor type-safe navigation
Example:
// In :feature:myfeature:api
@Serializable
sealed interface MyFeatureRoute : NavKey {
@Serializable
data class Detail(val id: String) : MyFeatureRoute
}
fun Navigator.navigateToMyFeatureDetail(id: String) {
navigateTo(MyFeatureRoute.Detail(id))
}If you encounter circular dependency errors:
- Check that
:core:networkdoesn't depend on:core:data - Use callback interfaces defined in the lower-level module
- Follow the Dependency Inversion Principle
- Stop the Gradle daemon:
./gradlew --stop - Clean build:
./gradlew clean - Invalidate caches in Android Studio: File → Invalidate Caches → Invalidate and Restart
- Check
gradle/libs.versions.tomlfor version conflicts
If Hilt or Room generated code isn't found:
- Stop Gradle daemon:
./gradlew --stop(often fixes KSP/Hilt issues) - Rebuild:
./gradlew clean build - Ensure KSP plugin is applied in the module's
build.gradle.kts - Check that annotation processors are configured correctly
If builds are hanging or behaving strangely:
- Stop all Gradle daemons:
./gradlew --stop - Clear Gradle cache:
rm -rf ~/.gradle/caches/ - Sync project again
Note: The Gradle daemon can get stuck when KSP or Hilt process large codebases. Stopping it often resolves mysterious build failures.
- Check CLAUDE.md for architecture details
- Review existing modules for patterns and examples
- Open an issue for discussion before major changes
By contributing, you agree that your contributions will be licensed under the same license as the project.