Descripción del complemento
La aplicación compass_app es un proyecto de ejemplo en Flutter para planificación de viajes, diseñada para mostrar la construcción de aplicaciones multiplataforma con estructura modular. Incluye múltiples pantallas, gestión de datos local y remota, y estilos personalizables.
Características principales
- Exploración de destinos por continente con información detallada
- Sistema de reserva de actividades integrado
- Autenticación de usuarios y gestión de perfiles
- Creación y compartición de itinerarios de viaje
- Soporte para entornos de desarrollo (datos locales JSON) y staging (servidor HTTP)
- Diseño responsivo que se adapta a diferentes tamaños de pantalla y modos de tema
Arquitectura técnica
La aplicación sigue un patrón de arquitectura en capas:
- Capa de datos: Maneja la persistencia local y la comunicación con APIs remotas.
- Capa de dominio: Contiene la lógica de negocio y casos de uso esenciales.
- Capa de presentación: Gestiona la interfaz de usuario y las interacciones.
- Capa de navegación: Controla el enrutamiento entrre pantallas.
- Capa de configuración: Administra la inyección de dependencias y ajustes de entorno.
Tecnologías utilizadas
- Flutter: Framework para interfaces de usuario multiplataforma.
- Provider: Biblioteca para gestión de estado.
- Freezed: Generador de modelos de datos inmutables.
- json_serializable: Serialización y deserialización de JSON.
- HTTP: Cliente para comunicaciones de red.
- SharedPreferences: Almacenamiento local de datos.
- Share Plus: Funcionalidad para compartir contenido.
Instalación y uso
Para integrar compass_app en un proyecto de OpenHarmony, se requiere añadir dependencias mediante Git:
Paso 1: Añadir dependencias
En el archivo pubspec.yaml, incluir:
dependencies:
flutter:
sdk: flutter
provider:
git:
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
path: "packages/provider/provider"
http:
git:
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
path: "packages/http/http"
shared_preferences:
git:
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
path: "packages/shared_preferences/shared_preferences"
share_plus:
git:
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
path: "packages/share_plus/share_plus"
freezed_annotation:
git:
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
path: "packages/freezed/freezed_annotation"
json_annotation:
git:
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
path: "packages/json_annotation/json_annotation"
dev_dependencies:
freezed:
git:
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
path: "packages/freezed/freezed"
json_serializable:
git:
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
path: "packages/json_serializable/json_serializable"
build_runner:
git:
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
path: "packages/build_runner/build_runner"
Paso 2: Obtener dependencias
flutter pub get
Paso 3: Configuración de OpenHarmony
Asegurar la configuración correcta del motor de Flutter y dependencias. Para aplicaciones con comunicación de red, añadir permisos en ohos/entry/src/main/module.json5:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},
{
"name": "ohos.permission.READ_USER_STORAGE"
},
{
"name": "ohos.permission.WRITE_USER_STORAGE"
}
]
}
}
Paso 4: Ejecutar la aplicación
Entorno de desarrollo (datos locales)
cd app
flutter run --target lib/main_development.dart
Entorno staging (datos remotos)
Iniciar el servidor:
cd server
dart run
# => Servidor escuchando en puerto 8080
Luego ejecutar la aplicación:
cd app
flutter run --target lib/main_staging.dart
Ejemplos de uso de API
A continuación, ejemplos de código que ilustran el funcionamiento básico de la aplicación:
1. Configuración de inyección de dependencias
// Configuración para datos remotos
List<SingleChildWidget> obtenerProveedoresRemotos {
return [
Provider(create: (_) => ServicioApiAutenticacion()),
Provider(create: (_) => ClienteApi()),
Provider(create: (_) => ServicioAlmacenamientoLocal()),
ChangeNotifierProvider(
create: (contexto) => RepositorioAutRemoto(
apiAuth: contexto.read(),
apiCliente: contexto.read(),
almLocal: contexto.read(),
) as RepositorioAutenticacion,
),
Provider(
create: (contexto) => RepositorioDestinosRemoto(
apiCliente: contexto.read(),
) as RepositorioDestinos,
),
// Otros repositorios...
];
}
2. Implementación de caso de uso
// Caso de uso para crear reservas
class CasoUsoCrearReserva {
final RepositorioDestinos _repoDestinos;
final RepositorioActividades _repoActividades;
final RepositorioReservas _repoReservas;
CasoUsoCrearReserva({
required RepositorioDestinos repoDestinos,
required RepositorioActividades repoActividades,
required RepositorioReservas repoReservas,
}) : _repoDestinos = repoDestinos,
_repoActividades = repoActividades,
_repoReservas = repoReservas;
Future<ResumenReserva> ejecutar({
required String idDestino,
required List<String> idsActividades,
required DateTime fechaInicio,
required DateTime fechaFin,
required int numViajeros,
}) async {
final destino = await _repoDestinos.obtenerPorId(idDestino);
final actividades = await Future.wait(
idsActividades.map((id) => _repoActividades.obtenerPorId(id)),
);
final reserva = Reserva(
id: GeneradorUUID().nuevoUUID(),
destinoId: idDestino,
destino: destino,
actividadesIds: idsActividades,
actividades: actividades,
inicio: fechaInicio,
fin: fechaFin,
viajeros: numViajeros,
creacion: DateTime.now(),
);
final reservaGuardada = await _repoReservas.guardar(reserva);
return ResumenReserva.desdeReserva(reservaGuardada);
}
}
3. Cliente API genérico
// Cliente base para peticiones HTTP
class ClienteApi {
final String urlBase;
final Duration tiempoEspera;
ClienteApi({
this.urlBase = 'http://localhost:8080',
this.tiempoEspera = const Duration(seconds: 30),
});
Future<Response> obtener(String ruta, {Map<String, String>? encabezados}) async {
final uri = Uri.parse('$urlBase$ruta');
return await http.get(
uri,
headers: encabezados,
).timeout(tiempoEspera);
}
Future<Response> enviar(
String ruta,
dynamic datos,
{Map<String, String>? encabezados},
) async {
final uri = Uri.parse('$urlBase$ruta');
final cuerpoJson = jsonEncode(datos);
return await http.post(
uri,
headers: {
'Content-Type': 'application/json',
...?encabezados,
},
body: cuerpoJson,
).timeout(tiempoEspera);
}
// Otros métodos HTTP...
}
4. Repositorio de destinos remoto
// Repositorio para acceder a destinos vía API
class RepositorioDestinosRemoto implements RepositorioDestinos {
final ClienteApi _clienteApi;
RepositorioDestinosRemoto({required ClienteApi clienteApi}) : _clienteApi = clienteApi;
@override
Future<List<Destino>> obtenerTodos() async {
final respuesta = await _clienteApi.obtener('/destinos');
if (respuesta.statusCode == 200) {
final List<dynamic> datos = jsonDecode(respuesta.body);
return datos.map((json) => Destino.fromJson(json)).toList();
} else {
throw Exception('Error al cargar destinos');
}
}
@override
Future<Destino> obtenerPorId(String id) async {
final respuesta = await _clienteApi.obtener('/destinos/$id');
if (respuesta.statusCode == 200) {
return Destino.fromJson(jsonDecode(respuesta.body));
} else {
throw Exception('Error al cargar destino');
}
}
@override
Future<List<Destino>> obtenerPorContinenteId(String continenteId) async {
final respuesta = await _clienteApi.obtener('/destinos?continenteId=$continenteId');
if (respuesta.statusCode == 200) {
final List<dynamic> datos = jsonDecode(respuesta.body);
return datos.map((json) => Destino.fromJson(json)).toList();
} else {
throw Exception('Error al cargar destinos por continente');
}
}
}
5. Pantalla de ejemplo
// Pantalla que muestra listado de destinos
class PantallaDestinos extends StatelessWidget {
const PantallaDestinos({super.key});
@override
Widget build(BuildContext contexto) {
return Scaffold(
appBar: AppBar(title: const Text('Destinos')),
body: FutureBuilder<List<Destino>>(
future: contexto.read<RepositorioDestinos>().obtenerTodos(),
builder: (contexto, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('No se encontraron destinos'));
} else {
final destinos = snapshot.data!;
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16.0,
mainAxisSpacing: 16.0,
),
padding: const EdgeInsets.all(16.0),
itemCount: destinos.length,
itemBuilder: (contexto, indice) {
final destino = destinos[indice];
return TarjetaDestino(destino: destino);
},
);
}
},
),
);
}
}