diff --git a/.fvmrc b/.fvmrc index c6c5344..ab8148c 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.41.3" + "flutter": "3.47.5" } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..280992f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,78 @@ +name: ci + +# Runs in the Flutter Base template and in every project created from it. +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +env: + # Use the SDK installed by flutter-action instead of the FVM link (.fvm/), + # which only exists on developer machines. + MELOS_SDK_PATH: auto + +jobs: + verify: + name: Format, analyze and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Read the pinned Flutter version + id: fvm + run: echo "version=$(jq -r '.flutter' .fvmrc)" >> "$GITHUB_OUTPUT" + + - uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: ${{ steps.fvm.outputs.version }} + cache: true + + - name: Install Melos + run: dart pub global activate melos + + - name: Bootstrap the workspace + run: melos bootstrap + + - name: Format, analyze and test + run: melos run verify + + build-android: + name: Build Android (debug) + runs-on: ubuntu-latest + needs: verify + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Read the pinned Flutter version + id: fvm + run: echo "version=$(jq -r '.flutter' .fvmrc)" >> "$GITHUB_OUTPUT" + + - uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: ${{ steps.fvm.outputs.version }} + cache: true + + # compileSdk 37 (android/build.properties) is newer than the runner image. + - name: Install Android SDK platform 37 + run: yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install "platforms;android-37.0" > /dev/null + + - name: Install Melos + run: dart pub global activate melos + + - name: Bootstrap the workspace + run: melos bootstrap + + - name: Build the dev flavor + working-directory: app + run: flutter build apk --debug -t lib/main/env/main_dev.dart --dart-define-from-file=env/.dev diff --git a/.github/workflows/sonar-qube-scann.yml b/.github/workflows/sonar-qube-scann.yml index 0d0468d..931ac5b 100644 --- a/.github/workflows/sonar-qube-scann.yml +++ b/.github/workflows/sonar-qube-scann.yml @@ -1,10 +1,14 @@ name: sonarqube -# ──────────────────────────────────────────────────────────────── -# CI TRIGGERS -# · push on main → historical baseline -# · pull_request PRs → quality gate before merge -# ──────────────────────────────────────────────────────────────── +# Project-only automation. It is skipped in the Flutter Base template and runs +# in projects created from it: the `gate` job reads flutter_base.json, and +# `melos run init` switches its "state" from "template" to "initialized". +# +# Required repository secrets once the project is initialized: +# SONAR_TOKEN SonarQube project token +# SONAR_URL SonarQube server URL +# Optional: +# SSH_PRIVATE_KEY only needed for git-based pub dependencies on: push: branches: [main] @@ -12,56 +16,67 @@ on: types: [opened, synchronize, reopened] jobs: - sonarQubeTrigger: - name: Sonarqube Trigger + gate: + name: Check repository state runs-on: ubuntu-latest + outputs: + initialized: ${{ steps.state.outputs.initialized }} + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: flutter_base.json + sparse-checkout-cone-mode: false + + - name: Read flutter_base.json + id: state + run: | + state=$(jq -r '.state' flutter_base.json) + echo "Repository state: $state" + if [ "$state" = "initialized" ]; then + echo "initialized=true" >> "$GITHUB_OUTPUT" + else + echo "initialized=false" >> "$GITHUB_OUTPUT" + echo "::notice::Template repository: SonarQube is disabled until 'melos run init' initializes a project." + fi + sonarqube: + name: SonarQube scan + needs: gate + if: needs.gate.outputs.initialized == 'true' + runs-on: ubuntu-latest + env: + MELOS_SDK_PATH: auto + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} steps: - # 1 — Checkout the repo - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - # 2 — SSH agent for any Git-based pub dependencies - - name: Start ssh-agent + - name: Start ssh-agent for git-based pub dependencies + if: env.SSH_PRIVATE_KEY != '' uses: webfactory/ssh-agent@v0.9.0 with: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - # 3 — Read the pinned Flutter version from .fvmrc - - name: Read Flutter version + - name: Read the pinned Flutter version id: fvm run: echo "version=$(jq -r '.flutter' .fvmrc)" >> "$GITHUB_OUTPUT" - # 4 — Install Flutter SDK at the pinned version - - name: Set up Flutter - uses: subosito/flutter-action@v2 + - uses: subosito/flutter-action@v2 with: channel: stable flutter-version: ${{ steps.fvm.outputs.version }} + cache: true - # 5 — Install Melos - name: Install Melos run: dart pub global activate melos - # 6 — Resolve workspace dependencies - - name: Bootstrap workspace + - name: Bootstrap the workspace run: melos bootstrap - # 7 — Static analysis - - name: Analyze - run: melos run analyze - - # 8 — Run tests (each package) - - name: Run tests - run: melos exec --dir-exists=test -- flutter test - - # 9 — Install SonarScanner CLI (needed by full_coverage.py) - - name: Setup Sonarqube Scanner + - name: Set up SonarScanner uses: warchant/setup-sonar-scanner@v8 - # 10 — Generate coverage & run SonarQube - - name: Generate coverage & run SonarQube + - name: Generate coverage and run SonarQube run: python3 coverage/full_coverage.py --ci env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_URL: ${{ secrets.SONAR_URL }} + SONAR_HOST_URL: ${{ secrets.SONAR_URL }} diff --git a/CLAUDE.md b/CLAUDE.md index bbc341e..fd2557d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,13 +6,21 @@ A **reusable company template**, not a product. Teams clone it to start new Flut here is inherited by future projects, so optimize for reuse and clear boundaries, and keep product-specific assumptions out of the base modules. -If you are working in a **project cloned from this template**, the same rules apply, but the example -features (auth, onboarding, home) and the placeholders are yours to replace. -See [docs/development/bootstrap-customization.md](docs/development/bootstrap-customization.md). +### Template or project? Check `flutter_base.json` + +`flutter_base.json` → `"state"` says which one you are in: + +- **`"template"`**: this is the Flutter Base itself. Keep project-specific values (real app names, bundle IDs, + endpoints, secrets) out of it. Project-only automation (the SonarQube workflow) is switched off. +- **`"initialized"`**: a project created from the base with `melos run init`. The example features (auth, + onboarding, home) and the remaining manual placeholders are yours to replace. + +Never edit `flutter_base.json` or the identity values by hand to switch state. The lifecycle, parameters and +manual steps are in [docs/development/project-initialization.md](docs/development/project-initialization.md). ## Architecture in one screen -A Dart pub workspace, managed with Melos 7, with one Flutter app and three local path packages, layered like this: +A Dart pub workspace, managed with Melos 8, with one Flutter app and three local path packages, layered like this: ``` app ──► domain ──► common @@ -28,6 +36,8 @@ app ──► domain ──► common | `modules/data/` | Data access | Repository **implementations**, Dio `NetworkConfig`, interceptors, `Preferences` (shared_preferences) | | `modules/common/` | Shared utilities with no feature knowledge | `ResultType`, `Resource`, `Failure`, platform/permission abstractions, analytics interface, validators | +`tool/project_init/` is a fifth workspace member: the pure-Dart project initializer (tooling, not an app layer). + - **State management:** `flutter_bloc` Cubits, which live in `domain` (not in `app`). Async screens use `BaseCubit`, which emits `Resource` (`RLoading`/`RSuccess`/`RError`). - **Result flow:** repository returns `Future>` (`TSuccess`/`TError`) → service → cubit → `Resource` → widget via `BlocBuilder`. @@ -37,19 +47,23 @@ app ──► domain ──► common - **Persistence:** the `Preferences` interface over `SharedPreferences` (in `data`). - **Errors:** the sealed `Failure` hierarchy plus the `DioException.toFailure()` mapper (in `common`). - **Flavors:** `dev` / `qa` / `prod` entrypoints + `flutter_dotenv`. Read [docs/architecture/overview.md § Environments](docs/architecture/overview.md#environments-and-flavors) before touching env handling. It has known inconsistencies. +- **Project identity** (name, package, bundle IDs) is kept in a few files the initializer owns: `flutter_base.json`, + `app/android/build.properties`, `app/ios/Flutter/AppIdentity.xcconfig` (see the repository map). Deeper docs: - [docs/architecture/overview.md](docs/architecture/overview.md): layers, data flow, DI, navigation, env - [docs/architecture/modules.md](docs/architecture/modules.md): **module boundaries, dependency rules, creating a module** - [docs/development/feature-guide.md](docs/development/feature-guide.md): how to add a feature end to end - [docs/development/testing.md](docs/development/testing.md): testing strategy and commands -- [docs/development/bootstrap-customization.md](docs/development/bootstrap-customization.md): what to change in a new project +- [docs/development/project-initialization.md](docs/development/project-initialization.md): **template vs project, `melos run init`, toolchain requirements** +- [docs/development/bootstrap-customization.md](docs/development/bootstrap-customization.md): what to keep, customize or replace in a new project - [docs/architecture/known-issues.md](docs/architecture/known-issues.md): verified defects and tech debt. **Read it before "fixing" something that looks wrong.** ## Repository map ``` -app/ Flutter application (package name: app) +flutter_base.json repository state (template | initialized) + current identity. Written only by the initializer +app/ Flutter application (Dart package `app` in the template; init renames it) lib/main.dart prod entrypoint lib/main/env/ main_dev.dart, main_qa.dart, env_config.dart (Flavor, FlavorConfig, Environment) lib/main/init.dart composition root: dotenv load, GetIt registration, runApp @@ -64,13 +78,17 @@ app/ Flutter application (package name: app) resources/ Dimen, Images (part files of resources.dart), locale/*.arb + generated/ env/ dotenv files bundled as assets (.dev, .env.example) test/ app tests - android/ ios/ web/ linux/ platform projects + android/build.properties Android identity (applicationId, namespace, label) + SDK levels + ios/Flutter/AppIdentity.xcconfig iOS identity (APP_BUNDLE_ID, APP_DISPLAY_NAME). Flavor xcconfigs: ios/dev.xcconfig, ios/qa.xcconfig + android/ ios/ web/ linux/ platform projects (iOS uses Swift Package Manager, no CocoaPods) modules/domain/lib/ bloc/, services/, repositories/ (interfaces), models/, env/, init.dart modules/data/lib/ repositories/ (impls), network/, preferences/, data_sources/ (placeholders), init.dart modules/common/lib/ core/, devices/, analytics/, ui/, validators/, init.dart +tool/project_init/ the initializer (bin/init.dart, lib/src/, tests that initialize a copy of this repo) pubspec.yaml (root) pub workspace root (`workspace:` members) + the `melos:` scripts; one shared pubspec.lock .fvmrc pinned Flutter SDK version (FVM). CI reads it too -.github/workflows/ sonar-qube-scann.yml: analyze + tests + coverage/SonarQube on every PR and push to main +.github/workflows/ ci.yml: format + analyze + test + Android build (template and project) + sonar-qube-scann.yml: coverage + SonarQube, project-only (skipped in the template) coverage/full_coverage.py multi-package LCOV merge + SonarQube upload sonar-project.properties SonarQube config (placeholders) addModule.py pulls a module from rootstrap/flutter-modules (see known-issues) @@ -78,25 +96,30 @@ addModule.py pulls a module from rootstrap/flutter-modules (see kno ## Commands -Prerequisites: FVM (`dart pub global activate fvm`, then `fvm install` installs the Flutter version pinned in -`.fvmrc`, 3.41.3) and Melos 7 (`dart pub global activate melos`). Packages require Dart `>=3.6.0` and Flutter `>=3.41.0`. -Run Flutter through FVM (`fvm flutter …`) so you use the pinned SDK. The commands below say `flutter` for brevity. +Toolchain: Flutter **3.47.5** (Dart **3.13.4**) pinned in `.fvmrc` (`dart pub global activate fvm`, then `fvm install`), +Melos **8.9** (`dart pub global activate melos`), JDK **17**, Android SDK platform **37** (compileSdk; install it with +`sdkmanager "platforms;android-37.0"`), and Xcode with the iOS 15+ SDK. CocoaPods is not needed. Run Flutter through +FVM (`fvm flutter …`) so you use the pinned SDK; the commands below say `flutter` for brevity. | Task | Command (from repo root unless noted) | |---|---| +| Initialize a project (template only) | `melos run init` (prompts), or `dart tool/project_init/bin/init.dart --name "My App" --package-name my_app --bundle-id com.company.myapp`. Add `--dry-run` to preview. | | Install deps for the whole workspace | `melos bootstrap` | -| Check toolchain | `melos doctor` | -| Static analysis (CI gate) | `melos run analyze` (runs `dart analyze . --fatal-infos` per package) | -| Format check | `melos run format` (runs `dart format --set-exit-if-changed .` per package, and fails on unformatted code) | -| Analyze + format | `melos run lint:all` | -| Regenerate l10n after editing `.arb` | `cd app && dart run intl_utils:generate` | -| build_runner | `melos run pub:runner`. No generators are configured today (see known-issues #12). | +| Everything CI checks | `melos run verify` (format, analyze, test) | +| Static analysis | `melos analyze` (same as `melos run analyze`: `dart analyze . --fatal-infos` per package) | +| Format (fails if a file changed) | `melos format` (same as `melos run format`) | +| Test | `melos run test` (`flutter test` in Flutter packages, `dart test` in pure-Dart ones) | +| Regenerate l10n after editing `.arb` | `melos run gen:l10n` | +| build_runner | `melos run pub:runner` (only packages that depend on build_runner; none use generators today) | | Run (dev) | `cd app && flutter run -t lib/main/env/main_dev.dart --dart-define-from-file=env/.dev` | -| Run web | `melos run run:web` (uses the **prod** entrypoint `lib/main.dart` with `env/.dev`) | -| Test (CI gate) | `melos exec --dir-exists=test -- flutter test` (runs every package that has a `test/` directory) | +| Run web (dev) | `melos run run:web` | +| Check toolchain | `melos doctor` | | Coverage + Sonar | `python3 coverage/full_coverage.py --dry-run` (drop `--dry-run` to execute; `--ci` for non-interactive) | -| Build Android | `cd app && flutter build appbundle -t lib/main.dart --dart-define-from-file=` | -| Build iOS | `cd app && flutter build ipa --release -t lib/main.dart --dart-define-from-file=` | +| Build Android | `cd app && flutter build apk --debug -t lib/main/env/main_dev.dart --dart-define-from-file=env/.dev` (release: `flutter build appbundle -t lib/main.dart --dart-define-from-file=`) | +| Build iOS | `cd app && flutter build ios --simulator --debug -t lib/main/env/main_dev.dart --dart-define-from-file=env/.dev` (release: `flutter build ipa --release -t lib/main.dart --dart-define-from-file=`) | + +`analyze`, `format` and `test` are root scripts that shadow Melos's built-in commands of the same name, so +`melos analyze` and `melos run analyze` run the same thing. Don't make such a script call the built-in: it recurses. Flavors: iOS has `Dev`/`QA`/`Runner` schemes (`--flavor dev|qa` works on iOS). Android has **no** `productFlavors`, so select the environment with `-t ` only. Details are in overview.md. @@ -124,6 +147,9 @@ Flavors: iOS has `Dev`/`QA`/`Runner` schemes (`--flavor dev|qa` works on iOS). A 9. **Don't introduce a second pattern** (Riverpod, Provider-only state, another HTTP client, another DI container, freezed/json_serializable) without an explicit architecture decision. 10. **Don't edit generated or platform-generated files** (see below). + Identity files (`flutter_base.json`, `android/build.properties` identity keys, `ios/Flutter/AppIdentity.xcconfig`) + are changed by the initializer. If you change a template file the initializer edits, update + `tool/project_init/lib/src/plan.dart` too; its tests fail if the two drift apart. 11. **Behavior changes need tests.** See [testing.md](docs/development/testing.md). Only `common` and `domain` have a few, so create the package's `test/` directory as the guide describes rather than skipping. CI picks it up automatically. 12. **Report pre-existing problems; don't silently fix them** in unrelated changes. Record them in @@ -133,6 +159,7 @@ Flavors: iOS has `Dev`/`QA`/`Runner` schemes (`--flavor dev|qa` works on iOS). A - `app/lib/presentation/resources/locale/generated/**`: intl_utils output. Edit the `.arb` files and regenerate. - `**/generated_plugin_registrant.*`, `app/linux/flutter/generated_*`, `ios/Flutter/Generated.xcconfig`: Flutter tool output. - `pubspec.lock` (a single workspace lock at the root, git-ignored), `.dart_tool/`, `.fvm/`, `build/`, `coverage/lcov*.info`. +- `flutter_base.json`: written by the initializer only. - If you add build_runner generators: `*.g.dart`, `*.freezed.dart`, `*.mocks.dart` (already excluded in Sonar and coverage). ## Before implementing @@ -148,18 +175,16 @@ Flavors: iOS has `Dev`/`QA`/`Runner` schemes (`--flavor dev|qa` works on iOS). A ## Definition of done -- [ ] `melos run format` passes (run `dart format .` in the package to fix) -- [ ] `melos run analyze` passes (it is `--fatal-infos`, so infos fail too) -- [ ] `melos exec --dir-exists=test -- flutter test` passes -- [ ] `.arb` edited in all locales and `dart run intl_utils:generate` output committed +- [ ] `melos run verify` passes (format, analyze with fatal infos, test). `format` rewrites files, so commit the result +- [ ] In the template, `flutter_base.json` still says `"template"` and no project-specific values were added +- [ ] `.arb` edited in all locales and `melos run gen:l10n` output committed - [ ] New code registered in the right `init.dart`; no forbidden imports (the rules in § Rules and modules.md) - [ ] App still launches on the dev entrypoint; `flutter build` succeeds for platforms touched - [ ] Docs updated if you changed a boundary, command, extension point, or env handling - [ ] PR follows `.github/pull_request_template.md` (description, issue link, preview) -CI (`.github/workflows/sonar-qube-scann.yml`) is meant to run analyze, the tests and coverage/SonarQube on -every PR, but it currently fails before any of them run (missing `SSH_PRIVATE_KEY` secret). It also has no -format or build step. Run the whole checklist locally (known-issues #3). +CI (`.github/workflows/ci.yml`) runs `melos run verify` and an Android debug build on every PR, in the template and +in projects. iOS is not built in CI, so build it locally when you touch iOS or shared native configuration. ## Delivery system diff --git a/README.md b/README.md index f7d6633..f2b8fca 100644 --- a/README.md +++ b/README.md @@ -1,277 +1,261 @@ -[![License](https://img.shields.io/github/license/rootstrap/ios-base.svg)](https://github.com/rootstrap/flutter-base/blob/master/LICENSE.md) - # Flutter Base Template -Flutter base is a boilerplate project created by Rootstrap for new projects using Flutter. The main -objective is helping any new projects jump start into feature development by providing a handful of -functionalities. +Flutter Base is Rootstrap's starting point for new Flutter apps. It gives a new project a working architecture, +tooling and CI from day one, so the team can start on features right away. -## Documentation +This repository is a **template**, not a product. You don't develop in it directly: you create a repository from it +and run one command (`melos run init`) that turns the copy into your project. The example features (auth, +onboarding, home, splash) are there to show the patterns. Replace them with your own. + +## What's included -- [CLAUDE.md](CLAUDE.md): the entry point for AI agents and a quick reference for developers (architecture, commands, rules, definition of done) -- [Architecture overview](docs/architecture/overview.md) · [Module guide](docs/architecture/modules.md) · [Known issues](docs/architecture/known-issues.md) -- [Feature development](docs/development/feature-guide.md) · [Testing](docs/development/testing.md) · [Bootstrap customization](docs/development/bootstrap-customization.md) +- **Architecture:** a Dart pub workspace with one app and three packages (`domain`, `data`, `common`) and enforced + dependency direction. See [modules.md](docs/architecture/modules.md). +- **State management:** `flutter_bloc` Cubits, with `BaseCubit` and a `Resource` loading/success/error state. +- **Dependency injection:** GetIt, registered per package. +- **Networking:** a Dio client with an auth-token interceptor and failure mapping. +- **Navigation:** go_router, with auth redirects and deep links (app_links). +- **Environments:** `dev` / `qa` / `prod` entrypoints and flutter_dotenv files. +- **Localization:** intl + intl_utils (English and Spanish). +- **Theming:** Material 3 light and dark themes. +- **Tooling:** Melos 8 scripts, the project initializer, FVM-pinned Flutter, and GitHub Actions CI with an optional + SonarQube scan. -Where this README and `docs/` disagree, `docs/` reflects the current code (see known-issues #15). +## Requirements -# Features +| Tool | Version | Install | +|---|---|---| +| Flutter / Dart | 3.47.5 / 3.13.4, pinned in `.fvmrc` | `dart pub global activate fvm`, then `fvm install` in the repo | +| Melos | 8.9+ | `dart pub global activate melos` | +| JDK | 17 | For Android builds | +| Android SDK | platform 37 | `sdkmanager "platforms;android-37.0"` | +| Xcode | with the iOS 15.0+ SDK | For iOS builds. CocoaPods is **not** needed: iOS plugins use Swift Package Manager | -This template comes with: +If your shell can't find `fvm` or `melos`, add `export PATH="$PATH":"$HOME/.pub-cache/bin"` to `~/.zshrc` or +`~/.bashrc`. -- Melos: Manage actions. -- Dependency injection (GetIt). -- HttpClient already configured for Rootstrap BE Projects(Dio). -- Theming setup. -- Navigation Router and DeepLinks config with go_router -- Intl. -- State Management (Blocs/Cubit). -- Env config and flavors. -- Chat with Gemini and Vertex AI (Documentation and setup WIP) -- GitWorkflow config: RS-GPT-Review -- GitWorkflow config: Sonarqube +Always run Flutter through FVM (`fvm flutter `), or add `alias flutter='fvm flutter'`, so everyone uses +the pinned SDK. Melos scripts already use it. -## Initial Setup +## Start a new project -1. Create a new repo using this template. +1. **Create your repository** from this template on GitHub ("Use this template"), and clone it. ![template](app/template.png) -2. Clone your new repo. -3. Install [FVM](https://fvm.app) (Flutter Version Management): -```text - dart pub global activate fvm -``` -4. Install the Flutter SDK version pinned in `.fvmrc`: -```text - fvm install -``` - This downloads the exact Flutter version the repo is pinned to and links it at `.fvm/flutter_sdk`. All contributors and CI use the same version. -5. Run Flutter commands through FVM: -```text - fvm flutter -``` - Optionally add a shell alias (`alias flutter='fvm flutter'`) so you can keep typing `flutter ...`. -6. Install [Melos](https://melos.invertase.dev/getting-started) 7.x globally: -```text - dart pub global activate melos -``` -7. Verify melos is on the path: `melos --version`. If your shell does not find the command, add `pub-cache` to `PATH` (e.g. in `~/.zshrc` or `~/.bashrc`): -```text - export PATH="$PATH":"$HOME/.pub-cache/bin" -``` -8. Bootstrap the workspace (downloads packages for every workspace member): -```text - melos bootstrap -``` -9. Run `melos doctor` to verify the setup. -> Melos 7 uses [Dart pub workspaces](https://dart.dev/tools/pub/workspaces). The workspace is declared in the root `pubspec.yaml`, each package sets `resolution: workspace`, and there is a single shared `pubspec.lock` at the repo root. +2. **Install the pinned Flutter version** from the repository root: -### Upgrading the pinned Flutter version + ```bash + fvm install + ``` -When the team agrees to move to a new Flutter version, run `fvm use ` at the repo root and commit the updated `.fvmrc`. CI reads the pinned version from `.fvmrc`, so the change propagates automatically. +3. **Initialize the project.** The command prompts for the app name, the Dart package name and the bundle ID, shows + every change, and asks for confirmation: -### IDE setup + ```bash + melos run init + ``` -- **VS Code**: settings are already wired in `.vscode/settings.json` — the Dart extension picks up `.fvm/flutter_sdk` automatically. -- **Android Studio / IntelliJ**: open `Preferences → Languages & Frameworks → Flutter` and set the SDK path to `/.fvm/flutter_sdk`. -10. Setup Android: - - Add to the build.properties file (and update when needed): -```text - flutter.versionName=1.0.0 - flutter.appId=base - flutter.versionCode=1 - flutter.compileSdkVersion=33 - flutter.minSdkVersion=21 - flutter.targetSdkVersion=33 -``` + To run it without prompts (scripts, CI, agents), call the script directly. Use this form whenever a value + contains spaces: -11. Android SignIn - - Create your release Key Store: + ```bash + dart tool/project_init/bin/init.dart --name "My App" --package-name my_app --bundle-id com.company.myapp + ``` -```text - keytool -genkey -v -keystore ~/keystore_name.jks -keyalg RSA -keysize 2048 -validity 10000 -alias your_alias" -``` + Add `--dry-run` to preview the changes. Init validates everything before writing, and it runs **only once**; after + that, change identifiers by hand. The parameters, rules and every file it edits are in + [project-initialization.md](docs/development/project-initialization.md). -- Create the 'key.properties' file with the keystore information: +4. **Install dependencies and check everything:** -```text - storePassword= - keyPassword= - keyAlias=> - storeFile= -``` + ```bash + melos bootstrap + melos run verify # format, analyze and test, as CI does + ``` -12. Add your env vars, create a config file for each env: - ![me](env_config_files.png) - - add the env config, i.e: +5. **Set up the environments** (see [Environments](#environments)). -```text - { - "API_URL": "https://dummyjson.com" - } -``` +6. **Run the app** (see [Run the app](#run-the-app)). + +7. **Finish the manual steps.** Init can't know these values: + - **Android release signing:** create a keystore and `app/android/key.properties`: + + ```bash + keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload + ``` -10. Setup iOs App Name and id: - - Locate the config file for each flavor and configure the FLUTTER_APP_NAME i.e: Debug.xcconfig -```text - FLUTTER_APP_ID=base.debug - FLUTTER_APP_NAME=RS Base Debug + ```properties + storePassword= + keyPassword= + keyAlias=upload + storeFile= + ``` + + Don't commit this file or the keystore. Without `key.properties`, release builds are signed with the **debug + key**, which the Play Store rejects. + - **iOS signing:** open `app/ios/Runner.xcworkspace` in Xcode and set your team and provisioning for the + `Runner` target. + - **Firebase** (optional): add the platform config files and uncomment `Firebase.initializeApp` in the + entrypoints. + - **SonarQube** (optional): add the `SONAR_TOKEN` and `SONAR_URL` repository secrets (see [CI](#ci)). + - **Branding:** app icons, launch screens, colors and fonts. See + [bootstrap-customization.md](docs/development/bootstrap-customization.md). + - **This README and a LICENSE** that fit your project. + +## Environments + +Each environment has an entrypoint and an env file in `app/env/`: + +| Environment | Entrypoint (`-t`) | Env file | +|---|---|---| +| dev | `lib/main/env/main_dev.dart` | `env/.dev` (committed, as an example) | +| qa | `lib/main/env/main_qa.dart` | `env/.qa` (create it) | +| prod | `lib/main.dart` | `env/.prod` (create it) | + +An env file looks like this: + +```properties +API_URL=https://your-api.example.com +ENV=dev ``` -## Set up an editor +Pass it with `--dart-define-from-file=env/.`. Its `ENV` value also selects which file flutter_dotenv loads, so +`ENV` must match the file name (`ENV=prod` in `env/.prod`). -- Follow the [Android Studio](https://docs.flutter.dev/get-started/editor?tab=androidstudio) - instructions to setup the editor +> **Everything in `app/env/` is bundled into the app** and can be read by anyone who has the binary. Never put real +> secrets there. Details and known inconsistencies: +> [overview.md § Environments](docs/architecture/overview.md#environments-and-flavors). -- Follow the [VS Code](https://docs.flutter.dev/get-started/editor?tab=vscode) instructions to setup - the editor +## Run the app -## Running the App +From `app/`: -1. Open a Simulator or Emulator -2. Open your project in your editor of preference +```bash +# dev +fvm flutter run -t lib/main/env/main_dev.dart --dart-define-from-file=env/.dev -**Note:** Starting with **Flutter 2.8** in order for you to launch the app in **Android** you must -define the `flutter.compileSdkVersion` inside the `local.properties` file. +# qa +fvm flutter run -t lib/main/env/main_qa.dart --dart-define-from-file=env/.qa +``` -You can read more about -this [here](https://docs.page/bizz84/complete-flutter-course/faq/android-build-gradle-issues). +- **iOS:** add `--flavor dev` or `--flavor qa` to use the `Dev`/`QA` schemes. Each installs with its own bundle ID + (`.debug.dev`, `.debug.qa`), so the environments can sit side by side on one device. +- **Android:** don't pass `--flavor`. Android has no product flavors, so the entrypoint alone selects the + environment, and every environment installs as `.debug`. +- **Web:** `melos run run:web` runs the dev environment in Chrome. -### Android Studio +**VS Code:** create a `.vscode/launch.json` (it is git-ignored) with one configuration per environment: -1. Add a **Run Configuration** - 1. Add new **Flutter** configuration - 2. Give it a meaningful name **IE:** Dev, QA, Staging, Prod - 3. Pick the entry point, main.dart file location **IE:** ``.../lib/main/env/main_dev.dart`` -2. Include any additional run arguments to launch the app. - 1. Create each env files config files in your root **app/** - ![me](env_config_files.png) - 2. Setup your build, add the env file to the build command in AE: - ![me](env_config.png) -3. Setup your env vars, i.e the api_url for each env: - ```text - { - "API_URL": "https://dummyjson.com" - } - ``` -4. Select the device to launch the App -5. Run the App - -### VS Code - -1. Go to **Run and Debug** section at the **Activity Bar** -2. At the top of the section expand the list and **Add Configuration** -3. Insert **Flutter Launch** configuration - 1. Update the environment name **(dev)** - 2. Update the launch program path **``/lib/main/env/main_dev.dart``** - 3. Update the **Flutter Mode** (debug, profile, release) - 4. Include any additional run argments to launch the app. - 1. Environment variables +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "dev", + "request": "launch", + "type": "dart", + "cwd": "app", + "program": "lib/main/env/main_dev.dart", + "toolArgs": ["--dart-define-from-file=env/.dev"] + } + ] +} +``` + +`.vscode/settings.json` already points the Dart extension at the FVM SDK. + +**Android Studio:** set the Flutter SDK path to `/.fvm/flutter_sdk` (Settings → Languages & Frameworks → +Flutter). Then add a Flutter run configuration per environment, with the entrypoint as "Dart entrypoint" and +`--dart-define-from-file=env/.dev` as "Additional run args". + +## Everyday commands - Add the env vars for each flavor with the property ``toolArgs`` - ![launch configuration example](app/vs-code-launch-configuration.png) +Run these from the repository root: -4. Inside the **Run and Debug** section select the environment you want to excute -5. Make sure you have the device you want to use already open -6. Run the App +| Task | Command | +|---|---| +| Install dependencies (whole workspace) | `melos bootstrap` | +| Everything CI checks: format, analyze, test | `melos run verify` | +| Static analysis (infos are fatal) | `melos run analyze` | +| Format check | `melos run format` (fix with `dart format .` in the package) | +| Tests | `melos run test` | +| Regenerate localization after editing `.arb` files | `melos run gen:l10n` | +| Check the toolchain | `melos run doctor` | -**Note 1:** Create as much **Launch Configurations** as you need for any specific environment. +A change is done when `melos run verify` passes and the app launches. The full checklist is in +[CLAUDE.md § Definition of done](CLAUDE.md#definition-of-done). -**Note 2:** You shouldn't commit the **``.vscode/launch.json``** file. +**Before you add code**, read these: +- [Feature guide](docs/development/feature-guide.md): add a feature end to end (repository → service → cubit → page). +- [Module guide](docs/architecture/modules.md): what goes in which package, and which imports are allowed. +- [Testing](docs/development/testing.md): what to test and how. +- [Known issues](docs/architecture/known-issues.md): verified defects. Read it before "fixing" something that looks + wrong. -## Build Production App: +Key conventions: +- Put user-facing strings in `app/lib/presentation/resources/locale/intl_*.arb` (all locales), then regenerate. + Never edit `locale/generated/`. +- Take spacing from `Dimen` and colors from the theme; don't hardcode them. +- Keep repository interfaces in `domain` and their implementations in `data`. Pages talk to cubits, not to + repositories. -The production entry point is `lib/main.dart`. Run the commands from `app/`. The env file passed to -`--dart-define-from-file` must define `ENV=prod`, because `init.dart` then loads `env/.prod` as the dotenv file. -Create `app/env/.prod` first (following `app/env/.dev`) and don't commit real secrets. +## Build for release -1. Build your android appBundle or apk: - - run the following command to build your appBundle +Run these from `app/`, after creating `env/.prod` (with `ENV=prod`) and setting up signing: - ```text - flutter build appbundle -t lib/main.dart --dart-define-from-file=env/.prod - ``` +```bash +fvm flutter build appbundle -t lib/main.dart --dart-define-from-file=env/.prod +fvm flutter build ipa --release -t lib/main.dart --dart-define-from-file=env/.prod +``` + +The version comes from `version:` in `app/pubspec.yaml`, for both platforms. - [TODO: add how to setup Xcode for apple signIn] -2. Configure your iOs app sigIn. - - run the following command to build your ipa +## CI - ```text - flutter build ipa --release -t lib/main.dart --dart-define-from-file=env/.prod - ``` +GitHub Actions runs on every pull request and every push to `main`: -For more information you can check the [docs](https://dartcode.org/docs/launch-configuration/) +- **`ci.yml`:** format, analyze and tests, plus an Android debug build. It runs in the template and in every project. +- **`sonar-qube-scann.yml`:** coverage and a SonarQube scan. It runs **only in initialized projects**: its `gate` job + reads `flutter_base.json` and skips the scan in the template. To enable it, add the repository secrets `SONAR_TOKEN` + and `SONAR_URL`, and review `sonar-project.properties`. `SSH_PRIVATE_KEY` is only needed if you add private git + dependencies. -## Packages +CI reads the Flutter version from `.fvmrc`. -- [GetIt](https://pub.dev/packages/get_it) For dependency injection. -- [Dio](https://pub.dev/packages/dio) A http client. -- [Blocs](https://pub.dev/packages/bloc) and [Cubit](https://pub.dev/packages/flutter_bloc) as State - management library. +## Upgrading Flutter -## Utilities +When the team agrees on a new version, run `fvm use ` at the repository root, update the `sdk`/`flutter` +constraints in the pubspecs if needed, check `melos run verify` and the platform builds, and commit the updated +`.fvmrc`. -- [intl](https://pub.dev/packages/intl) and [intl_utils](https://pub.dev/packages/intl_utils) for - localization. -- [flutter_svg](https://pub.dev/packages/flutter_svg) Svg Image loader. -- Auto generate translations files with [intl_utils](https://pub.dev/packages/intl_utils). - -- ```text - dart run intl_utils:generate - ``` -## Code Quality Standards - -In order to meet the required code quality standards, this project is following -this [tech guides considerations](https://github.com/rootstrap/tech-guides/blob/master/flutter/README.md) -. -It also runs [flutter analyze](https://dart.dev/tools/dart-analyze) for each build on your CI/CD -tool. - -## Security recommendations - -### Obfuscation - -TBD - -## CI/CD configuration with Bitrise (updated on Dec 12th 2021) - -We are using Bitrise to configure and run -the [CI/CD pipelines](https://www.notion.so/rootstrap/Flutter-CI-CD-9a0a5957ee8442908fc00c3ea8f49bf1) - -### Github Actions: RS-GPT-Review -- Configure GPT secrets vars on your repo settings: - - OPENAI_KEY -#### Note: The action will only run if the description or comments mentions @rs-gpt-review - -### Github Actions: Sonarqube -- Go to you sonarqube server and configure a new project. -- Configure the sonar-project.properties: - example: - ''' - sonar.projectKey=your-app-key - sonar.projectName=your-project-name - sonar.host.url=https://your-sonarqube-server.net - sonar.projectVersion=1.0 - sonar.sourceEncoding=UTF-8 - ''' -# Main source directories -sonar.sources=app/lib,modules/domain,modules/data,modules/common -sonar.dart.exclusions=pubspec.yaml -sonar.dart.analyzer.report.mode=LEGACY -- Configure Sonarqube secrets vars on your repo settings: - - SONAR_TOKEN (your sonarqube project token) - - SONAR_URL (your sonarqube server url) +## Contributing to the template -## License +These apply only when you change **this** repository, not a project created from it: -Flutter-Base is available under the MIT license. See the LICENSE file for more info. +- Keep it committed in the template state. `flutter_base.json` must say `"state": "template"`. Never commit the + result of running `melos run init`. +- Every change is inherited by future projects. Keep the base modules generic and free of product-specific names, + endpoints or rules. +- If you change a file the initializer edits, update `tool/project_init/lib/src/plan.dart` in the same change. If you + add or upgrade a dependency, the initializer tests may ask you to add package names to + `tool/project_init/lib/src/resolved_packages.dart`. See + [project-initialization.md § Maintaining the initializer](docs/development/project-initialization.md#maintaining-the-initializer). +- Follow the pull request template in `.github/pull_request_template.md`. + +## Documentation -**NOTE:** Remove the free LICENSE file for private projects or replace it with the corresponding -license. +- [CLAUDE.md](CLAUDE.md): quick reference for developers and AI agents (architecture, commands, rules, definition of + done). +- Architecture: [overview](docs/architecture/overview.md) · [modules](docs/architecture/modules.md) · + [known issues](docs/architecture/known-issues.md) +- Development: [project initialization](docs/development/project-initialization.md) · + [feature guide](docs/development/feature-guide.md) · [testing](docs/development/testing.md) · + [bootstrap customization](docs/development/bootstrap-customization.md) ## Credits -**Flutter Base** is maintained by [Rootstrap](http://www.rootstrap.com) with the help of +**Flutter Base** is maintained by [Rootstrap](https://www.rootstrap.com) with the help of our [contributors](https://github.com/rootstrap/flutter-base/contributors). -[](http://www.rootstrap.com) +[](https://www.rootstrap.com) diff --git a/app/.gitignore b/app/.gitignore index f419504..d0fb9d2 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -141,3 +141,7 @@ firebase-debug.log # Local development files local.properties *.properties + +# Tracked Android build configuration (the rules above would ignore it). +!android/build.properties +!android/gradle.properties diff --git a/app/analysis_options.yaml b/app/analysis_options.yaml index cd6fba3..a58a4df 100644 --- a/app/analysis_options.yaml +++ b/app/analysis_options.yaml @@ -3,6 +3,15 @@ # # Lint rules come from `package:flutter_lints/flutter.yaml`. To customize, add # entries under `linter.rules` below. +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - linux/** + # intl_utils output (regenerate with `melos run gen:l10n`, never edit by hand). + - lib/presentation/resources/locale/generated/** include: package:flutter_lints/flutter.yaml linter: diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index 7e83317..f4b22e9 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -1,17 +1,13 @@ plugins { id "com.android.application" - id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id "dev.flutter.flutter-gradle-plugin" } -def flutterProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - flutterProperties.load(reader) - } -} - +/** + * Project identity and SDK levels live in android/build.properties. + * The identity keys are written by the project initializer (`melos run init`). + */ def buildProperties = new Properties() def buildPropertiesFile = rootProject.file('build.properties') if (buildPropertiesFile.exists()) { @@ -20,6 +16,14 @@ if (buildPropertiesFile.exists()) { } } +def requiredBuildProperty = { String key -> + def value = buildProperties.getProperty(key) + if (value == null || value.trim().isEmpty()) { + throw new GradleException("Missing '$key' in android/build.properties") + } + return value.trim() +} + /** * Declare the env vars here * **/ @@ -36,70 +40,63 @@ if (project.hasProperty('dart-defines')) { } } -def flutterVersionCode = flutterProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = flutterProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') -if (keystorePropertiesFile.exists()) { - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +def releaseSigningConfigured = keystorePropertiesFile.exists() +if (releaseSigningConfigured) { + keystorePropertiesFile.withInputStream { keystoreProperties.load(it) } } android { - namespace 'com.rootstrap.base.flutter_base_rootstrap' - compileSdkVersion buildProperties.getProperty('flutter.compileSdkVersion').toInteger() - ndkVersion flutter.ndkVersion + namespace = requiredBuildProperty('flutter.namespace') + compileSdk = requiredBuildProperty('flutter.compileSdkVersion').toInteger() + ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } defaultConfig { - applicationId "com.rs." + buildProperties.getProperty('flutter.appId') - // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion buildProperties.getProperty('flutter.minSdkVersion') - targetSdkVersion buildProperties.getProperty('flutter.targetSdkVersion') - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName + applicationId = requiredBuildProperty('flutter.applicationId') + minSdk = requiredBuildProperty('flutter.minSdkVersion').toInteger() + targetSdk = requiredBuildProperty('flutter.targetSdkVersion').toInteger() + // Uses the version from pubspec.yaml. + versionCode = flutter.versionCode + versionName = flutter.versionName + // Added to (not replacing) the placeholders the Flutter plugin sets. + manifestPlaceholders.put('appLabel', requiredBuildProperty('flutter.appName')) } signingConfigs { - release { - keyAlias keystoreProperties['keyAlias'] - keyPassword keystoreProperties['keyPassword'] - storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null - storePassword keystoreProperties['storePassword'] + if (releaseSigningConfigured) { + release { + keyAlias = keystoreProperties['keyAlias'] + keyPassword = keystoreProperties['keyPassword'] + storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword = keystoreProperties['storePassword'] + } } } buildTypes { release { - signingConfig signingConfigs.release + // Signed with android/key.properties when present. Without it, the debug keys are + // used so `flutter build apk --release` works in the template and in CI. + signingConfig = releaseSigningConfigured ? signingConfigs.release : signingConfigs.debug } debug { - applicationIdSuffix ".debug" + applicationIdSuffix = ".debug" } } } -flutter { - source '../..' +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } } +flutter { + source = '../..' +} diff --git a/app/android/app/src/debug/AndroidManifest.xml b/app/android/app/src/debug/AndroidManifest.xml index 85cce84..399f698 100644 --- a/app/android/app/src/debug/AndroidManifest.xml +++ b/app/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,4 @@ - + - + - flutter_base_rootstrap + RS Base