Meta title: Tutorial Flutter Dart: Crea tu primera app móvil multiplataforma paso a paso
Meta description: Guía práctica para crear apps móviles con Flutter y Dart: instalación, estructura, widgets Flutter, navegación, gestión de estado Provider, consumo de APIs, almacenamiento local, theming, responsive, i18n, Firebase y publicación.
URL slug: tutorial-flutter-dart-crear-apps-moviles
Tu primera app móvil multiplataforma con Flutter y Dart: guía completa paso a paso
¿Quieres crear apps móviles con Flutter desde cero? En este tutorial Flutter Dart aprenderás a instalar el SDK, configurar Android Studio y Xcode (opcional), crear tu primer proyecto, entender los widgets Flutter, gestionar estado con Provider, navegar con GoRouter, consumir APIs, almacenar datos localmente, aplicar theming y modo oscuro, hacer diseño responsivo, internacionalizar con intl, integrar Firebase Flutter y preparar builds para publicar en tiendas. Ideal para desarrolladores principiantes e intermedios.
1) Requisitos previos
- Conocimientos básicos de programación (idealmente en Dart, JavaScript o similar).
- Sistema operativo: Windows, macOS o Linux.
- 10+ GB libres en disco, 8 GB RAM recomendado.
- Git instalado y línea de comandos.
Checklist:
- [ ] Instalaste Git
- [ ] Tienes permisos de administrador en tu equipo
- [ ] Espacio y RAM suficientes
2) Instalación del SDK de Flutter
2.1 Windows
1 2 3 4 5 6 |
choco install flutter -y # con Chocolatey # o manual: # Descarga: https://docs.flutter.dev/get-started/install/windows # Descomprime en C:\src\flutter y agrega C:\src\flutter\bin al PATH flutter doctor |
Explicación: instala Flutter y valida el entorno con flutter doctor.
2.2 macOS
1 2 3 |
brew install --cask flutter flutter doctor |
Explicación: Homebrew simplifica la instalación y actualización en macOS.
2.3 Linux
1 2 3 4 |
# Descarga el tar.xz desde la web oficial y descomprime en ~/development # Agrega ~/development/flutter/bin al PATH flutter doctor |
Explicación: en Linux la instalación suele ser manual; verifica dependencias con flutter doctor.
Checklist:
- [ ] flutter –version funciona
- [ ] PATH configurado
- [ ] flutter doctor sin errores críticos
3) Configuración de Android Studio y (opcional) Xcode
3.1 Android Studio
1) Instala Android Studio (https://developer.android.com/studio)
2) Desde el SDK Manager, instala:
- Android SDK Platform (última estable)
- Android SDK Build-Tools
- Android Emulator
3) Instala plugins: Flutter y Dart.
1 2 3 |
# Acepta licencias del SDK flutter doctor --android-licenses |
Explicación: Android Studio provee SDK, emulador y herramientas necesarias para compilar y depurar Android.
3.2 Xcode (opcional, solo macOS para iOS)
1) Instala Xcode desde App Store.
2) Abre Xcode una vez y acepta las licencias.
3) Instala CocoaPods:
1 2 |
sudo gem install cocoapods |
Explicación: Xcode es requerido para compilar y probar en simulador/dispositivo iOS.
Checklist:
- [ ] Plugins Flutter y Dart en Android Studio
- [ ] Licencias de Android aceptadas
- [ ] Xcode y CocoaPods (si compilas para iOS)
4) Crear y ejecutar tu primer proyecto
4.1 Crear proyecto
1 2 3 |
flutter create mi_primera_app cd mi_primera_app |
Explicación: genera una plantilla con una app de contador.
4.2 Ejecutar en emulador o dispositivo
- Inicia un emulador Android desde AVD Manager o:
1 2 3 4 5 |
# Lista AVDs emulator -list-avds # Ejecuta uno emulator -avd Pixel_6_API_34 |
- Conecta un dispositivo físico Android (activa Depuración USB).
- En iOS (macOS): abre el simulador con:
1 2 |
open -a Simulator |
- Ejecuta:
1 2 3 |
flutter devices flutter run |
Explicación: flutter run detecta dispositivos/emuladores conectados y lanza la app con hot reload.
Checklist:
- [ ] Proyecto creado sin errores
- [ ] Emulador/simulador o dispositivo físico reconocido
- [ ] App ejecutando con hot reload activo
5) Estructura de carpetas del proyecto
Estructura principal:
- android/: configuración de Android (Gradle, AndroidManifest.xml)
- ios/: configuración de iOS (Xcode, Info.plist)
- lib/: código Dart de tu app
- test/ e integration_test/: pruebas
- pubspec.yaml: dependencias y assets
Checklist:
- [ ] Identificaste lib/ como carpeta principal de código
- [ ] Revisaste pubspec.yaml
- [ ] Ubicaste Manifests (AndroidManifest.xml, Info.plist)
6) Fundamentos de widgets Flutter
6.1 StatelessWidget vs StatefulWidget
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import 'package:flutter/material.dart'; class HolaStateless extends StatelessWidget { final String nombre; const HolaStateless({super.key, required this.nombre}); @override Widget build(BuildContext context) { return Text('Hola, $nombre'); } } class ContadorStateful extends StatefulWidget { const ContadorStateful({super.key}); @override State<ContadorStateful> createState() => _ContadorStatefulState(); } class _ContadorStatefulState extends State<ContadorStateful> { int _contador = 0; @override Widget build(BuildContext context) { return Column( children: [ Text('Clicks: $_contador'), ElevatedButton( onPressed: () => setState(() => _contador++), child: const Text('Incrementar'), ), ], ); } } |
Explicación: StatelessWidget no mantiene estado; StatefulWidget sí, usando setState.
Checklist:
- [ ] Diferencias entre Stateless y Stateful claras
- [ ] Practicaste setState()
7) Layout y composición: Row, Column, Stack
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
Widget layoutDemo() { return Stack( children: [ Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: const [ Icon(Icons.menu), Text('Título'), Icon(Icons.search), ], ), const Expanded( child: Center(child: Text('Contenido')), ), ], ), const Positioned( right: 16, bottom: 16, child: FloatingActionButton(onPressed: null, child: Icon(Icons.add)), ), ], ); } |
Explicación: Column apila verticalmente, Row horizontalmente, Stack superpone elementos y Positioned posiciona absolutos.
Checklist:
- [ ] Usaste Row/Column para estructuras básicas
- [ ] Comprendiste Stack/Positioned para overlays
8) Navegación con GoRouter (y alternativa Navigator 2.0)
Instala GoRouter en pubspec.yaml y define rutas tipadas.
1 2 3 4 5 6 |
# pubspec.yaml (fragmento) dependencies: flutter: sdk: flutter go_router: ^14.0.0 |
Explicación: go_router simplifica Deep Links, rutas declarativas y redirecciones.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
// lib/main.dart (extracto de rutas) import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; void main() { runApp(const MyApp()); } final _router = GoRouter( routes: [ GoRoute( path: '/', builder: (context, state) => const HomePage(), routes: [ GoRoute( path: 'details/:id', builder: (context, state) => DetailPage(id: state.pathParameters['id']!), ), ], ), ], ); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: _router, title: 'Mi app Flutter', theme: ThemeData.light(), darkTheme: ThemeData.dark(), ); } } class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Inicio')), body: ListView.builder( itemCount: 5, itemBuilder: (_, i) => ListTile( title: Text('Item $i'), onTap: () => context.go('/details/$i'), ), ), ); } } class DetailPage extends StatelessWidget { final String id; const DetailPage({super.key, required this.id}); @override Widget build(BuildContext context) { return Scaffold(appBar: AppBar(title: Text('Detalle $id'))); } } |
Alternativa: Navigator 2.0 nativo con RouterConfig
Checklist:
- [ ] Definiste rutas declarativas
- [ ] Probaste navegación y paso de parámetros
9) Gestión de estado con Provider (y alternativa Riverpod)
1 2 3 4 |
# pubspec.yaml (fragmento) dependencies: provider: ^6.1.2 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// lib/state/app_state.dart import 'package:flutter/material.dart'; class AppState extends ChangeNotifier { int counter = 0; ThemeMode themeMode = ThemeMode.system; void increment() { counter++; notifyListeners(); } void toggleTheme() { themeMode = themeMode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark; notifyListeners(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
// lib/main.dart (Provider + GoRouter + theming) import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'state/app_state.dart'; import 'themes.dart'; import 'package:go_router/go_router.dart'; void main() { runApp(ChangeNotifierProvider( create: (_) => AppState(), child: const MyApp(), )); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { final app = context.watch<AppState>(); final router = GoRouter(routes: [ GoRoute(path: '/', builder: (_, __) => const HomePage()), ]); return MaterialApp.router( routerConfig: router, title: 'Provider Demo', theme: lightTheme, darkTheme: darkTheme, themeMode: app.themeMode, ); } } class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { final app = context.watch<AppState>(); return Scaffold( appBar: AppBar( title: const Text('Gestión de estado Provider'), actions: [ IconButton( icon: const Icon(Icons.brightness_6), onPressed: context.read<AppState>().toggleTheme, ), ], ), body: Center(child: Text('Counter: ${app.counter}')), floatingActionButton: FloatingActionButton( onPressed: context.read<AppState>().increment, child: const Icon(Icons.add), ), ); } } |
Explicación: Provider expone AppState y permite escuchar cambios. Alternativa: Riverpod (más segura y escalable) con Riverpod/Flutter Riverpod.
Checklist:
- [ ] AppState con ChangeNotifier
- [ ] Providers añadidos en el árbol de widgets
- [ ] Incremento y toggle de tema funcionando
10) Consumo de APIs con http o Dio + manejo de errores
1 2 3 4 |
dependencies: dio: ^5.5.0 # o http: ^1.2.1 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// lib/services/api_service.dart import 'package:dio/dio.dart'; class ApiService { final Dio _dio = Dio(BaseOptions( baseUrl: 'https://jsonplaceholder.typicode.com', connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), )); Future<Map<String, dynamic>> getTodo(int id) async { try { final res = await _dio.get('/todos/$id'); return Map<String, dynamic>.from(res.data); } on DioException catch (e) { final status = e.response?.statusCode; throw Exception('Error HTTP ${status ?? ''}: ${e.message}'); } catch (e) { throw Exception('Error inesperado: $e'); } } } |
Explicación: usa Dio con timeouts y captura errores. Con http, usa http.get y jsonDecode.
Checklist:
- [ ] Cliente HTTP configurado
- [ ] Manejo de errores con try/catch
- [ ] Respuestas tipadas o mapeadas
11) Almacenamiento local: SharedPreferences y Hive
1 2 3 4 5 |
dependencies: shared_preferences: ^2.2.3 hive: ^2.2.3 hive_flutter: ^1.1.0 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// lib/services/local_storage.dart import 'package:shared_preferences/shared_preferences.dart'; import 'package:hive_flutter/hive_flutter.dart'; class LocalStorage { static Future<void> init() async { await Hive.initFlutter(); await Hive.openBox('app'); } static Future<void> saveToken(String token) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('token', token); } static Future<String?> getToken() async { final prefs = await SharedPreferences.getInstance(); return prefs.getString('token'); } static Future<void> cacheItem(String key, dynamic value) async { final box = Hive.box('app'); await box.put(key, value); } static dynamic getItem(String key) { final box = Hive.box('app'); return box.get(key); } } |
Explicación: SharedPreferences para pares clave-valor simples; Hive para datos más complejos/offline.
Checklist:
- [ ] Inicializaste Hive al inicio
- [ ] Guardas y recuperas tokens/preferencias
12) Validación de formularios
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
final _formKey = GlobalKey<FormState>(); String? _email; Widget formDemo() { return Form( key: _formKey, child: Column(children: [ TextFormField( decoration: const InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress, validator: (v) { if (v == null || v.isEmpty) return 'Requerido'; final ok = RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(v); return ok ? null : 'Email inválido'; }, onSaved: (v) => _email = v, ), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); // procesar _email } }, child: const Text('Enviar'), ) ]), ); } |
Explicación: Form y TextFormField con validator y onSaved.
Checklist:
- [ ] Validaciones implementadas
- [ ] Mensajes de error claros
13) Theming y modo oscuro
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// lib/themes.dart import 'package:flutter/material.dart'; final lightTheme = ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true, ); final darkTheme = ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark), useMaterial3: true, ); |
Explicación: centraliza temas con Material 3. Cambia ThemeMode desde Provider.
Checklist:
- [ ] ThemeData centralizado
- [ ] Soporte para modo oscuro
14) Diseño responsivo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Widget responsiveDemo(BuildContext context) { final width = MediaQuery.of(context).size.width; final isTablet = width >= 600; return Padding( padding: EdgeInsets.symmetric(horizontal: isTablet ? 48 : 16), child: LayoutBuilder( builder: (_, constraints) { if (constraints.maxWidth > 900) { return Row(children: const [Expanded(child: Placeholder()), Expanded(child: Placeholder())]); } return const Column(children: [Placeholder(), SizedBox(height: 16), Placeholder()]); }, ), ); } |
Explicación: combina MediaQuery y LayoutBuilder para adaptar UI a móviles/tablets.
Checklist:
- [ ] Breakpoints definidos
- [ ] Probaste en diferentes tamaños
15) Internacionalización básica con intl
1 2 3 4 5 |
dependencies: flutter_localizations: sdk: flutter intl: ^0.19.0 |
1 2 3 4 5 6 7 8 9 10 11 12 |
// lib/main.dart (extracto de localización) import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:intl/intl.dart'; // dentro de MaterialApp supportedLocales: const [Locale('en'), Locale('es')], localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], |
1 2 3 4 5 6 7 8 9 10 |
// Uso simple de intl String saludoPorHora(DateTime now) { final hour = now.hour; return hour < 12 ? 'Buenos días' : (hour < 19 ? 'Buenas tardes' : 'Buenas noches'); } String fechaFormateada(DateTime d, String locale) { return DateFormat.yMMMMd(locale).format(d); } |
Explicación: habilita idiomas y usa intl para formateo de fechas/números. Para textos traducidos, considera ARB y generación con herramientas de l10n de Flutter.
Checklist:
- [ ] Locales y delegados agregados
- [ ] Formateo con intl probado
16) (Opcional) Integración con Firebase (Auth y Firestore)
1 2 3 4 5 |
dependencies: firebase_core: ^2.27.0 firebase_auth: ^4.17.8 cloud_firestore: ^5.6.1 |
Pasos rápidos:
1) Instala FlutterFire CLI:
1 2 3 |
dart pub global activate flutterfire_cli flutterfire configure |
2) Inicializa Firebase en main:
1 2 3 4 5 6 7 8 9 |
import 'package:firebase_core/firebase_core.dart'; import 'firebase_options.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); runApp(const MyApp()); } |
3) Autenticación anónima y escribir/leer en Firestore:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import 'package:firebase_auth/firebase_auth.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; Future<void> loginAnon() async { await FirebaseAuth.instance.signInAnonymously(); } Future<void> addNote(String text) async { final uid = FirebaseAuth.instance.currentUser!.uid; await FirebaseFirestore.instance.collection('users/$uid/notes').add({ 'text': text, 'createdAt': FieldValue.serverTimestamp(), }); } Stream<QuerySnapshot<Map<String, dynamic>>> notesStream() { final uid = FirebaseAuth.instance.currentUser!.uid; return FirebaseFirestore.instance .collection('users/$uid/notes') .orderBy('createdAt', descending: true) .snapshots(); } |
Explicación: Firebase Flutter facilita autenticación y base de datos tiempo real con Firestore.
Checklist:
- [ ] flutterfire configure ejecutado
- [ ] Firebase.initializeApp en main
- [ ] Reglas de seguridad revisadas
17) Pruebas: unitarias, de widget e integración
17.1 Unitarias
1 2 3 4 5 6 7 8 9 10 11 |
// test/math_test.dart import 'package:flutter_test/flutter_test.dart'; int add(int a, int b) => a + b; void main() { test('add suma correctamente', () { expect(add(2, 3), 5); }); } |
17.2 De widget
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// test/counter_widget_test.dart import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; void main() { testWidgets('incrementa contador al pulsar FAB', (tester) async { int counter = 0; await tester.pumpWidget(MaterialApp( home: Scaffold( floatingActionButton: FloatingActionButton( onPressed: () => counter++, child: const Icon(Icons.add), ), body: Text('Counter: $counter'), ), )); await tester.tap(find.byType(FloatingActionButton)); await tester.pump(); expect(counter, 1); }); } |
17.3 De integración
1 2 3 4 5 |
# pubspec.yaml (fragmento) dev_dependencies: integration_test: sdk: flutter |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// integration_test/app_test.dart import 'package:integration_test/integration_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('flujo básico', (tester) async { await tester.pumpWidget(MaterialApp(home: Scaffold( appBar: AppBar(title: const Text('Inicio')), floatingActionButton: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)), ))); await tester.tap(find.byIcon(Icons.add)); await tester.pumpAndSettle(); expect(find.text('Inicio'), findsOneWidget); }); } |
Ejecuta:
1 2 3 |
flutter test flutter test integration_test |
Checklist:
- [ ] Al menos una prueba unitaria
- [ ] Una prueba de widget
- [ ] Integración mínima funcionando
18) Configuración de archivos clave
18.1 pubspec.yaml completo (ejemplo)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
name: mi_primera_app description: Demo para crear apps móviles con Flutter publish_to: 'none' version: 1.0.0+1 environment: sdk: '>=3.3.0 <4.0.0' dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.6 go_router: ^14.0.0 provider: ^6.1.2 dio: ^5.5.0 shared_preferences: ^2.2.3 hive: ^2.2.3 hive_flutter: ^1.1.0 flutter_localizations: sdk: flutter intl: ^0.19.0 # Opcional Firebase firebase_core: ^2.27.0 firebase_auth: ^4.17.8 cloud_firestore: ^5.6.1 dev_dependencies: flutter_test: sdk: flutter integration_test: sdk: flutter flutter: uses-material-design: true assets: - assets/ |
Explicación: lista dependencias clave del tutorial Flutter Dart y assets.
18.2 AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<!-- android/app/src/main/AndroidManifest.xml --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.mi_primera_app"> <uses-permission android:name="android.permission.INTERNET" /> <application android:name="${applicationName}" android:label="mi_primera_app" android:icon="@mipmap/ic_launcher"> <activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <meta-data android:name="flutterEmbedding" android:value="2" /> </application> </manifest> |
Explicación: INTERNET para llamadas HTTP. Configuración estándar de Flutter.
18.3 Info.plist
1 2 3 4 5 6 7 |
<!-- ios/Runner/Info.plist --> <key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict> |
Explicación: permite cargas HTTP no seguras en desarrollo. Para producción, usa HTTPS y elimina esta excepción.
Checklist:
- [ ] pubspec.yaml sin errores
- [ ] INTERNET en Android agregada
- [ ] ATS configurado temporalmente en iOS para desarrollo
19) Preparar build de producción y publicar
19.1 Android: APK/AAB
1) Genera keystore (una vez):
1 2 |
keytool -genkey -v -keystore ~/.keystores/miapp.jks -keyalg RSA -keysize 2048 -validity 10000 -alias miapp |
2) Configura android/key.properties:
1 2 3 4 5 |
storePassword=TU_PASS keyPassword=TU_PASS keyAlias=miapp storeFile=/Users/tuuser/.keystores/miapp.jks |
3) android/app/build.gradle (firma):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
def keystoreProperties = new Properties() def keystoreFile = rootProject.file('key.properties') if (keystoreFile.exists()) { keystoreProperties.load(new FileInputStream(keystoreFile)) } android { signingConfigs { release { storeFile file(keystoreProperties['storeFile']) storePassword keystoreProperties['storePassword'] keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] } } buildTypes { release { signingConfig signingConfigs.release minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } |
4) Construye:
1 2 3 |
flutter build appbundle --release # para Play Store flutter build apk --release # para distribución directa |
5) Sube el .aab a Google Play Console y completa listados y pruebas.
19.2 iOS: build y subida básica (macOS)
1) Abre ios/Runner.xcworkspace en Xcode.
2) Configura Signing & Capabilities con tu equipo y Bundle Identifier único.
3) Incrementa versión y build.
4) Compila archive desde Product > Archive o:
1 2 |
flutter build ipa --release |
5) Sube con Xcode Organizer o la app Transporter.
Checklist:
- [ ] Firma configurada (Android/iOS)
- [ ] Build de release generado
- [ ] Pruebas en Test/Play Internal Track antes de producción
20) Resolución de problemas comunes
- flutter doctor muestra issues con Android licenses:
- Ejecuta flutter doctor –android-licenses y acepta todas.
- Gradle falla por memoria:
- Incrementa org.gradle.jvmargs=-Xmx4096m en android/gradle.properties.
- CocoaPods no instala dependencias:
- cd ios && pod repo update && pod install. Asegura tener Ruby/Gems actualizados.
- Emulador Android lento o no arranca:
- Usa imágenes x86_64/arm64, habilita aceleración (Intel HAXM/Hypervisor o Apple Virtualization).
- Dispositivo iOS no aparece:
- Activa Developer Mode en iOS 16+, confía el equipo, revisa certificados en Xcode.
- Error de red en iOS (NSAppTransportSecurity):
- Usa HTTPS o configura excepciones puntuales en Info.plist.
- Problemas con permisos en Android:
- Declara permisos en AndroidManifest.xml y solicita permisos en tiempo de ejecución si aplica.
Checklist:
- [ ] Revisaste logs con flutter run -v
- [ ] Probaste flutter clean && flutter pub get
- [ ] Validaste configuración específica de cada plataforma
Conclusión y siguientes pasos
Has completado un recorrido integral para crear apps móviles con Flutter: desde instalación y estructura de proyecto hasta widgets Flutter, gestión de estado Provider, navegación con GoRouter, consumo de APIs, almacenamiento local, formularios, theming, diseño responsivo, internacionalización y una introducción a Firebase Flutter, además de pruebas y publicación.
Siguiente paso: construye una mini app real (por ejemplo, notas con login anónimo y Firestore), añade tests y publica una versión beta. ¿Listo para el siguiente nivel? Suscríbete a nuestro boletín y comparte este tutorial Flutter Dart con tu equipo.
Buenas prácticas:
- Mantén una arquitectura clara (capas: presentation, application/state, domain, data).
- Maneja errores de red con retroalimentación al usuario y reintentos.
- Usa const donde sea posible para optimizar renders.
- Divide widgets en componentes pequeños y reutilizables.
- Automatiza CI/CD para builds y tests.
- Mide rendimiento con Flutter DevTools y perf overlays.
FAQ (orientado a featured snippets)
-
¿Qué es Flutter y por qué usarlo?
-
Flutter es un SDK de UI de Google para crear apps móviles, web y escritorio con un solo código en Dart. Ventajas: rendimiento nativo, widgets propios y hot reload.
-
¿Qué necesito para empezar con Flutter?
-
Instalar Flutter SDK, Android Studio (y Xcode en macOS), configurar emuladores o dispositivos y correr flutter doctor.
-
¿Navigator 2.0 o GoRouter?
-
Navigator 2.0 es la base nativa y poderosa; GoRouter abstrae su complejidad con rutas declarativas y soporte de deep links.
-
¿Provider o Riverpod para gestión de estado?
-
Provider es simple y oficial; Riverpod ofrece inmutabilidad y testing más ergonómico. Elige según complejidad del proyecto.
-
¿http o Dio para APIs?
-
http es ligero; Dio ofrece interceptores, cancelación y mejores opciones de timeouts/reintentos.
-
¿Cómo activo modo oscuro?
-
Define lightTheme y darkTheme y alterna ThemeMode con Provider.
-
¿Cómo hago responsive?
-
Usa MediaQuery, LayoutBuilder, breakpoints y widgets flexibles (Expanded/Flexible). Prueba en diferentes tamaños.
-
¿Cómo publicar en Play Store y App Store?
-
Genera build de release (AAB/IPA), configura firma, crea listados en tiendas y sigue los procesos de revisión.