Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.github.com>
Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com> configuracion para firebase
This commit is contained in:
276
recolecta_app/lib/app/app.dart
Normal file
276
recolecta_app/lib/app/app.dart
Normal file
@@ -0,0 +1,276 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import '../firebase_options.dart';
|
||||
|
||||
final bootstrapProvider = FutureProvider<void>((ref) async {
|
||||
await dotenv.load(fileName: 'assets/.env');
|
||||
|
||||
// Inicializar Firebase (si hay DefaultFirebaseOptions, úsalas; sino, intenta initializeApp() y espera que haya google-services/Info.plist)
|
||||
final FirebaseOptions? options = DefaultFirebaseOptions.currentPlatform;
|
||||
if (options != null) {
|
||||
await Firebase.initializeApp(options: options);
|
||||
} else {
|
||||
await Firebase.initializeApp();
|
||||
}
|
||||
|
||||
// Registrar handler para mensajes en background
|
||||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||
});
|
||||
|
||||
// Handler top-level requerido por FCM
|
||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
// Asegurar Firebase inicializado en background isolate
|
||||
try {
|
||||
await Firebase.initializeApp();
|
||||
} catch (_) {
|
||||
// ignore if already initialized
|
||||
}
|
||||
// Aquí puedes procesar y guardar la notificación si hace falta
|
||||
debugPrint(
|
||||
'FCM background message received: ${message.messageId} | data: ${message.data}',
|
||||
);
|
||||
}
|
||||
|
||||
final apiClientProvider = Provider<Dio>((ref) {
|
||||
final baseUrl = dotenv.env['API_BASE_URL'] ?? 'http://10.0.2.2:8000';
|
||||
|
||||
return Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
connectTimeout: const Duration(seconds: 15),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
headers: const <String, dynamic>{'Content-Type': 'application/json'},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
final secureStorageProvider = Provider<FlutterSecureStorage>((ref) {
|
||||
return const FlutterSecureStorage();
|
||||
});
|
||||
|
||||
final routerProvider = Provider<GoRouter>((ref) {
|
||||
return GoRouter(
|
||||
initialLocation: '/home',
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/home',
|
||||
name: 'home',
|
||||
builder: (context, state) => const HomePage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/status',
|
||||
name: 'status',
|
||||
builder: (context, state) => const StatusPage(),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
class RecolectaApp extends ConsumerWidget {
|
||||
const RecolectaApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final bootstrap = ref.watch(bootstrapProvider);
|
||||
|
||||
return bootstrap.when(
|
||||
loading: () => const MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: BootstrapLoadingPage(),
|
||||
),
|
||||
error: (error, stackTrace) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: BootstrapErrorPage(error: error),
|
||||
),
|
||||
data: (_) => MaterialApp.router(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Recolecta',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1F6F78)),
|
||||
scaffoldBackgroundColor: const Color(0xFFF4F7F6),
|
||||
useMaterial3: true,
|
||||
),
|
||||
routerConfig: ref.watch(routerProvider),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BootstrapLoadingPage extends StatelessWidget {
|
||||
const BootstrapLoadingPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
}
|
||||
|
||||
class BootstrapErrorPage extends StatelessWidget {
|
||||
const BootstrapErrorPage({super.key, required this.error});
|
||||
|
||||
final Object error;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 48),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'No se pudo cargar la configuración inicial.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HomePage extends ConsumerWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final dio = ref.read(apiClientProvider);
|
||||
final storage = ref.read(secureStorageProvider);
|
||||
final baseUrl = dio.options.baseUrl;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Recolecta'),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => context.goNamed('status'),
|
||||
icon: const Icon(Icons.route),
|
||||
tooltip: 'Estado',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: ListView(
|
||||
children: [
|
||||
const Text(
|
||||
'Bootstrap listo',
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'La app ya carga .env, Riverpod y GoRouter para la base del MVP.',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_InfoCard(title: 'API base URL', value: baseUrl, icon: Icons.cloud),
|
||||
const SizedBox(height: 16),
|
||||
_InfoCard(
|
||||
title: 'Secure Storage',
|
||||
value: storage.runtimeType.toString(),
|
||||
icon: Icons.lock,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_ImageCard(
|
||||
title: 'Widget listo para caché de imágenes',
|
||||
imageUrl:
|
||||
'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=800&q=80',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StatusPage extends StatelessWidget {
|
||||
const StatusPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Estado')),
|
||||
body: const Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Text(
|
||||
'Aquí después se conectarán ETA, notificaciones, ruta asignada y métricas de privacidad.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoCard extends StatelessWidget {
|
||||
const _InfoCard({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String value;
|
||||
final IconData icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
child: ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(title),
|
||||
subtitle: Text(value),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ImageCard extends StatelessWidget {
|
||||
const _ImageCard({required this.title, required this.imageUrl});
|
||||
|
||||
final String title;
|
||||
final String imageUrl;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
height: 180,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => const SizedBox(
|
||||
height: 180,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
errorWidget: (context, url, error) => const SizedBox(
|
||||
height: 180,
|
||||
child: Center(child: Icon(Icons.image_not_supported)),
|
||||
),
|
||||
),
|
||||
Padding(padding: const EdgeInsets.all(16), child: Text(title)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user