| name | flutter-fullstack-init |
| description | End-to-end Flutter project initialization with Firebase, AdMob, RevenueCat IAP, GA4 Analytics, Crashlytics, BYOK secure key management, Riverpod 3.0 state management, and pluggable engine architecture. This is the ONE skill to use when creating any new Flutter app from scratch. Triggers on keywords like 'new Flutter app', 'Flutter project', 'init Flutter', '建立新專案', '新專案', 'create Flutter', 'start project'. Also use when setting up Firebase, AdMob, IAP, or GA4 for an existing project that is missing these integrations. This skill supersedes flutter-project-standards for project creation — it contains the full executable workflow, not just a checklist. |
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 library
mcp_context7_query-docs — verify the actual API
Never assume API signatures from memory. This is the #1 source of errors.
1.3 Core Dependencies
Add to pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^3.0.0
riverpod_annotation: ^3.0.0
go_router: ^15.0.0
firebase_core: ^3.0.0
firebase_analytics: ^11.0.0
firebase_crashlytics: ^4.0.0
google_mobile_ads: ^6.0.0
purchases_flutter: ^8.0.0
flutter_secure_storage: ^9.0.0
shared_preferences: ^2.0.0
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
firebase login:use <account>@gmail.com
dart pub global activate flutterfire_cli
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:
EngineRouter picks 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, ChangeNotifierProvider still work but are moved to package:flutter_riverpod/legacy.dart. New code should use @riverpod code 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:
flutter pub get
dart run build_runner build --delete-conflicting-outputs
flutter analyze
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.