Flutter Full-Stack Init
One-shot skill that creates a production-ready Flutter app with all standard integrations wired up and verified.
When to Use
- Creating a new Flutter app from scratch
- Adding missing integrations (Firebase, AdMob, RevenueCat, GA4, Crashlytics) to an existing Flutter project
- Setting up a BYOK (Bring Your Own Key) pattern for user-provided API keys
- Building a pluggable engine architecture (e.g., ASR, Translation, AI, etc.)
Pre-Flight
Before starting, gather these from the user:
| Question | Example |
|---|---|
| App name (English) | VoxBridge |
| Package name | com.example.yourapp |
| What does it do? (one sentence) | Real-time meeting translator |
| Which Firebase account? | Use gcp-firebase-project-map skill to find low-usage project |
| Any external APIs? (BYOK) | Deepgram, Claude, Groq, etc. |
Phase 1: Create Project (5 min)
1.1 Flutter Create
flutter create --org com.example --platforms android,ios,web --project-name <app_name> ./
1.2 Context7 Verification Rule
CRITICAL: Before writing ANY code that uses a library, ALWAYS:
mcp_context7_resolve-library-id— find the librarymcp_context7_query-docs— verify the actual APINever assume API signatures from memory. This is the #1 source of errors.
1.3 Core Dependencies
Add to pubspec.yaml:
dependencies:
flutter:
sdk: flutter
# State Management
flutter_riverpod: ^3.0.0
riverpod_annotation: ^3.0.0
# Navigation
go_router: ^15.0.0
# Firebase
firebase_core: ^3.0.0
firebase_analytics: ^11.0.0
firebase_crashlytics: ^4.0.0
# Monetization
google_mobile_ads: ^6.0.0
purchases_flutter: ^8.0.0
# Storage
flutter_secure_storage: ^9.0.0
shared_preferences: ^2.0.0
# UI
flutter_animate: ^4.0.0
google_fonts: ^6.0.0
dev_dependencies:
build_runner: ^2.4.0
riverpod_generator: ^3.0.0
Then: flutter pub get
Phase 2: Project Structure (10 min)
Create this directory structure:
lib/
├── main.dart # Entry point + Firebase + Crashlytics + AdMob init
├── app.dart # MaterialApp.router + ProviderScope
├── firebase_options.dart # Generated by flutterfire
├── core/
│ ├── constants/app_constants.dart
│ ├── router/app_router.dart # GoRouter config
│ ├── theme/app_theme.dart # ThemeData + AppColors
│ └── engine/ # Pluggable engine layer (if app has API backends)
│ ├── engine_interface.dart # Abstract interface
│ └── engine_router.dart # BYOK + fallback routing
├── features/
│ ├── home/presentation/home_screen.dart
│ ├── settings/presentation/settings_screen.dart # BYOK keys + Pro purchase
│ └── <feature>/
│ ├── providers/<feature>_provider.dart
│ └── presentation/<feature>_screen.dart
├── services/
│ ├── analytics_service.dart # GA4 wrapper
│ ├── crash_service.dart # Crashlytics wrapper
│ ├── ad_service.dart # AdMob wrapper (admob-ux-best-practices)
│ ├── iap_service.dart # RevenueCat wrapper
│ └── key_vault.dart # flutter_secure_storage BYOK
└── shared/
├── widgets/ # Reusable widgets
└── models/ # Shared data models
Phase 3: Service Layer Templates
3.1 Key Vault (BYOK)
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class KeyVault {
static const _storage = FlutterSecureStorage();
static Future<String?> getKey(String name) =>
_storage.read(key: 'byok_$name');
static Future<void> setKey(String name, String value) =>
_storage.write(key: 'byok_$name', value: value);
static Future<void> deleteKey(String name) =>
_storage.delete(key: 'byok_$name');
}
3.2 Analytics Service (GA4)
import 'package:firebase_analytics/firebase_analytics.dart';
class AnalyticsService {
static final instance = FirebaseAnalytics.instance;
static Future<void> logEvent(String name, [Map<String, Object>? params]) =>
instance.logEvent(name: name, parameters: params);
static Future<void> logScreenView(String screenName) =>
instance.logScreenView(screenName: screenName);
}
3.3 Crash Service
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
class CrashService {
static final instance = FirebaseCrashlytics.instance;
static Future<void> recordError(dynamic e, StackTrace? s) =>
instance.recordError(e, s);
static Future<void> setContext(String key, String value) =>
instance.setCustomKey(key, value);
}
3.4 AdMob Service
Use the admob-ux-best-practices skill for placement rules:
- Banner: bottom of screen only, away from interactive elements
- Interstitial: between natural task breaks only
- Rewarded: user-initiated only (never forced)
import 'dart:async';
import 'dart:io';
import 'package:google_mobile_ads/google_mobile_ads.dart';
class AdService {
static final instance = AdService._();
AdService._();
BannerAd? _bannerAd;
BannerAd? get bannerAd => _bannerAd;
/// Load an adaptive banner ad (uses screen width for optimal sizing)
Future<BannerAd?> loadAdaptiveBanner({
required double width,
bool useTestAds = false,
}) async {
final adUnitId = useTestAds
? (Platform.isIOS
? 'ca-app-pub-3940256099942544/2435281174'
: 'ca-app-pub-3940256099942544/9214589741')
: (Platform.isIOS
? AppConstants.admobBannerIos // from app_constants.dart
: AppConstants.admobBannerAndroid);
final size = await AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(
width.truncate());
if (size == null) return null;
final completer = Completer<BannerAd?>();
final ad = BannerAd(
adUnitId: adUnitId,
size: size,
request: const AdRequest(),
listener: BannerAdListener(
onAdLoaded: (ad) => completer.complete(ad as BannerAd),
onAdFailedToLoad: (ad, error) {
ad.dispose();
completer.complete(null);
},
),
)..load();
_bannerAd = await completer.future;
return _bannerAd;
}
void dispose() {
_bannerAd?.dispose();
_bannerAd = null;
}
}
[!TIP] Always use adaptive banner instead of fixed
AdSize.banner. Adaptive banners fill the full width and improve eCPM by 20-30%.
3.5 IAP Service (RevenueCat)
import 'package:purchases_flutter/purchases_flutter.dart';
class IapService {
static final instance = IapService._();
IapService._();
Future<void> initialize({required String apiKey}) async {
await Purchases.configure(PurchasesConfiguration(apiKey));
}
Future<bool> isPro() async {
try {
final info = await Purchases.getCustomerInfo();
return info.entitlements.active.containsKey('pro');
} catch (_) {
return false;
}
}
Future<Offerings?> getOfferings() async {
try { return await Purchases.getOfferings(); } catch (_) { return null; }
}
Future<bool> purchase(Package pkg) async {
try {
// purchases_flutter >=8.x: use PurchaseParams instead of purchasePackage
await Purchases.purchase(PurchaseParams.package(pkg));
return true;
} catch (_) { return false; }
}
Future<bool> restore() async {
try { await Purchases.restorePurchases(); return true; } catch (_) { return false; }
}
}
Phase 4: Firebase Setup (5 min)
4.1 Find the right Firebase account
Use gcp-firebase-project-map skill to list all accounts and pick one with low usage.
4.2 Configure FlutterFire
# Switch to chosen account
firebase login:use <account>@gmail.com
# Install CLI
dart pub global activate flutterfire_cli
# Configure (generates firebase_options.dart)
flutterfire configure \
--project=<firebase-project-id> \
--platforms=android,ios,web \
--android-package-name=com.example.<app> \
--ios-bundle-id=com.example.<app> \
--yes
4.3 main.dart
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'firebase_options.dart';
import 'app.dart';
import 'core/constants/app_constants.dart';
import 'services/iap_service.dart';
Future<void> main() async {
await runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
// AdMob init with timeout protection (prevents blocking app startup)
unawaited(
MobileAds.instance.initialize().timeout(
const Duration(seconds: 5),
onTimeout: () => null,
),
);
// RevenueCat init (platform-specific SDK keys)
await IapService.instance.initialize(
apiKey: Platform.isIOS
? AppConstants.revenueCatKeyIos
: AppConstants.revenueCatKeyAndroid,
);
runApp(const ProviderScope(child: MyApp()));
}, (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
});
}
[!IMPORTANT] AdMob initialization MUST use
unawaited()with timeout. Without this, if Google Play Services is unavailable (e.g., Huawei devices), startup hangs forever. The 5-second timeout ensures the app always launches.
Phase 5: Pluggable Engine Architecture (If App Uses External APIs)
When the app consumes external AI/API services, use this pattern:
// Abstract interface
abstract class MyEngine {
String get name;
Future<void> dispose();
}
// Concrete implementation (verify API with Context7 first!)
class ConcreteEngine implements MyEngine {
final String apiKey; // From KeyVault
ConcreteEngine({required this.apiKey});
// ... implementation
}
Key rules:
- Context7 first: Always query the actual SDK API before writing implementation
- BYOK pattern: API keys come from
KeyVault, never hardcoded - Fallback routing:
EngineRouterpicks the best engine based on user's available keys - Cost tracking: Track API costs per-call for transparency
Phase 6: Riverpod 3.0 State Management
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'my_feature_provider.g.dart';
@riverpod
class MyFeature extends _$MyFeature {
@override
MyState build() => const MyState.initial();
Future<void> doSomething() async {
state = state.copyWith(status: Status.loading);
try {
// ... business logic
state = state.copyWith(status: Status.success);
} catch (e, s) {
CrashService.recordError(e, s);
state = state.copyWith(status: Status.error);
}
}
}
Then run codegen: dart run build_runner build --delete-conflicting-outputs
Riverpod 3.0 Important Changes
[!CAUTION] ProviderException wrapping: Riverpod 3.0 wraps all provider computation failures in
ProviderException. If your code catches specific exception types from providers, you must update:// Before (Riverpod 2.x) try { await ref.read(myProvider.future); } on MyCustomError catch (e) { /* handle */ } // After (Riverpod 3.0) try { await ref.read(myProvider.future); } on ProviderException catch (e) { if (e.exception is MyCustomError) { /* handle */ } }Legacy providers:
StateProvider,StateNotifierProvider,ChangeNotifierProviderstill work but are moved topackage:flutter_riverpod/legacy.dart. New code should use@riverpodcode generation exclusively.Testing: Use
ProviderContainer.test()for unit tests — it auto-disposes after the test ends:test('my test', () { final container = ProviderContainer.test(); // Use container... });
Phase 7: Settings Screen (BYOK + Pro)
The Settings screen must include:
- BYOK Key Fields — obscured text fields for each API key, loaded from / saved to
KeyVault - Pro Subscription — RevenueCat purchase button
- About — version, privacy, terms
[!CAUTION]
Apple Guideline 3.1.2(c) — 訂閱頁法律連結(反覆被 reject!)
購買按鈕同一畫面(Restore Purchases 下方)必須有:
- Privacy Policy 可點擊連結 →
launchUrl()外部瀏覽器- Terms of Use 可點擊連結 →
launchUrl()外部瀏覽器URL:
https://your-github-username.github.io/{appname}-privacy.html/{appname}-terms.html連結必須在 app binary 內,不是只在 App Store Connect metadata。 部署前用curl驗證 URL 回 HTTP 200。
Phase 8: Verification Checklist
Run in this exact order:
# 1. Dependencies
flutter pub get
# 2. Code generation
dart run build_runner build --delete-conflicting-outputs
# 3. Static analysis — MUST be 0 errors
flutter analyze
# 4. (Optional) Build check
flutter build apk --debug
Expected result: No issues found!
Phase 9: CLAUDE.md
Create a CLAUDE.md at project root:
# <App Name>
## Build Commands
\`\`\`bash
flutter pub get
dart run build_runner build --delete-conflicting-outputs
flutter analyze
\`\`\`
## Project Overview
<One paragraph description>
## Architecture
- State: Riverpod 3.0 (code generation)
- Navigation: GoRouter
- Firebase: <project-id> (<account>)
- Monetization: AdMob + RevenueCat
## Key Design Decisions
- BYOK pattern: User provides own API keys via Settings
- Pluggable engines: Abstract interface → concrete implementations
- Cost transparency: Real-time cost tracking shown to user
Related Skills
These skills should be triggered at appropriate steps:
| Skill | When |
|---|---|
gcp-firebase-project-map |
Phase 4.1 — Find Firebase account |
admob-manager |
After Phase 3.4 — Create ad units in console |
admob-ux-best-practices |
Phase 3.4 — Ad placement rules |
revenuecat-manager |
After Phase 3.5 — Configure products |
ga4-manager |
After Phase 3.2 — Create GA4 property |
ga4-analytics |
Post-launch — Query analytics data |
crash-stability-reporting |
Post-launch — Monitor crash rates |
app-icon-generator |
Before store submission |
flutter-deploy |
When ready to deploy |
store-publishing-automation |
App Store / Google Play metadata |
firebase-auth-manager |
If app needs authentication |
firebase-appcheck-manager |
If app needs App Check |
Related skills
firebase-flutter-setup— use as an alternative if you only need Firebase setup without the full fullstack init (more lightweight).flutter-verify— use after flutter-fullstack-init to validate all integrations are working.