iOS dependency manager: this project has been migrated from CocoaPods to Swift Package Manager (SPM). No
pod installis required. All iOS plugins are resolved automatically via SPM on build.
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.
- Overview
- Getting Started
- Project Structure
- Design System
- Routing
- Layouts
- Screens
- Widgets
- Helpers & Validators
- AI Tooling — CLAUDE.md & Workflows
- Deployment
- Versioning & Git Tags
- Dependencies
- De-templating Checklist — Using This as a Template
- Migration Notes & Known Toolchain Transitions
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).
- Flutter SDK ≥ 3.44.0
- Dart SDK ^3.11.3
- Xcode (for iOS development)
- Android Studio / Android SDK (for Android development)
Option 1: Use as GitHub Template (Recommended)
- Click "Use this template" on GitHub
- Clone your new repository:
git clone https://github.com/your-username/your-project.git
cd your-projectOption 2: Clone directly
git clone https://github.com/stefanoBid/sb-flutter-template.git my-project
cd my-project
rm -rf .git && git initAfter 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| 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 |
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
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.
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 backgroundStatic 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 |
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)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 |
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)Routing is powered by go_router. The template adds a type-safe navigation layer on top that prevents passing wrong parameters at compile time.
| 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 |
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);| Constant | Path | Parameters |
|---|---|---|
AppRouter.home |
/home |
none |
AppRouter.forms |
/form |
none |
AppRouter.profile |
/profile |
none |
AppRouter.details |
/details/:detailId |
DetailParams(detailId) |
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),
);
},
),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.
Layouts are reusable page-level scaffolds in lib/layouts/. A screen should compose one layout rather than building its own Scaffold structure.
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.
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. |
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. |
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. |
An overlay app bar for use on top of hero images or full-bleed backgrounds. Fully transparent background.
Screens live in lib/screens/, organised by feature folder. Each folder should contain the screen file and, optionally, feature-specific widgets.
- One screen per file. File name:
[feature]_screen.dart, class name:[Feature]Screen. - Screens are
StatelessWidgetunless local state is strictly necessary. - All layout is delegated to a layout from
lib/layouts/— screens do not build rawScaffolds. - Navigation is always performed via
AppRouter.
| 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 |
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.
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)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. |
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))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. |
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. |
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. |
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. |
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. |
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. |
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. |
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. |
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) |
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 closeA 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. |
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.
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,
),
)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),
);All helpers live in lib/helpers/ as flat files. The set of files in this folder is fixed — do not rename or reorganise them.
Exports AppColors. See Design System — Colours.
Exports AppDesign. See Design System — Spacing & Radius.
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 |
Exports AppTypography. See Design System — Typography.
Exports the ThemeData used in MaterialApp. Edit this file to change the font family (uses google_fonts) or override Material component themes.
Exports AppRouter, AppTypedRoute<P>, GenericRouteParams, NoParams, and built-in params classes (DetailParams). See Routing.
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 |
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.
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.
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.
- Bump version/build with the Version Bump skill
- Build the release IPA:
flutter build ipa --release - Open
build/ios/archive/Runner.xcarchivein Xcode Organizer - Product → Archive → Distribute App → App Store Connect → TestFlight
- Invite internal or external testers from App Store Connect
- Ensure version and build number are correct
flutter build ipa --release- Open
build/ios/archive/Runner.xcarchivein Xcode Organizer → Distribute → App Store Connect - Complete metadata, screenshots and review information on App Store Connect
- 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 ..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 drawerThe 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 --releaseThe 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 installdefaults to--releasemode — always pass--debugexplicitly if that's the build you have, otherwise it looks forapp-release.apkand failsflutter install --uninstall-onlyremoves 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 runinstead offlutter installwhen you want to keep the logs attached and enable hot reload
- Bump version/build with the Version Bump skill
flutter build appbundle --release(preferred) orflutter build apk --release- Google Play → Internal Testing track → upload
.aab - Firebase App Distribution (alternative) → upload
.apkand invite testers
- Ensure
versionNameandversionCodeare correct inpubspec.yaml - Configure signing: create
android/key.propertiesand add the keystore block toandroid/app/build.gradle flutter build appbundle --release- Upload to Google Play Console → Production track
- Complete store listing, content rating and submit for review
- Never commit keystore files or
key.propertiesto version control - Add them to
.gitignorebefore the first commit - Store secrets in environment variables or a secrets manager (e.g. GitHub Secrets for CI)
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.
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.
# 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 (
-aflag) 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.
| 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 |
# 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.0Triggered 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.
| 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 |
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.
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 |
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 |
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.
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.
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.
assets/sb-template-flutter-logo.png— replace or remove; update theassets:entry inpubspec.yamlif 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
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
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.
- 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 oldandroid {}accessor plus thekotlinOptions {}block (replaced bykotlin { compilerOptions {} }), and dropped support for applying the separateorg.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:
- Full migration (
android.newDsl=true/android.builtInKotlin=true, code updated tokotlin { compilerOptions {} },kotlin-androidplugin removed): applyingdev.flutter.flutter-gradle-pluginitself fails —Flutter stable's own Gradle plugin (class com.android.build.gradle.internal.dsl.ApplicationExtensionImpl$AgpDecorated_Decorated cannot be cast to class com.android.build.gradle.AbstractAppExtensionflutter_tools/gradle) is not yet compatible with AGP 9's new DSL. - Opt-out (
android.newDsl=false/android.builtInKotlin=false, kept the legacykotlin-androidplugin +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 onandroid {}andkotlinOptions {}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 thenewDsl/builtInKotlinruntime 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.
- Full migration (
- Current setup:
android/gradle/wrapper/gradle-wrapper.properties,android/settings.gradle.ktsandandroid/app/build.gradle.ktsare back to the pre-migration state (Gradle 8.14, AGP 8.11.1, Kotlin 2.2.20,kotlin-androidplugin,kotlinOptions {}block). Noandroid.newDsl/android.builtInKotlinflags ingradle.properties— irrelevant below AGP 9. - Revisit when: a stable Flutter release changelog explicitly confirms AGP 9 support (check
flutter upgraderelease 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):
- Bump
android/gradle/wrapper/gradle-wrapper.propertiesto Gradle ≥9.1.0,android/settings.gradle.ktsAGP to ≥9.0.1. android/settings.gradle.kts: remove theid("org.jetbrains.kotlin.android") version "..." apply falseplugin declaration.android/app/build.gradle.kts: removeid("kotlin-android")fromplugins {}, remove thekotlinOptions {}block fromandroid {}, add:kotlin { compilerOptions { jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } }android/gradle.properties: addandroid.newDsl=trueandandroid.builtInKotlin=true.- Before flipping the flags, confirm every native Android plugin dependency (check each package's
android/build.gradle*in~/.pub-cache) no longer applieskotlin-android/org.jetbrains.kotlin.androiditself — 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_storage11.0.0 (Java-only, no Kotlin plugin) andimage_picker_android0.8.13+19 (already oncompilerOptions) are both fine. - Run
flutter build apk --debugto confirm before removing this entry.
- Bump
Built with ❤️ by Stefano Biddau
