Contributing to Grove
July 6, 2026 ยท View on GitHub
Thanks for your interest in contributing! Grove is free, open-source software built for everybody & any help is welcome, whether that's code, bug reports, or ideas.
Reporting Bugs & Requesting Features
Found something broken? Have an idea? You can reach out however works best for you:
- GitHub Issues - preferred for bugs and feature requests so they're tracked
- GitHub Discussions - great for open-ended ideas or questions
- Wherever - if you find another way to reach me, that's fine too
When reporting a bug, try to include:
- Your Android version and device
- Steps to reproduce the issue
- What you expected vs. what actually happened
- Logs or screenshots if you have them
Project Structure
A quick map of lib/ so new code ends up in the right place:
| Folder | Contains |
|---|---|
models/ | Plain data classes - HabitTree, RelapseEvent, the GrowthStage/HabitMode enums. No Flutter UI code here. |
painters/ | CustomPainter implementations - currently just the fractal tree renderer. |
providers/ | App state - GroveModel (habits, persistence) and GroveSettings (theme, layout, prefs), both ChangeNotifiers consumed via provider. |
screens/ | Full-page routes - home screen, habit detail screen. |
services/ | Platform/IO integrations - notifications, biometrics, the native widget bridge. |
theme/ | GroveTheme and the color/layout enums. |
widgets/ | Reusable UI pieces used within screens (cards, sheets, dialogs, calendar). |
l10n/ | Generated localization (.arb sources, see the translations section below). |
If you're not sure where something goes, look at what it's most similar to and put it next to that.
Contributing Code
- Open an issue first for anything significant, it avoids duplicate work and lets us align before you invest time writing code
- Fork the repo and create a branch from
main - Make your changes, keep commits focused and readable
- Test on a real device if possible, not just an emulator
- Open a pull request with a clear description of what you changed and why
Guidelines
- Follow the existing code style & when in doubt, match what's already there
- Keep pull requests scoped to one thing; big mixed PRs are hard to review
- Update the
CHANGELOG.mdunder[Unreleased]with any user-facing changes - Bump
pubspec.yamlonly if we've agreed on a version bump
Code Style
- State management is
provider+ChangeNotifier. Read app state withcontext.watch<T>()when the widget should rebuild on change, orcontext.read<T>()for one-off actions (e.g. inside a button'sonPressed). - Theming always goes through
GroveSettings.theme(aGroveTheme) please don't hardcode colors that should adapt to the active theme mode. Reuse the named constants onGroveTheme(mossGreen,clayRed, etc.) for non-theme-dependent accents. - All user-facing strings go through
AppLocalizations.of(context)!. Don't hardcode UI text, even for small or "obviously temporary" strings instead see the translations section below for adding the corresponding.arbentries. - Data models (
HabitTree,RelapseEvent) are immutable; updates go throughcopyWith(...), never by mutating fields directly. - In
services/, failures are caught and logged withdebugPrintrather than thrown & these run in contexts (platform channels, background callbacks) where an uncaught exception is worse than a silently skipped feature. Match that pattern rather than letting new service code throw.
Architecture Notes
A couple of spots that are easy to break without realizing it:
- Peak Sweep recompute (
GroveModel._sweepPeaks): any mutation that changes a habit's relapse history or start date (recording/removing a relapse, editing the start date) needs to flow through_sweepPeaksafterward soRelapseEvent.peakDaysstays correct. If you add a new way to mutate relapse data, route it through this rather than recomputing peaks ad hoc. - Widget bridge (
services/widget_bridge.dart):GroveWidgetBridgetalks to native code over aMethodChannelto update Android home-screen widgets. Outside of a real device/emulator (tests, etc.) the native side doesn't exist, so calls no-op via a caughtMissingPluginExceptionrather than failing. Don't remove that catch or assume the channel call always succeeds. HabitModebranching: habits are eitherHabitMode.abstain(streak resets on relapse) orHabitMode.checkIn(streak built from daily check-ins). These two modes branch separately ingrove_models.dart,grove_model.dart, andhabit_card.dart. If you add a feature touching streaks, stages, or stats, check that it makes sense โ and is implemented โ for both modes, not just the one you tested with.
Privacy & Telemetry Checklist
Before opening a PR, double check:
- No new dependency added to
pubspec.yamlmakes network calls, phones home, or includes analytics/crash-reporting SDKs - No new persistence mechanism introduced,
shared_preferencesis the only storage layer Grove uses today. If your feature seems to need a database or cloud sync, open an issue to discuss it first rather than building it into a PR - Nothing added requires an internet connection for core functionality to keep working offline
What's welcome
- Bug fixes
- Performance improvements
- Accessibility improvements
- New features that fit Grove's theme (habit/sobriety tracking, the fractal tree system, etc)
- New language translations
- Improvements to existing translations
What to check first
- There's no open issue or PR already covering your change
- The change aligns with Grove's philosophy: offline-first, no tracking, no ads
Project Philosophy
Grove is FOSS software built for people, not profit. Contributions should respect that:
- No telemetry, analytics, or data collection of any kind
- No external dependencies that phone home
- Privacy is non-negotiable
Adding or Updating Translations
Grove welcomes translation contributions. If you'd like to add support for a new language or improve an existing translation, follow these steps:
Adding a New Language
-
Locate the localization files in
lib/l10n/ -
Copy
app_en.arband rename it using the appropriate language code, for example:app_es.arbfor Spanishapp_fr.arbfor Frenchapp_de.arbfor German
-
Translate all string values while keeping:
- Placeholder names unchanged (
{count},{name}, etc.) - Metadata entries (
@key) intact - Formatting and punctuation consistent where appropriate
- Placeholder names unchanged (
-
Register the new locale in two places... an
.arbfile alone isn't enough to make a language selectable in the app:-
lib/app.dartadd it to the_supportedLocaleslist onGroveApp:static const _supportedLocales = [ Locale('en'), // ...existing locales... Locale('xx'), // your new locale ]; -
lib/screens/home_screen.dartadd an entry to the_languageOptionsmap so it shows up in the in-app language picker, using a flag emoji and the language's native name (matching the existing style):static const _languageOptions = <String, Locale?>{ // ...existing entries... '๐ฝ๐ฝ Native Name': Locale('xx'), };
If your language needs a country code to disambiguate (like
zh_TW), pass it as the secondLocale()argument in both places, e.g.Locale('zh', 'TW'). -
-
Generate localization files:
flutter gen-l10n
-
Run the app and verify:
- The language appears correctly when selected
- Text fits within the UI
- No untranslated strings remain
Updating Existing Translations
- Keep translations natural and contextually accurate rather than translating word-for-word.
- Maintain consistency with existing terminology throughout the app.
- Test any modified translations before submitting a pull request.
Translation Guidelines
- Use clear, natural language.
- Avoid machine-generated translations without review.
- Preserve placeholders and variables exactly as written.
- Keep accessibility and readability in mind.
If you're unsure about a translation, feel free to open a discussion before submitting a pull request.
Development Setup
Grove pins an exact Flutter version in pubspec.yaml (currently 3.41.2, see the environment: block) rather than a range. This isn't a typo, it keeps everyone's generated code (flutter gen-l10n, build output) consistent and avoids "works on my machine" issues from contributors on a different Flutter version. Please match it rather than using whatever Flutter you already have installed, especially before reporting a build issue.
-
Check your installed version:
flutter --version. If it doesn't match the version pinned inpubspec.yaml, you have a couple of options:-
FVM (recommended) it lets you install and switch between Flutter versions per-project without touching your global install:
dart pub global activate fvm fvm install 3.41.2 fvm use 3.41.2 fvm flutter pub get fvm flutter run -
Manual install clone the Flutter SDK and check out the matching tag/commit for that version, then add it to your
PATH(or point your IDE's Flutter SDK path at it) instead of your existing install.
-
-
Clone your fork:
git clone https://github.com/YOUR_USERNAME/Grove.git -
Install dependencies:
flutter pub get(orfvm flutter pub getif using FVM) -
Install/select the right JDK - see below, this part seems to trip up more people than the Flutter version does
-
Accept the Android SDK licenses - see below
-
Run on a connected device or emulator:
flutter run(orfvm flutter run)
If pubspec.yaml's pinned version is ever bumped, that'll be called out in the PR that does it - if you maintain a fork or long-running branch, it's worth re-checking this section after pulling main.
Some of Grove's dependencies report newer versions being available when you run pub get (e.g. flutter pub outdated). You don't need to act on these yourself, they're usually held back by a constraint (often another package, or a major/minor bump that could break something) and get bumped intentionally once that's sorted out.
JDK Version
Building the Android app requires JDK 21 (e.g. Eclipse Temurin 21). A mismatched JDK is the single most common build failure, and shows up as a Gradle configuration error rather than anything obviously Java-related, so it's easy to misread.
Once JDK 21 is installed, Gradle needs to actually be pointed at it:
- Command line: set
JAVA_HOMEto your JDK 21 install directory before runningflutter run/flutter build. - Android Studio:
Settings โ Build, Execution, Deployment โ Build Tools โ Gradle โ Gradle JDK, and pick the JDK 21 entry.
After changing this, do a clean build (flutter clean then rebuild) rather than an incremental one, stale Gradle caches can otherwise keep using the old JDK.
Android SDK Licenses
The build will also fail if the required Android SDK/NDK license hasn't been accepted, with an error like:
Failed to install the following Android SDK packages as some licences have not been accepted.
Fix this with:
sdkmanager --licenses
Don't run this with sudo if your SDK install is user-owned, doing so can write the accepted licenses to root's home directory instead of the SDK's licenses/ folder, so Gradle still won't see them accepted even though sdkmanager reported success. If your SDK directory itself is root-owned (common on some Linux package-manager installs) and you can't chown it, either:
-
run
sdkmanager --licenses --sdk_root=/path/to/your/sdkexplicitly withsudo, or -
switch to a user-owned SDK entirely:
mkdir -p ~/Android/Sdk export ANDROID_SDK_ROOT=~/Android/Sdk export ANDROID_HOME=~/Android/Sdk(add those exports to your shell profile so they persist), then re-run
sdkmanager --licenses.
If you're still stuck
If a build still won't succeed locally after checking the above, forking the repo and letting GitHub Actions build it is a reliable fallback: push to your fork, let the workflow run, then grab the APK from the build artifacts. You'll need to sign it yourself (optional) afterward with apksigner/jarsigner before installing it, since CI-built APKs aren't pre-signed.
License
By contributing, you agree that your contributions will be licensed under the same GPL-3.0 License that covers this project.