GrowthTrack Mobile

Flutter App — Arquitectura completa multiplataforma (iOS · Android)

Flutter 3.x Riverpod iOS · Android
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
Buenas tardes
Dashboard
2.4M
Impr.
+18%
3.8×
ROAS
+0.4×
1.2K
Leads
+22%
Campañas activas
📱
Meta — Brand Awareness
Activa · €120/día
4.2×
🔍
Google Search — Leads
Activa · €85/día
5.1×
🎵
TikTok — Retargeting
Pausada · €50/día
2.1×
🏠 Inicio
📋 Campañas
📊 Analytics
⚙️ Config
Código fuente — módulos clave
lib/providers/campaigns_provider.dart Dart · Riverpod
// 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);
lib/router.dart Dart · GoRouter
// 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()),
      ],
    ),
  ],
);
lib/services/biometric_service.dart Dart · Platform Channel
// 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