Skip to content

Repository files navigation

iOS dependency manager: this project has been migrated from CocoaPods to Swift Package Manager (SPM). No pod install is required. All iOS plugins are resolved automatically via SPM on build.

SB-Template Flutter Logo

SB-Template Flutter

Version Flutter Dart License

Stop wasting time on boilerplate. Start building features.

A Flutter starter template with an opinionated design system, type-safe routing, reusable UI components, and pre-configured AI tooling. Clone it, initialise it for your project, and start building features on day one.


Table of Contents

  1. Overview
  2. Getting Started
  3. Project Structure
  4. Design System
  5. Routing
  6. Layouts
  7. Screens
  8. Widgets
  9. Helpers & Validators
  10. AI Tooling — CLAUDE.md & Workflows
  11. Deployment
  12. Versioning & Git Tags
  13. Dependencies
  14. De-templating Checklist — Using This as a Template
  15. Migration Notes & Known Toolchain Transitions

1. Overview

SB-Template Flutter is designed to provide a solid and opinionated starting structure for building new mobile applications. It ships with a pre-configured design system, type-safe routing, reusable UI components, helpers, and layouts so that developers can focus on building features rather than scaffolding.

The template is meant to be cloned and initialised for a specific project (via the Project Initialisation skill), progressively replacing placeholder screens and components with real ones while keeping the underlying conventions and tooling intact.

Target audience: Flutter developers who want a clean, consistent foundation without bikeshedding on folder structure, naming conventions, or design tokens.

This file (README.md) documents the template itself — its structure, conventions and tooling — for anyone studying or contributing to it. When you initialise a real project from this template, TEMPLATE.md is copied over README.md in its place: a ready-to-fill skeleton for documenting your app, minus the template-only sections (this Overview, De-templating Checklist, Migration Notes).


2. Getting Started

Prerequisites

  • Flutter SDK ≥ 3.44.0
  • Dart SDK ^3.11.3
  • Xcode (for iOS development)
  • Android Studio / Android SDK (for Android development)

Installation

Option 1: Use as GitHub Template (Recommended)

  1. Click "Use this template" on GitHub
  2. Clone your new repository:
git clone https://github.com/your-username/your-project.git
cd your-project

Option 2: Clone directly

git clone https://github.com/stefanoBid/sb-flutter-template.git my-project
cd my-project
rm -rf .git && git init

Project Initialisation

After cloning, trigger the Project Initialisation skill (e.g. say "inizializza il progetto" — see AI Tooling) to rename the project, update all config files, and reset the version to 1.0.0+1.

That skill covers app name, version and CLAUDE.md context. It does not touch the Android package id, iOS bundle id, app icons or store metadata — see De-templating Checklist for everything else to change before shipping under a new identity.

Then install dependencies and run the app:

flutter pub get
flutter run

Available Commands

Command Description
flutter run Run on connected device/emulator
flutter run -d ios Run on iOS simulator
flutter run -d android Run on Android emulator
flutter build ipa --release Build iOS IPA
flutter build appbundle --release Build Android App Bundle
flutter analyze Analyse code for issues
cider bump patch Bump patch version (1.0.0 → 1.0.1)
cider bump minor Bump minor version (1.0.0 → 1.1.0)
cider bump major Bump major version (1.0.0 → 2.0.0)
cider version X.Y.Z Set a specific version

3. Project Structure

This section shows the annotated directory tree of lib/. The project follows a feature-agnostic structure where each top-level folder has a single responsibility.

lib/
├── main.dart                        # App entry point
├── router.dart                      # GoRouter configuration (appRouter instance)
│
├── helpers/                         # Design system tokens and utilities
│   ├── app_colors.dart              # Adaptive colour palette
│   ├── app_design.dart              # Spacing, border radius, padding tokens
│   ├── app_image.dart               # Image type resolver and widget builder
│   ├── app_logger.dart              # Debug-only logger (stripped in release)
│   ├── app_router.dart              # Type-safe navigation layer (AppRouter)
│   ├── app_storage.dart             # Encrypted key-value storage singleton
│   ├── app_theme.dart               # ThemeData configuration
│   ├── app_typography.dart          # Text style scale
│   └── app_validation.dart          # Static form validators
│
├── layouts/                         # Reusable page-level layout scaffolds
│   ├── app_layout.dart              # Shell with bottom navigation bar
│   ├── app_bars/
│   │   ├── classic_app_bar.dart     # Gradient app bar with title and actions
│   │   └── transparent_app_bar.dart # Transparent overlay app bar
│   └── body/
│       ├── standard_page_layout.dart # Column layout: app bar + scrollable body
│       └── hero_page_layout.dart    # Full-bleed hero image + slide-up card body
│
├── models/                          # Data models
│   └── json_serializable.dart       # Base JSON serialization helpers
│
├── screens/                         # Feature screens
│   ├── home/                        # Home screen (bottom nav tab)
│   ├── form/                        # Form screen (bottom nav tab)
│   ├── profile/                     # Profile screen (bottom nav tab)
│   └── details/                     # Detail screen (pushed with path parameter)
│
├── services/                        # Business logic and API services
│
└── widgets/                         # Reusable UI components
    ├── base_badge.dart              # Status badge
    ├── base_bottom_sheet.dart       # Modal bottom sheet with drag handle
    ├── base_button.dart             # Primary action button
    ├── base_card.dart               # Image + text card
    ├── base_checkbox.dart           # Styled checkbox with label
    ├── base_dropdown.dart           # Single-select dropdown field
    ├── base_form_field.dart         # Form-integrated text field
    ├── base_icon_button.dart        # Icon-only button
    ├── base_image_container.dart    # Network / asset image with fade
    ├── base_image_picker.dart       # Tappable image picker with preview
    ├── base_image_selector_bottom_sheet.dart  # Bottom sheet: gallery / camera / remove
    ├── base_input.dart              # Standalone text input
    ├── base_multiselect.dart        # Multi-select field with chips
    ├── base_scaffold_messenger.dart # Themed SnackBar utility
    ├── base_value_card.dart         # Metric display card (value + label)
    └── group-container/
        ├── gc_list_view.dart        # Null-safe ListView.builder wrapper
        └── gc_grid_view.dart        # GridView.count wrapper with dimensions

4. Design System

The design system is entirely contained in lib/helpers/ and provides a single source of truth for colours, typography, spacing and border radius. Never use hardcoded values — always reference these helpers.

Colours — AppColors

AppColors is an adaptive class: instantiate it with AppColors.of(context) to get colours that automatically switch between light and dark mode.

final colors = AppColors.of(context);

colors.background   // page background
colors.surface      // card / input background
colors.text         // primary text
colors.muted        // secondary / disabled text
colors.bottomBar    // bottom navigation bar background

Static constants (non-adaptive, use directly):

Token Value Usage
AppColors.primary #60C9F8 Brand colour, active states
AppColors.secondary #0A599C Dark brand, dark-mode accents
AppColors.error #B00020 Validation errors
AppColors.success #10B981 Success states
AppColors.warning #F59E0B Warning states

Typography — AppTypography

Instantiate with AppTypography.of(context). All styles inherit the adaptive text colour from AppColors.

Style Size Weight Usage
heading1 28px Bold Main screen titles
heading2 22px Bold Section titles
heading3 18px SemiBold Subsection titles
heading4 16px SemiBold Card titles, list item titles
body 16px Regular Body text, paragraphs
bodyMedium 14px Regular Inputs, dense UI
bodySecondary 16px Regular Secondary / muted text
caption 12px Regular Labels, captions
small 11px Regular Badges, tiny labels
Text('Welcome', style: AppTypography.of(context).heading1)
Text('Details', style: AppTypography.of(context).bodySecondary)

Spacing & Radius — AppDesign

All tokens are static const — use them directly without instantiation.

Border radius:

Token Radius Usage
AppDesign.borderRadiusXXs 6px Small chips, tight UI
AppDesign.borderRadiusXs 10px Inputs, buttons, small cards
AppDesign.borderRadiusSm 20px Medium cards
AppDesign.borderRadiusMd 32px Large cards, image containers
AppDesign.borderRadiusLg 48px Full pill, bottom sheets

Top-only and bottom-only variants follow the same suffix pattern (e.g. borderRadiusTopSm, borderRadiusBottomMd).

Padding presets:

Token Description
AppDesign.paddingXs/Sm/Md/Lg/Xl Uniform padding on all sides
AppDesign.paddingSymmetricSm/Md/Lg Horizontal > vertical
AppDesign.paddingHorizontalSm/Md/Lg Horizontal only
AppDesign.paddingPage Standard page content padding

Icons — Material Icons

This project uses Flutter's built-in Material Icons (Icons.*). Do not use PhosphorIcons or any other external icon library — no extra import is needed beyond flutter/material.dart.

Prefer outlined variants for a lighter visual style (Icons.home_outlined, Icons.mail_outline). Use filled variants for active or selected states.

Icon(Icons.home)
Icon(Icons.mail_outline)
Icon(Icons.check_circle_outline)

5. Routing

Routing is powered by go_router. The template adds a type-safe navigation layer on top that prevents passing wrong parameters at compile time.

Key files

File Role
lib/router.dart GoRouter instance (appRouter) with all route registrations
lib/helpers/app_router.dart Type-safe AppRouter class — the only place consuming code should touch

AppRouter API

Never call context.go() directly in your screens. Use AppRouter instead:

// Navigate to a route with no parameters
AppRouter.goTo(context, AppRouter.home);
AppRouter.goTo(context, AppRouter.forms);
AppRouter.goTo(context, AppRouter.profile);

// Push a detail route with a typed path parameter
AppRouter.goDeep(context, AppRouter.details, params: DetailParams(detailId: '42'));

// Go back (pops if possible, otherwise navigates home)
AppRouter.goBack(context);

Defined routes

Constant Path Parameters
AppRouter.home /home none
AppRouter.forms /form none
AppRouter.profile /profile none
AppRouter.details /details/:detailId DetailParams(detailId)

Adding a new route

Step 1 — Define the params class (skip if no parameters are needed):

class RecipeParams extends GenericRouteParams {
  final String recipeId;
  const RecipeParams({required this.recipeId});

  @override
  Map<String, String> toPathParams() => {'recipeId': recipeId};
}

Step 2 — Add the constant in AppRouter (lib/helpers/app_router.dart):

static const recipe = AppTypedRoute<RecipeParams>('/recipe/:recipeId');

Step 3 — Register the GoRoute in lib/router.dart:

GoRoute(
  path: '/recipe/:recipeId',
  pageBuilder: (context, state) {
    final recipeId = state.pathParameters['recipeId']!;
    return CustomTransitionPage(
      key: state.pageKey,
      child: RecipeScreen(recipeId: recipeId),
      transitionsBuilder: _customTransitionBuilder,
      transitionDuration: const Duration(milliseconds: 150),
    );
  },
),

Transitions

The default transition is a fade (FadeTransition) defined by _customTransitionBuilder in router.dart. Bottom-nav tab routes use NoTransitionPage for an instant switch. Detail routes use the custom fade transition.


6. Layouts

Layouts are reusable page-level scaffolds in lib/layouts/. A screen should compose one layout rather than building its own Scaffold structure.

AppLayout

The root shell used by GoRouter's ShellRoute. Renders the bottom navigation bar with three tabs (Home, Forms, Profile). Pass withBottomNav: false for full-screen flows like detail screens.

StandardPageLayout

A column layout with an optional app bar slot and a body that fills the remaining space.

Prop Type Description
body Widget Required. The main content area.
appBar Widget? Optional app bar widget placed at the top.
hasPadding bool Apply AppDesign.paddingPage to body. Defaults to true.

HeroPageLayout

Full-bleed hero image at the top with a rounded card that slides up over it. Suited for detail screens.

Prop Type Description
imageUrl String Required. URL of the hero image.
body Widget Required. Content placed inside the scrollable card.
imageHeight double Hero image height. Defaults to 280.
onBack VoidCallback? Custom back button handler.

ClassicAppBar

A gradient app bar (primary → background) with title, optional leading widget, optional actions and an optional bottomContent slot for search bars or tabs.

Prop Type Description
title String? App bar title.
titleStyle TextStyle? Override default heading2 style.
leading Widget? Widget shown before the title.
actions List<Widget>? Widgets shown after the title.
bottomContent Widget? Content below the title row.

TransparentAppBar

An overlay app bar for use on top of hero images or full-bleed backgrounds. Fully transparent background.


7. Screens

Screens live in lib/screens/, organised by feature folder. Each folder should contain the screen file and, optionally, feature-specific widgets.

Conventions

  • One screen per file. File name: [feature]_screen.dart, class name: [Feature]Screen.
  • Screens are StatelessWidget unless local state is strictly necessary.
  • All layout is delegated to a layout from lib/layouts/ — screens do not build raw Scaffolds.
  • Navigation is always performed via AppRouter.

Available screens

Screen Path Description
HomeScreen /home Main landing tab
FormScreen /form Form examples tab
ProfileScreen /profile Profile tab
DetailsScreen /details/:detailId Detail view pushed with a detailId parameter

8. Widgets

All reusable widgets live in lib/widgets/. Widget names must describe what the widget is, not where it is used. All widgets use the design system tokens — never hardcoded values.

BaseButton

Full-featured action button with variants, loading state and optional icon.

Prop Type Description
label String? Button label text.
icon IconData? Optional icon (at least one of label/icon is required).
onPressed VoidCallback? Tap handler. null renders the button as disabled.
type BaseButtonType filled (default), outlined, or ghost.
color Color? Overrides the accent colour.
pill bool Rounded pill shape. Defaults to false.
fullWidth bool Expand to fill available width. Defaults to false.
isLoading bool Show loading spinner instead of content. Defaults to false.
tooltip String? Accessibility tooltip.
BaseButton(label: 'Submit', onPressed: _submit, isLoading: _isLoading)
BaseButton(icon: Icons.add, type: BaseButtonType.outlined, onPressed: _add)

BaseIconButton

Icon-only button with filled or outlined style.

Prop Type Description
icon IconData Required. Icon to display.
onPressed VoidCallback? Tap handler.
type IconButtonType filled (default) or outlined.
color Color? Background (filled) or border (outlined) colour.
iconColor Color? Icon colour override.
badgeCount int? Red notification badge with count.
tooltip String? Accessibility tooltip.

BaseBadge

Status or label badge with optional icon.

Prop Type Description
label String? Badge text.
icon IconData? Optional leading icon (at least one of label/icon is required).
style BadgeStyle Styling configuration (see below).

BadgeStyle props:

Prop Type Description
color Color? Background (filled) or border (outlined) colour.
foregroundColor Color? Icon and text colour.
size BadgeSize small (default) or normal.
variant BadgeVariant filled (default) or outlined.
borderRadius BorderRadiusGeometry Defaults to AppDesign.borderRadiusXXs.
BaseBadge(label: 'Active', style: BadgeStyle(color: AppColors.success))
BaseBadge(icon: Icons.star_border, style: BadgeStyle(variant: BadgeVariant.outlined))

BaseCard

Image + text card for lists and grids.

Prop Type Description
title String Required. Card title.
content String Required. Card subtitle / description.
imageUrl String Required. Network image URL.
width double Card width. Defaults to 220.
height double Card height. Defaults to 220.
padding EdgeInsetsGeometry Outer padding. Defaults to EdgeInsets.zero.
onTap VoidCallback? Tap handler.

BaseFormField

Form-integrated text field with label, validation and error display. Use inside a Form widget.

Prop Type Description
controller TextEditingController Required.
label String? Label displayed above the field.
hint String? Placeholder text.
prefixIcon IconData? Leading icon.
suffixIcon Widget? Trailing widget (e.g. visibility toggle).
obscureText bool Mask text (password). Defaults to false.
keyboardType TextInputType? Input type.
validator String? Function(String?)? Validation function.
autovalidateMode AutovalidateMode Defaults to AutovalidateMode.onUnfocus.
onChanged ValueChanged<String>? Called on every keystroke.
onFieldSubmitted ValueChanged<String>? Called on keyboard submit.

BaseInput

Standalone text field without form integration. Use for search bars or filters.

Prop Type Description
controller TextEditingController Required.
hint String? Placeholder text.
prefixIcon Widget? Leading widget.
suffixIcon Widget? Trailing widget.
onChanged ValueChanged<String>? Called on every keystroke.
fillColor Color? Background colour override.

BaseCheckbox

Styled checkbox with optional label. Tapping the entire row toggles the value.

Prop Type Description
value bool Required. Current checked state.
onChanged ValueChanged<bool> Required. Called when toggled.
label String? Optional label shown beside the checkbox.
fullWidth bool Expands the row to full width. Defaults to false.

BaseDropdown

Styled single-select DropdownButtonFormField for use inside a Form. For multi-select use BaseMultiselect.

Prop Type Description
initialValue T? Required. Currently selected value.
items List<BaseDropdownOption<T>> Required. Available options.
label String? Label displayed above the field.
hint String? Placeholder text.
prefixIcon IconData? Leading icon.
voidSelectionItemLabel String? Adds a null option at the top of the list.
disabled bool Disables interaction. Defaults to false.
isLoading bool Shows loading spinner. Defaults to false.
validator String? Function(T?)? Validation function.
onChanged ValueChanged<T?>? Called when selection changes.

BaseMultiselect

Styled multi-select field for use inside a Form. Opens an AlertDialog with checkboxes; selected values are shown as deletable chips. Uses BaseDropdownOption<T> — the same data class as BaseDropdown.

Prop Type Description
items List<BaseDropdownOption<T>> Required. Available options.
initialValues List<T> Currently selected values. Defaults to [].
label String? Label displayed above the field.
hint String? Placeholder text.
prefixIcon IconData? Leading icon.
disabled bool Disables interaction. Defaults to false.
isLoading bool Shows loading spinner. Defaults to false.
validator String? Function(List<T>?)? Validation function.
onChanged ValueChanged<List<T>>? Called when selection changes.

BaseImageContainer

Network or asset image with fade-in animation and optional darkening filter.

Prop Type Description
imageUrl String Required. URL or asset path.
filter ImageFilter ImageFilter.none (default) or .darken.
fit ImageFit ImageFit.cover (default) or .contain.
width double? Container width.
height double? Container height.
borderRadius BorderRadius Defaults to AppDesign.borderRadiusMd.
fadeDuration Duration Fade-in duration. Defaults to 300ms.

BaseValueCard

Compact metric display showing a value and a label.

Prop Type Description
value String Required. Main metric text (large).
label String Required. Description below the value.

BaseScaffoldMessenger

Static utility that shows a themed SnackBar anchored to the bottom of the screen.

BaseScaffoldMessenger.show(
  context,
  message: 'Saved successfully',
  type: SnackBarType.success,
);
Type Colour
SnackBarType.success AppColors.success
SnackBarType.error AppColors.error
SnackBarType.warning AppColors.warning
SnackBarType.info Primary (adaptive)

BaseBottomSheet

Static utility that shows a modal bottom sheet with an optional header. Never call showModalBottomSheet directly.

BaseBottomSheet.show(
  context,
  title: 'Title',        // optional
  subtitle: 'Subtitle',  // optional
  heightFactor: 0.5,     // optional — fraction of screen height (0, 1]
  child: MyContent(),
);

BaseBottomSheet.hide(context); // programmatic close

GcListView (group-container)

A null-safe ListView.builder wrapper. Items returned as null from itemBuilder are silently skipped.

Prop Type Description
itemBuilder Widget? Function(BuildContext, int) Required.
itemCount int Required. Negative values are treated as 0.
scrollDirection Axis Defaults to Axis.vertical.
padding EdgeInsetsGeometry? Defaults to EdgeInsets.zero.

GcGridView (group-container)

A GridView.count wrapper with a GridDimensions configuration object.

Prop Type Description
children List<Widget> Required. Grid items.
dimensions GridDimensions Grid configuration. Defaults to GridDimensions().

GridDimensions defaults: crossAxisCount: 2, childAspectRatio: 3/2, crossAxisSpacing and mainAxisSpacing use AppDesign.gapItemMd.

BaseImagePicker

Tappable image preview with a placeholder icon. Opens BaseImageSelectorBottomSheet on tap. The caller owns the image state.

Prop Type Description
imageUrl String? Current image path or URL. null shows the placeholder.
height double Container height. Defaults to 200.
onImageSelected ValueChanged<XFile?> Required. Called with the picked XFile or null on remove.
BaseImagePicker(
  imageUrl: _imageUrl,
  onImageSelected: (XFile? file) => setState(
    () => _imageUrl = file?.path,
  ),
)

BaseImageSelectorBottomSheet

Static utility that shows a bottom sheet with gallery / camera options and an optional remove action.

Prop Type Description
onImageSourceSelected void Function(ImageSource) Required. Called with the chosen source.
hasImage bool Show the Remove option. Defaults to false.
onRemove VoidCallback? Called when Remove is tapped.
BaseImageSelectorBottomSheet.show(
  context,
  onImageSourceSelected: (source) => _pickImage(source),
  hasImage: _imageUrl != null,
  onRemove: () => setState(() => _imageUrl = null),
);

9. Helpers & Validators

All helpers live in lib/helpers/ as flat files. The set of files in this folder is fixed — do not rename or reorganise them.

app_colors.dart

Exports AppColors. See Design System — Colours.

app_design.dart

Exports AppDesign. See Design System — Spacing & Radius.

app_image.dart

Exports AppImage — a static utility for resolving the source type of an image URL/path and building the appropriate widget.

final type = AppImage.getType(url); // → ImageType.network | .asset | .file
final widget = AppImage.buildImage(context, imageUrl: url, type: type, fit: BoxFit.cover);
ImageType URL prefix Widget rendered
network http:// or https:// CachedNetworkImage
asset starts with assets/ Image.asset
file any other path Image.file

app_typography.dart

Exports AppTypography. See Design System — Typography.

app_theme.dart

Exports the ThemeData used in MaterialApp. Edit this file to change the font family (uses google_fonts) or override Material component themes.

app_router.dart

Exports AppRouter, AppTypedRoute<P>, GenericRouteParams, NoParams, and built-in params classes (DetailParams). See Routing.

app_storage.dart

Exports AppStorage — an app-wide singleton for encrypted key-value storage backed by flutter_secure_storage. Uses Android EncryptedSharedPreferences and iOS Keychain.

await AppStorage.instance.write('token', value);
final token = await AppStorage.instance.read('token');
await AppStorage.instance.delete('token');

// JSON objects
await AppStorage.instance.writeObject('user', user, (u) => u.toJson());
final user = await AppStorage.instance.readObject('user', User.fromJson);
Method Description
read(key) Returns stored string or null
write(key, value) Stores a string value
delete(key) Removes the entry
readObject<T>(key, fromJson) Deserialises a JSON object or returns null
writeObject<T>(key, value, toJson) Serialises and stores a JSON object

app_validation.dart

Exports AppValidation — a collection of static validators for use inside TextFormField / BaseFormField validators.

Chain validators with ?? — the first failure wins:

validator: (v) => AppValidation.notEmpty(v) ?? AppValidation.email(v),
Method Description
notEmpty(v) Field must not be null or empty.
email(v) Must be a valid email address.
minLength(v, min) Minimum character length.
maxLength(v, max) Maximum character length.
match(v, other) Both values must be equal (e.g. confirm password).
numeric(v) Must contain only digits.
strongPassword(v) Must contain uppercase, lowercase and a digit.

All methods accept an optional message parameter to override the default error string.

app_logger.dart

Exports AppLogger — a debug-only logger gated behind kDebugMode. All output is automatically stripped in release and profile builds. Never use print() directly — always use AppLogger.

AppLogger.debug('User loaded', tag: 'HomeScreen');
AppLogger.warn('Token is about to expire');
AppLogger.error('Failed to fetch', error: e, stackTrace: st);
Method Level When to use
AppLogger.debug(message, {tag}) [D] General flow information
AppLogger.warn(message, {tag}) [W] Non-critical anomalies
AppLogger.error(message, {tag, error, stackTrace}) [E] Exceptions and failures

The optional tag parameter (e.g. tag: 'AuthService') prefixes the output for easier filtering in the console.


10. AI Tooling — CLAUDE.md & Workflows

This repository is built to be worked on with Claude Code. A single CLAUDE.md file at the project root gives the assistant full context: app description, assistant persona and response language, naming rules, the entire design system reference (colours, typography, spacing), navigation conventions, and screen/widget placement rules. It is versioned alongside the code and is the source of truth the AI follows on every task.

On top of that, a set of recurring maintenance tasks are packaged as on-demand skills under .claude/skills/. Each skill loads automatically when its trigger phrase is used in chat — no manual invocation needed.

Workflow Trigger phrase What it does
Project Initialisation "Inizializziamo il progetto" · "inizializza il progetto" · "reset del progetto" Collects username, project name and app context; renames the app across pubspec.yaml/main.dart/Android/iOS; resets version to 1.0.0+1; refreshes CLAUDE.md against the actual lib/ state
Full Project Checkup "checkup completo" · "checkup del progetto" · "controllo completo" Runs the three maintenance workflows below in sequence and produces one combined summary
Dependency Check & Update "controlla le dipendenze" · "check dependencies" Runs flutter pub outdated, auto-applies safe same-major bumps, lists breaking changes for manual review
Documentation Update "aggiorna la documentazione" · "update docs" Compares README.md against the actual codebase and rewrites it
Lint / Code Quality Check "check del progetto" · "il progetto è pulito?" · "fai un lint check" Runs dart fix, dart format and flutter analyze; auto-fixes what's safe, reports blocking errors
Version Bump "aggiornami il progetto alla versione X.Y.Z" Bumps the version with cider, proposes a CHANGELOG entry for approval, then applies it

Just type the trigger phrase (Italian or English both work) in a Claude Code chat inside this repository — the matching skill loads its instructions into the conversation automatically.


11. Deployment

iOS

Test distribution (TestFlight)

  1. Bump version/build with the Version Bump skill
  2. Build the release IPA: flutter build ipa --release
  3. Open build/ios/archive/Runner.xcarchive in Xcode Organizer
  4. Product → Archive → Distribute App → App Store Connect → TestFlight
  5. Invite internal or external testers from App Store Connect

Production release (App Store)

  1. Ensure version and build number are correct
  2. flutter build ipa --release
  3. Open build/ios/archive/Runner.xcarchive in Xcode Organizer → Distribute → App Store Connect
  4. Complete metadata, screenshots and review information on App Store Connect
  5. Submit for review
# 1. Clean Flutter build artifacts
flutter clean

# 2. Reinstall packages respecting the certified lockfile (master branch only)
flutter pub get --enforce-lockfile

# 3. Clean Xcode DerivedData cache
rm -rf ~/Library/Developer/Xcode/DerivedData

# 4. Clean and reinstall CocoaPods
cd ios
pod deintegrate
pod cache clean --all
pod install
cd ..

Android

Local Development & Testing

Running on emulator (development mode):

# Start an Android emulator first (from Android Studio or command line)
# Then run the app directly — Flutter handles build + install + launch automatically
flutter run

# Or target a specific device
flutter devices  # list available devices
flutter run -d <device-id>

Building and installing an APK manually:

When you need to build and install an APK without running the full development session:

# 1. Build debug APK (faster, includes debugging symbols)
flutter build apk --debug

# 2. Install on connected device/emulator using Flutter
# --debug is required: flutter install defaults to --release and will
# fail with "app-release.apk does not exist" if you only built debug
flutter install --debug

# 3. Launch the app manually from the device home screen or app drawer

The APK is saved at: build/app/outputs/flutter-apk/app-debug.apk

Building a release APK for testing:

# 1. Build release APK (optimised, no debugging symbols)
flutter build apk --release

# 2. Install on connected device/emulator
flutter install --release

# Or combine both steps using flutter run
flutter run --release

The release APK is saved at: build/app/outputs/flutter-apk/app-release.apk

Uninstalling the app:

# Uninstall from connected device/emulator
flutter install --uninstall-only

# Or target a specific device
flutter install --uninstall-only -d <device-id>

Notes:

  • flutter install defaults to --release mode — always pass --debug explicitly if that's the build you have, otherwise it looks for app-release.apk and fails
  • flutter install --uninstall-only removes the app from the device without needing adb directly
  • For release builds, you need signing configuration in android/app/build.gradle (see Signing & secrets)
  • Use flutter run instead of flutter install when you want to keep the logs attached and enable hot reload

Test distribution (Internal Testing / Firebase App Distribution)

  1. Bump version/build with the Version Bump skill
  2. flutter build appbundle --release (preferred) or flutter build apk --release
  3. Google Play → Internal Testing track → upload .aab
  4. Firebase App Distribution (alternative) → upload .apk and invite testers

Production release (Google Play)

  1. Ensure versionName and versionCode are correct in pubspec.yaml
  2. Configure signing: create android/key.properties and add the keystore block to android/app/build.gradle
  3. flutter build appbundle --release
  4. Upload to Google Play Console → Production track
  5. Complete store listing, content rating and submit for review

Signing & secrets

  • Never commit keystore files or key.properties to version control
  • Add them to .gitignore before the first commit
  • Store secrets in environment variables or a secrets manager (e.g. GitHub Secrets for CI)

12. Versioning & Git Tags

The project uses cider to manage the version in pubspec.yaml and maintains a CHANGELOG.md following the Keep a Changelog convention. Every production release should be tagged in Git so that the history stays navigable and CI/CD pipelines can anchor artifacts to a precise commit.

Version format

Versions follow Semantic Versioning (MAJOR.MINOR.PATCH) with a build number appended after + (e.g. 1.2.0+7). The build number is incremented automatically by cider bump and is used by the app stores.

Workflow — from bump to tag

# 1. Update the version (choose the appropriate bump type)
cider bump patch   # 1.0.0 → 1.0.1
cider bump minor   # 1.0.0 → 1.1.0
cider bump major   # 1.0.0 → 2.0.0
# Or set an exact version:
cider version 2.0.0

# 2. Stage and commit the version change
git add pubspec.yaml CHANGELOG.md
git commit -m "chore: bump version to $(cider version)"

# 3. Create an annotated tag on main
git tag -a "v$(cider version)" -m "Release v$(cider version)"

# 4. Push the commit and the tag
git push origin main
git push origin "v$(cider version)"

Always use annotated tags (-a flag) rather than lightweight ones. Annotated tags store the tagger, date and message — they are first-class objects in Git and are picked up correctly by GitHub Releases and most CI/CD systems.

Tag naming convention

Pattern Example When to use
vMAJOR.MINOR.PATCH v1.2.0 Every production release
vMAJOR.MINOR.PATCH-beta.N v2.0.0-beta.1 Pre-release / beta builds

Listing and deleting tags

# List all tags (sorted by version)
git tag --sort=-version:refname

# Show details of a specific tag
git show v1.2.0

# Delete a tag locally (e.g. if created by mistake)
git tag -d v1.2.0

# Delete the tag from the remote as well
git push origin --delete v1.2.0

Using the Version Bump skill

Triggered with "aggiornami il progetto alla versione X.Y.Z", it automates steps 1–4: it detects changes via git, generates a CHANGELOG draft for your approval, bumps the version with cider, commits, tags and pushes. See AI Tooling for usage details.


13. Dependencies

Package Version Purpose
go_router ^18.0.0 Declarative routing with deep linking
google_fonts ^8.2.1 Font loading (Lato used by default)
cached_network_image ^3.4.1 Network image loading with cache and fade
flutter_secure_storage ^11.0.0 Encrypted key-value storage (Keychain / Android Keystore)
image_picker ^1.2.3 Camera and gallery access
intl ^0.20.3 Internationalisation utilities
uuid ^4.6.0 Unique ID generation
cupertino_icons ^1.0.9 iOS-style icon assets
cider (dev) ^0.2.10 CLI version management
flutter_lints (dev) ^6.0.0 Flutter team's recommended lints

14. De-templating Checklist — Using This as a Template

When you clone this repo to start a real project, going through the Project Initialisation skill only handles part of the job (app name, version, CLAUDE.md context). Everything below is not touched automatically and must be done by hand before you ship under a new identity.

Automated by the Project Initialisation skill

Trigger it with "inizializza il progetto" (see AI Tooling). It updates:

Also included: README.md is overwritten with TEMPLATE.md's content, with the project name/tagline/app-context placeholders filled in from what you provided in Step 1 of that skill.

File Field
pubspec.yaml name:
lib/main.dart title: inside MaterialApp.router
android/app/src/main/AndroidManifest.xml android:label
ios/Runner/Info.plist CFBundleName, CFBundleDisplayName
pubspec.yaml version: reset to 1.0.0+1
CHANGELOG.md cleared and restarted from 1.0.0
CLAUDE.md assistant identity/persona and app context

Manual — Android application ID

The package id still reads com.example.sb_template_flutter after init. Change it in:

File What to change
android/app/build.gradle.kts namespace and defaultConfig.applicationId
android/app/src/main/kotlin/com/example/sb_template_flutter/MainActivity.kt move the file to a new path matching the new package (e.g. android/app/src/main/kotlin/com/yourcompany/yourapp/MainActivity.kt) and update its package declaration

Manual — iOS bundle identifier

ios/Runner.xcodeproj/project.pbxproj still has PRODUCT_BUNDLE_IDENTIFIER = com.example.sbTemplateFlutter (and the .RunnerTests variant) across all build configurations (Debug/Profile/Release). Easiest path: open ios/Runner.xcworkspace in Xcode → Runner target → Signing & Capabilities → set the new Bundle Identifier, then repeat for the RunnerTests target.

Manual — App icon

No icon-generation package is configured yet. Recommended: add flutter_launcher_icons as a dev dependency, point it at your new 1024×1024 source image, then run dart run flutter_launcher_icons to regenerate:

  • android/app/src/main/res/mipmap-*/ic_launcher*.png (adaptive icon — background/foreground/monochrome variants included)
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/*.png

Without this, the app ships with the template's default icon.

Manual — Splash screen

No splash-screen package is configured. If the app needs a branded launch screen, add flutter_native_splash and configure/generate it — otherwise Android/iOS fall back to their platform default blank splash.

Manual — Branding assets & README

  • assets/sb-template-flutter-logo.png — replace or remove; update the assets: entry in pubspec.yaml if the filename changes
  • README logo image (top of this file) and the footer credit/links — replace with your own
  • LICENSE — update author/copyright if you don't want MIT-as-Stefano-Biddau to carry over

Manual — Repository & store metadata

  • git remote set-url origin <your-new-repo-url> (or re-init if you cloned without a template flow)
  • Signing keys: Android keystore (android/key.properties, never committed) and iOS signing certificate/provisioning are per-bundle-id — you'll need new ones once the application id / bundle id changes
  • Google Play Console / App Store Connect app listings must be created fresh under the new application id / bundle id — they cannot be renamed from the template's placeholder

15. Migration Notes & Known Toolchain Transitions

A living log of toolchain transitions this template has hit or is watching — useful both to understand where the project currently stands and, if you're using this template and run into the same wall, to know it's a known issue rather than something you broke. Each entry stays until the transition is fully resolved and merged into the main docs above; new entries go on top.

Android — AGP 9 / Kotlin built-in DSL migration — blocked, reverted to pre-AGP-9

  • Status: not migrated. Project reverted to Gradle 8.14 / AGP 8.11.1 / Kotlin 2.2.20 — the last combination confirmed to build cleanly. AGP 9 is not usable on this project yet, in any configuration tried so far.
  • What happened: Android Gradle Plugin 9.0 made the new DSL (ApplicationExtension) the default and deprecated the old android {} accessor plus the kotlinOptions {} block (replaced by kotlin { compilerOptions {} }), and dropped support for applying the separate org.jetbrains.kotlin.android (KGP) plugin in favour of AGP's own built-in Kotlin compiler. Full details: AGP 9.0 release notes.
  • Two approaches tried, both failed:
    1. Full migration (android.newDsl=true / android.builtInKotlin=true, code updated to kotlin { compilerOptions {} }, kotlin-android plugin removed): applying dev.flutter.flutter-gradle-plugin itself fails —
      class com.android.build.gradle.internal.dsl.ApplicationExtensionImpl$AgpDecorated_Decorated
      cannot be cast to class com.android.build.gradle.AbstractAppExtension
      
      Flutter stable's own Gradle plugin (flutter_tools/gradle) is not yet compatible with AGP 9's new DSL.
    2. Opt-out (android.newDsl=false / android.builtInKotlin=false, kept the legacy kotlin-android plugin + kotlinOptions {} block — as documented as a supported fallback in the AGP 9.0 release notes): build still fails with the exact same deprecation-as-error on android {} and kotlinOptions {} as with no flags at all. On AGP 9.0.1, these are hard @Deprecated(level = ERROR) annotations baked into the plugin's own compiled classes — the Kotlin script compiler enforces them regardless of the newDsl/builtInKotlin runtime flags. The documented opt-out did not work in practice on this setup — treat it as unverified until seen working, not as a reliable escape hatch.
  • Current setup: android/gradle/wrapper/gradle-wrapper.properties, android/settings.gradle.kts and android/app/build.gradle.kts are back to the pre-migration state (Gradle 8.14, AGP 8.11.1, Kotlin 2.2.20, kotlin-android plugin, kotlinOptions {} block). No android.newDsl / android.builtInKotlin flags in gradle.properties — irrelevant below AGP 9.
  • Revisit when: a stable Flutter release changelog explicitly confirms AGP 9 support (check flutter upgrade release notes), then retry approach 1 (full migration) first — it's the one Google intends to be permanent, since the AGP 10.0 removal of the opt-out flags makes approach 2 a dead end regardless.
  • Migration steps for approach 1, once Flutter's tooling supports it (kept here so it doesn't have to be re-researched):
    1. Bump android/gradle/wrapper/gradle-wrapper.properties to Gradle ≥9.1.0, android/settings.gradle.kts AGP to ≥9.0.1.
    2. android/settings.gradle.kts: remove the id("org.jetbrains.kotlin.android") version "..." apply false plugin declaration.
    3. android/app/build.gradle.kts: remove id("kotlin-android") from plugins {}, remove the kotlinOptions {} block from android {}, add:
      kotlin {
          compilerOptions {
              jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
          }
      }
    4. android/gradle.properties: add android.newDsl=true and android.builtInKotlin=true.
    5. Before flipping the flags, confirm every native Android plugin dependency (check each package's android/build.gradle* in ~/.pub-cache) no longer applies kotlin-android/org.jetbrains.kotlin.android itself — an unmigrated third-party plugin will conflict with built-in Kotlin the same way the Flutter Gradle plugin currently does. As of this entry, flutter_secure_storage 11.0.0 (Java-only, no Kotlin plugin) and image_picker_android 0.8.13+19 (already on compilerOptions) are both fine.
    6. Run flutter build apk --debug to confirm before removing this entry.

Built with ❤️ by Stefano Biddau

stefanobiddau.com · @stefanoBid

About

A Flutter starter template with an opinionated design system, type-safe routing, reusable UI components, and pre-configured GitHub Copilot Agent tooling.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages