flutter-mobile-design
Comprehensive reference for Flutter mobile app development and UI/UX design. Covers architecture, design patterns, component creation, animations, state management, Firebase integration, flavor configuration, localization, and deployment.
Install
mkdir -p .claude/skills/flutter-mobile-design && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/13717" && unzip -o skill.zip -d .claude/skills/flutter-mobile-design && rm skill.zipInstalls to .claude/skills/flutter-mobile-design
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Comprehensive reference for Flutter mobile app development and UI/UX design. Covers architecture, design patterns, component creation, animations, state management, Firebase integration, flavor configuration, localization, and deployment.About this skill
Flutter Mobile Design Skill
Universal reference for building premium Flutter mobile applications. Project-agnostic — use with any Flutter + Firebase project.
1. Recommended Project Structure
{project_root}/
├── lib/
│ ├── main.dart # App entry point
│ ├── main_{flavor}.dart # Flavor-specific entry points (if flavors used)
│ ├── app.dart # MaterialApp / root widget
│ ├── core/
│ │ ├── config/
│ │ │ ├── app_config.dart # Runtime config (env, URLs, feature flags)
│ │ │ ├── firebase_config.dart # Firebase initialization
│ │ │ └── flavor_config.dart # Flavor enum, branding tokens
│ │ ├── theme/
│ │ │ ├── app_colors.dart # Color tokens
│ │ │ ├── app_typography.dart # Text styles
│ │ │ └── app_spacing.dart # Spacing, radius, padding constants
│ │ ├── session/
│ │ │ └── session_initializer.dart
│ │ └── constants.dart # App-wide constants
│ ├── models/ # Data classes (fromJson/toJson)
│ ├── services/ # API calls, Firebase ops, external integrations
│ ├── providers/ # ChangeNotifier state management classes
│ ├── router/
│ │ └── app_router.dart # GoRouter / Navigator 2.0 config
│ ├── screens/ # Full-page views (one per screen)
│ ├── widgets/ # Reusable UI components
│ ├── l10n/ # ARB localization source files
│ │ ├── app_en.arb
│ │ └── app_es.arb
│ └── generated/ # ⚠️ Auto-generated (NEVER edit manually)
│ └── l10n/
├── assets/
│ ├── images/
│ ├── videos/
│ └── animations/ # Lottie JSON files
├── android/
│ └── app/src/{flavor}/ # Flavor-specific Android resources
├── ios/
│ ├── Runner/
│ └── config/{flavor}/ # Flavor-specific iOS configs
├── test/ # Unit & widget tests
├── integration_test/ # E2E tests
├── pubspec.yaml
└── analysis_options.yaml
2. Layered Architecture
2.1 Layer Responsibilities
┌─────────────────────────────────────────────────────┐
│ SCREENS (Pages) │
│ Full-page views. Compose widgets, consume providers.│
│ One file per screen. Never contains business logic. │
├─────────────────────────────────────────────────────┤
│ WIDGETS (Components) │
│ Reusable UI. Stateless preferred. Accept data via │
│ constructor. No business logic, no service calls. │
├─────────────────────────────────────────────────────┤
│ PROVIDERS (State / Business Logic) │
│ ChangeNotifier classes. Call services, expose state │
│ to UI. Handle loading/error/data states. │
├─────────────────────────────────────────────────────┤
│ SERVICES (Data Layer) │
│ Firebase, REST APIs, local storage. Return raw data.│
│ No UI awareness. Stateless static methods or │
│ singleton instances. │
├─────────────────────────────────────────────────────┤
│ MODELS (Data Structures) │
│ Dart classes with fromJson/toJson/fromFirestore. │
│ Immutable when possible. No logic beyond parsing. │
├─────────────────────────────────────────────────────┤
│ CORE (Infrastructure) │
│ Config, theme, session, constants. Shared across │
│ all layers. │
└─────────────────────────────────────────────────────┘
2.2 Dependency Flow
Screens → Widgets
Screens → Providers (via Consumer/context.read)
Providers → Services
Services → Models
Core ← used by all layers
Rules:
- Screens NEVER call services directly
- Widgets NEVER access providers (receive data via constructor)
- Providers NEVER import UI classes
- Services NEVER import Flutter material
2.3 Clean Architecture — Gradual Refactor Levels
Level 1 (Starting point): Screens + Providers + Services + Models. Good enough for most apps < 20 screens.
Level 2 (Moderate complexity): Add a repositories/ layer between providers and services for data source abstraction (local cache + remote).
// lib/repositories/investment_repository.dart
class InvestmentRepository {
final InvestmentRemoteService _remote;
final InvestmentCacheService _cache;
Future<List<Investment>> getAll({bool forceRefresh = false}) async {
if (!forceRefresh) {
final cached = await _cache.getAll();
if (cached.isNotEmpty) return cached;
}
final data = await _remote.fetchAll();
await _cache.saveAll(data);
return data;
}
}
Level 3 (Large apps, 30+ screens): Add use_cases/ (interactors) between providers and repositories to isolate business operations.
// lib/use_cases/calculate_returns.dart
class CalculateReturns {
final InvestmentRepository _repo;
Future<ReturnsSummary> execute(String investorId) async {
final investments = await _repo.getByInvestor(investorId);
// Business logic here
return ReturnsSummary.fromInvestments(investments);
}
}
3. State Management (Provider)
3.1 Provider Template
class FeatureProvider extends ChangeNotifier {
List<Item> _items = [];
bool _isLoading = false;
String? _error;
// Getters — never expose internal state directly
List<Item> get items => List.unmodifiable(_items);
bool get isLoading => _isLoading;
String? get error => _error;
bool get hasData => _items.isNotEmpty;
Future<void> load() async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_items = await FeatureService.getAll();
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
void clear() {
_items = [];
_error = null;
notifyListeners();
}
}
3.2 Provider Registration
// In main.dart or app.dart
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthProvider()),
ChangeNotifierProvider(create: (_) => InvestmentProvider()),
// Add providers as features grow
],
child: const MyApp(),
)
3.3 Consuming in UI
// ✅ Consumer — rebuilds only this subtree
Consumer<FeatureProvider>(
builder: (context, provider, child) {
if (provider.isLoading) return const LoadingSkeleton();
if (provider.error != null) return ErrorState(message: provider.error!);
if (!provider.hasData) return const EmptyState();
return ItemList(items: provider.items);
},
)
// ✅ context.read — for one-time actions (button taps)
ElevatedButton(
onPressed: () => context.read<FeatureProvider>().load(),
child: const Text('Refresh'),
)
// ❌ NEVER use context.watch in callbacks
// ❌ NEVER use setState for shared state
4. Flavor / Environment Configuration
4.1 Flavor Enum
// lib/core/config/flavor_config.dart
enum AppFlavor {
development,
staging,
production;
// Per-flavor values
String get firebaseFunctionsBaseUrl => switch (this) {
development => 'http://localhost:5001/{project_id}/us-central1',
staging => 'https://us-central1-{staging_project_id}.cloudfunctions.net',
production => 'https://us-central1-{prod_project_id}.cloudfunctions.net',
};
String get appName => switch (this) {
development => '{App Name} DEV',
staging => '{App Name} STG',
production => '{App Name}',
};
}
4.2 Client/Brand Flavor (Multi-tenant)
For apps serving multiple clients with different branding:
// lib/core/config/brand_config.dart
enum AppBrand {
brandA,
brandB;
// These values change per client
String get displayName => switch (this) {
brandA => 'Brand A Name',
brandB => 'Brand B Name',
};
Color get primaryColor => switch (this) {
brandA => const Color(0xFF1A237E),
brandB => const Color(0xFF004D40),
};
Color get accentColor => switch (this) {
brandA => const Color(0xFFFFB300),
brandB => const Color(0xFF00BFA5),
};
String get logoAsset => switch (this) {
brandA => 'assets/images/logo_brand_a.png',
brandB => 'assets/images/logo_brand_b.png',
};
String get firebaseProjectId => switch (this) {
brandA => '{firebase_project_id_a}',
brandB => '{firebase_project_id_b}',
};
}
4.3 Runtime Configuration with --dart-define
// lib/core/config/app_config.dart
class AppConfig {
static late final AppFlavor flavor;
static late final AppBrand brand;
static void initialize() {
// Read from --dart-define at compile time
const flavorStr = String.fromEnvironment('FLAVOR', defaultValue: 'development');
const brandStr = String.fromEnvironment('BRAND', defaultValue: 'brandA');
flavor = AppFlavor.values.firstWhere(
(f) => f.name == flavorStr,
orElse: () => AppFlavor.development,
);
brand = AppBrand.values.firstWhere(
(b) => b.name == brandStr,
orElse: () => AppBrand.brandA,
);
}
static String get functionsBaseUrl => flavor.firebaseFunctionsBaseUrl;
static String get appName => '${brand.displayName}${flavor != AppFlavor.production ? ' (${flavor.name.toUpperCase()})' : ''}';
}
4.4 Running with Flavors
# Development + Brand A
flutter run --dart-define=FLAVOR=development --dart-define=BRAND=brandA
# Production + Brand B
flutter run --release --dart-define=FLAVOR=production --dart-define=BRAND=brandB
# Build APK for specific flavor
flutter build apk --release \
--dart-define=FLAVOR=production \
--dart-define=BRAND=brandB
4.5 Flavor-Specific Entry Points (Alternative)
// lib/main_brand_a.dart
void main() {
AppConfig.initializeWith(brand: AppBrand.brandA, flavor: AppFlavor.production);
runApp(const MyApp());
}
// lib/main_brand_b.dart
void main() {
AppConfig.initializeWith(b
---
*Content truncated.*