Dashboard de la app — KPIs de campaña
Impresiones
2.4M
▲ +18% esta semana
ROAS Promedio
3.8×
▲ +0.4× vs mes anterior
Gasto Total
€18.4K
▼ 92% presupuesto usado
Leads Generados
1.247
▲ CPL €14.75 promedio
Arquitectura de capas (Clean Architecture Flutter)
UI Layer
📱 DashboardScreen
📋 CampaignListScreen
🔍 CampaignDetailScreen
🔐 LoginScreen
State
⚡ authProvider
⚡ campaignsProvider
⚡ dashboardProvider
⚡ biometricProvider
Router
🗺 GoRouter
🔒 AuthGuard
🐚 ShellRoute (Nav)
Services
🌐 ApiClient (Dio)
🔑 AuthService
👆 BiometricService
🔔 PushService
Data
📦 CampaignRepository
🗄 HiveCache
🔐 SecureStorage
Native
🤖 Android (Kotlin)
🍎 iOS (Swift)
⬛ MethodChannel
2.4M
Impr.
+18%
3.8×
ROAS
+0.4×
1.2K
Leads
+22%
Campañas activas
Meta — Brand Awareness
4.2×
Google Search — Leads
5.1×
TikTok — Retargeting
2.1×
Código fuente — módulos clave
// Estado asíncrono de campañas con Riverpod — AsyncNotifier + caché Hive import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/campaign.dart'; import '../repositories/campaign_repository.dart'; // Modelo Freezed — inmutable, serializable @freezed class Campaign with _$Campaign { const factory Campaign({ required String id, required String name, required double roas, required double dailyBudget, required bool isActive, required int impressions, required int leads, }) = _Campaign; factory Campaign.fromJson(Map<String, dynamic> json) => _$CampaignFromJson(json); } // AsyncNotifier — maneja loading / data / error automáticamente class CampaignsNotifier extends AsyncNotifier<List<Campaign>> { @override Future<List<Campaign>> build() async { final repo = ref.read(campaignRepositoryProvider); final cached = await repo.getCached(); // Hive — respuesta instantánea if (cached.isNotEmpty) state = AsyncData(cached); return repo.fetchAll(); // API — actualiza en background } /// Actualiza el presupuesto diario de una campaña (optimistic update) Future<void> updateBudget(String id, double newBudget) async { final prev = state.value ?? []; state = AsyncData(prev.map((c) => c.id == id ? c.copyWith(dailyBudget: newBudget) : c ).toList()); try { await ref.read(campaignRepositoryProvider).updateBudget(id, newBudget); } catch (e) { state = AsyncData(prev); // Rollback si la API falla rethrow; } } } final campaignsProvider = AsyncNotifierProvider<CampaignsNotifier, List<Campaign>>(CampaignsNotifier.new);
// Rutas declarativas con redirección por auth y ShellRoute de navegación import 'package:go_router/go_router.dart'; final router = GoRouter( initialLocation: '/', redirect: (context, state) { final auth = providerContainer.read(authProvider); final isLoggedIn = auth.user != null; final onAuth = state.matchedLocation.startsWith('/auth'); if (!isLoggedIn && !onAuth) return '/auth/login'; if (isLoggedIn && onAuth) return '/'; return null; }, routes: [ GoRoute(path: '/auth/login', builder: (_, __) => const LoginScreen()), // Shell con barra de navegación inferior compartida ShellRoute( builder: (_, __, child) => AppShell(child: child), routes: [ GoRoute(path: '/', builder: (_, __) => const DashboardScreen()), GoRoute(path: '/campaigns', builder: (_, __) => const CampaignListScreen()), GoRoute( path: '/campaigns/:id', builder: (_, s) => CampaignDetailScreen(id: s.pathParameters['id']!), ), GoRoute(path: '/settings', builder: (_, __) => const SettingsScreen()), ], ), ], );
// Autenticación biométrica vía MethodChannel — Face ID (iOS) / Huella (Android) import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class BiometricService { static const _channel = MethodChannel('io.growthtrack/biometric'); /// Devuelve true si el dispositivo soporta biometría Future<bool> isAvailable() async { try { return await _channel.invokeMethod<bool>('isAvailable') ?? false; } on PlatformException { return false; } } /// Muestra el prompt de Face ID / huella y devuelve si fue exitosa Future<bool> authenticate({required String reason}) async { try { return await _channel.invokeMethod<bool>( 'authenticate', {'reason': reason}, ) ?? false; } on PlatformException catch (e) { throw AuthException(e.message ?? 'Biometric failed'); } } } // Provider global final biometricServiceProvider = Provider((ref) => BiometricService());
Estructura del proyecto y dependencias
Estructura lib/
📁 lib/
📁 models/
📄 campaign.dart
📄 user.dart
📁 providers/
📄 auth_provider.dart
📄 campaigns_provider.dart
📄 dashboard_provider.dart
📁 repositories/
📄 campaign_repository.dart
📁 services/
📄 api_client.dart
📄 auth_service.dart
📄 biometric_service.dart
📄 push_service.dart
📁 screens/
📄 dashboard_screen.dart
📄 campaign_list_screen.dart
📄 campaign_detail_screen.dart
📄 login_screen.dart
📁 widgets/
📄 stat_card.dart
📄 campaign_tile.dart
📄 roas_chart.dart
📄 router.dart
📄 main.dart
pubspec.yaml — dependencias clave
| Paquete | Versión | Propósito | Tipo |
|---|---|---|---|
| flutter_riverpod | ^2.5.1 | Estado async/reactivo | estado |
| go_router | ^13.2.0 | Rutas declarativas + deep link | core |
| dio | ^5.4.3 | HTTP client + interceptors JWT | red |
| freezed_annotation | ^2.4.1 | Modelos inmutables + JSON | core |
| hive_flutter | ^1.1.0 | Caché local offline | storage |
| flutter_secure_storage | ^9.0.0 | JWT en Keychain / Keystore | storage |
| firebase_messaging | ^14.9.2 | Push notifications (FCM) | nativo |
| fl_chart | ^0.68.0 | Gráficas ROAS / evolución | core |
Features implementadas y decisiones técnicas
Lo que cubre esta arquitectura
-
Estado asíncrono con Riverpod AsyncNotifier Loading/data/error automáticos; caché Hive como stale-while-revalidate
-
Auth guard en GoRouter + refresh JWT automático Interceptor Dio reintenta el request con token nuevo si recibe 401
-
Biometría nativa vía MethodChannel Face ID en iOS / huella dactilar en Android sin dependencia externa
-
Optimistic updates en edición de presupuesto UI responde en < 16ms; rollback automático si la API falla
-
Push notifications con FCM (Firebase) Alerta cuando campaña supera el 90% del presupuesto diario
-
const widgets en toda la UI Flutter skippea rebuild en widgets const; mejora FPS en listas largas
Comandos de desarrollo
Setup
flutter create growthtrack_mobile \
--org io.growthtrack \
--platforms ios,android
flutter pub add flutter_riverpod go_router dio
flutter pub add freezed_annotation hive_flutter
flutter pub add flutter_secure_storage fl_chart
Desarrollo
flutter run -d ios # Simulador iOS
flutter run -d android # Emulador Android
dart run build_runner watch # Freezed codegen
Build distribución
flutter build ipa --release # App Store
flutter build appbundle --release # Google Play
Guías aplicadas al proyecto
7 principios Flutter aplicados a GrowthTrack:
Composition over inheritance Riverpod AsyncNotifier const widgets por defecto GoRouter + ShellRoute Lógica en providers, no en widgets Platform.isIOS para UI adaptativa --dart-define para entornos dev/prod
Composition over inheritance Riverpod AsyncNotifier const widgets por defecto GoRouter + ShellRoute Lógica en providers, no en widgets Platform.isIOS para UI adaptativa --dart-define para entornos dev/prod