Agreguen esta lÃnea a su Bloc para evitar leaks de memoria:
@override
void dispose() {
_placeSelectedController.close();
}
Arquitectura de Aplicaciones
Pasos para aprender Flutter Avanzado
¿Qué es una Arquitectura de software?
Tipos de Arquitecturas para Flutter
Arquitectura BLoC en Flutter
BLoC + Clean Architecture en Flutter
Estructurando nuestro proyecto utilizando la Arquitectura BLoC
Haciendo BLoC Singleton en Flutter
Aplicando Providers al Proyecto
Firebase y Flutter
Integrando Firebase Authentication, Cloud Firestore y Firebase Storage al proyecto
Integrando Firebase a Flutter para Android
Integrando Firebase a Flutter para iOS
Creación de Pantalla de Login con Flutter
Creando botones reutilizables en Flutter
Autenticación de Firebase con Google
Implementando Firebase Authentication en BLoC Pattern
Streams en Flutter
Manejando una sesión con Firebase Authentication y Flutter
Implementando Google SignOut en BLoC
Implementando Google SignOut en View
Monitoreando y validando la conexión al Sign con Google
Mostrando los datos de usuario de Google en la interfaz en Flutter
Cloud Firestore de Firebase en Flutter
¿Qué es Cloud Firestore de Firebase?
Analizando un modelo de datos no relacional
Creando un Modelo de datos en Cloud Firestore
Enviando datos a Cloud Firestore
Creando un Widget gradiente personalizado
Manejo de Desbordamiendo de Texto de Widget Text
Botón de Back en un Appbar en Flutter
Navegación entre pantallas en Flutter
Widget Text Appbar personalizado en Flutter
Widget TextField personalizado en Flutter
Creando una Safe Area para una interfaz que tiene un AppBar
Widget TextField con iconos en Flutter
Retocando el CardView
Mostrando imágenes en un CardView
Creando un botón de Submit en Flutter
EnvÃo de datos de un fórmulario en Flutter
Subiendo datos a Firestore de Firebase
Formularios en Flutter
Acceso al Hardware con Flutter
Acceso a la cámara en Flutter
LibrerÃas de acceso a Hardware en Flutter
Firebase Storage en Flutter
Qué es y cómo funciona Firebase Storage en Flutter
Subiendo una imagen a Firebase Storage desde Flutter
Querys avanzados en Cloud Firestore de Firebase en Flutter
Manejo de imágenes en Cloud Firestore
Cloud Firestore insertando referencias y arrays en la base de datos
Descargar imágenes de Firebase Storage y mostrarlas en Flutter
Procesando datos con BLoC Pattern
Trayendo datos de Cloud Firestore
Persistiendo datos de un usuario logueado
Aplicando Filtros en Cloud Firestore
Construyendo los Places en la pantalla de Home
Mostrando los Places en la pantalla de Home
Actualizando datos en tiempo real
Manejando la lógica de likes, como botón toggle.
Insertando y obteniendo referencias en datos de Firestore.
Usando el caché para cargar imágenes más rápido
StreamController, sink, add y StreamBuilder
Conclusiones
Conclusiones
Lectura
Aprovecharemos lo aprendido en nuestras primeras clases sobre Streams para crear un Stream personalizado.
Durante el curso estuvimos utilizando los Streams que nos proveÃa Firebase, pero es momento de crear un Stream propio. Esto surge por la necesidad de implementar la muestra de los detalles de cada Place.
En la última vista de nuestra aplicación mostrabamos un texto genérico refiriéndonos a las Bahamas.
Por la estructura que tiene nuestra aplicación en la ventana HomeTrips()
no tenemos accesibles los datos ni de la lista de lugares para la descripción ni de la descripción para la lista, ambos se encuentran en archivos independientes.
class HomeTrips extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Stack(
children: <Widget>[
ListView(
children: <Widget>[
DescriptionPlace("Bahamas", 4, descriptionDummy),
ReviewList()
],
),
HeaderAppBar()
],
);
}
}
Lo que haremos con la definición del StreamController es asà como con Firebase ponÃamos alerta el Widget StreamBuilder y este siempre estaba pendiente de reflejar los cambios en la interfaz de usuario, ahora necesitamos reflejar siempre los datos del Place seleccionado en DescriptionPlace
de tal forma que no tengamos que pasar los datos como parámetro sino que este automáticamente los refleje.
bloc_user.dart
StreamController placeSelectedStreamController = StreamController();
stream
por eso lo transformaremos de esta forma:Stream get placeSelectedStream => placeSelectedStreamController.stream;
sink
que solo se puede implementar al StreamController.StreamSink get placeSelectedSink => placeSelectedStreamController.sink;
Una vez que todo está declarado en el archivo Bloc que mantiene una persistencia Global en la aplicación, ya podemos ingresar el objeto Place cuando lo necesitemos.
Mira el archivo completo aquÃ
card_image_list.dart
para añadir la funcionalidad de al dar click en la Foto podamos mostrar los detallesGestureDetector()
. Este widget es muy útil pues básicamente hace clickeable cualquier área y además nos provee de un método onTap que nos permitirá controlar el comportamiento.return ListView(
padding: EdgeInsets.all(25.0),
scrollDirection: Axis.horizontal,
children: places.map((place){
return GestureDetector(
onTap: (){
print("CLICK PLACE: ${place.name}");
userBloc.placeSelectedSink.add(place);
},
child: CardImageWithFabIcon(
pathImage: place.urlImage,
width: 300.0,
height: 250.0,
left: 20.0,
iconData: place.liked?iconDataLiked:iconDataLike,
onPressedFabIcon: (){
setLiked(place);
},
internet: true,
),
);
}).toList(),
);
setLiked
implementaremos la lógica de incremento o decremento de likes del place, para que este dato también sea persistente en la descripción.void setLiked(Place place){
setState(() {
place.liked = !place.liked;
userBloc.likePlace(place, widget.user.uid);
place.likes = place.liked?place.likes+1:place.likes-1;
userBloc.placeSelectedSink.add(place);
});
}
Mira el archivo completo aquÃ
description_place.dart
implementaremos el StreamBuilder
que estará alerta para mostrar los datos del Place una vez que se seleccione.UserBloc
en el método build asÃ:UserBloc userBloc = BlocProvider.of (context);
return StreamBuilder (
stream: userBloc.placeSelectedStream,
builder: (BuildContext context, AsyncSnapshot snapshot){
});
if (snapshot.hasData) {
Place place = snapshot.data;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
titleStars(place),
descriptionWidget(place.description),
ButtonPurple(buttonText: "Navigate", onPressed: (){})
],
);
}else {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container (
margin: EdgeInsets.only(
top: 400.0,
left: 20.0,
right: 20.0
),
child: Text(
"Selecciona un lugar",
style: TextStyle(
fontFamily: "Lato",
fontSize: 30.0,
fontWeight: FontWeight.w900
),
textAlign: TextAlign.left,
),
)
],
);
}
Notaste que los Widgets ahora están en forma de funciones titleStars
y descriptionWidget
:
Widget titleStars(Place place){
return Row (
children: [
Container (
margin: EdgeInsets.only(
top: 350.0,
left: 20.0,
right: 20.0
),
child: Text(
place.name,
style: TextStyle(
fontFamily: "Lato",
fontSize: 30.0,
fontWeight: FontWeight.w900
),
textAlign: TextAlign.left,
),
),
Container (
margin: EdgeInsets.only(
top: 370.0,
),
child: Text(
"Hearts: ${place.likes}",
style: TextStyle(
fontFamily: "Lato",
fontSize: 18.0,
fontWeight: FontWeight.w900,
color: Colors.amber
),
textAlign: TextAlign.left,
),
),
],
);
}
Widget descriptionWidget(String descriptionPlace){
return Container(
margin: new EdgeInsets.only(
top: 20.0,
left: 20.0,
right: 20.0
),
child: new Text(
descriptionPlace,
style: const TextStyle(
fontFamily: "Lato",
fontSize: 16.0,
fontWeight: FontWeight.bold,
color: Color(0xFF56575a)
),
),
);
}
Aportes 13
Preguntas 0
Agreguen esta lÃnea a su Bloc para evitar leaks de memoria:
@override
void dispose() {
_placeSelectedController.close();
}
Al implementar este Stream personalizado pude evidenciar fácilmente la utilidad del patrón BLoC!
Vistas totalmente distintas en un nivel de jerarquÃa distinto pueden interactuar con la lógica de negocio a través de los Streams. Todo esto gracias al BlocProvider, el cual se encuentra ubicado en lo más alto del nivel jerárquico y que permite tener acceso al UserBloc (En donde se definen los streams).
Para este caso, los Places (Cards) de la CardImageList en su evento onTap:
onTap: (){
print("CLICK PLACE: ${place.name}");
userBloc.placeSelectedSink.add(place);
},
Utilizan la entrada del Stream: el SINK para añadir el objeto Place (que servirá como referencia para saber que lugar se clickeó de la lista de cards)
De esta manera todos los StreamBuilder que estén a la escucha del stream identifiquen este cambio.
Es por esto que en el cuerpo de la pantalla HomeTrips, especÃficamente en DescriptionPlace, se construye el código del StreamBuilder que permanece escuchando los cambios
AQUI SE PUEDE NOTAR EN LA LINEA
stream: userBloc.placeSelectedStream,
CODIGO QUE PERTENECE A DESCRITION PLACE
return StreamBuilder<Place>(
stream: userBloc.placeSelectedStream,
builder: (BuildContext context, AsyncSnapshot<Place> snapshot){
if (snapshot.hasData) {
print("PLACE SELECTED: ${snapshot.data.name}");
Place place = snapshot.data;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
titleStars(place),
descriptionWidget(place.description),
ButtonPurple(buttonText: "Navigate", onPressed: (){})
],
);
}else {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container (
margin: EdgeInsets.only(
top: 400.0,
left: 20.0,
right: 20.0
),
child: Text(
"Selecciona un lugar",
style: TextStyle(
fontFamily: "Lato",
fontSize: 30.0,
fontWeight: FontWeight.w900
),
textAlign: TextAlign.left,
),
)
],
);
}
},
);
En conclusión, comunicamos 2 vistas pasando cualquier dato, sin importar el nivel jerárquico que tengan en el árbol de Widgets.
Que paso con PlaceBloc? Solo se puede implentar un bloc?
Si les aparece el siguiente error:
Stream has already been listened to.
Utilicen esto en el bloc y listo:
StreamController<Place> placeSelectedStreamController = StreamController<Place>.broadcast();
Amigos, hasta aqui ya casi me doy por vencido, solo una pregunta, como puedo hacer esta publicacion un archivo de produccion uno pára Andrid y otro para IOs, espero me puedan ayudar. Saludos.
Como se nota que se les acabaron los recursos para grabar video ðŸ˜
El like en en los places que muestra en Profile no esta programado.
Hola que tal, una pregunta amigos, ya que no pose una mac para poder probar al mismo tiempo la versin en iOS, puedo hacer la app primero en androidy luego probar su avance en una Mac prestada?. o Saben de alguna forma de desarrollar en estos casos. Gracias
Genial ahora solo queda tratar de embellecer mas la app y quedará muy bien.
Hola,
Esta implementación no actualiza automáticamente la descripción del Place, cuando surge algún cambio en la base de datos ocasionado por algún otro usuario (dar like, modificar descripcion, eliminación, etc). Es necesario volver a hacer click en el place para actualizarlo, ya que el sink toma valor actualizado solo cuando se da click en un place.
Pensaba solucionarlo poniendo a disposición los places en una jerarquÃa mayor(HomeTrips) con el streamBuider. Luego pasar los places a CardImageList y DescriptionPlace. En DescriptionPlace, Buscar en la lista de places recibida el place a mostrar con datos actualizados, usando el ID del PlaceSeleccionado, de modo tal que el place mostrado siempre se actualizará cuando haya un cambio de BD o cuando el usuario seleccione otro place. También se podrÃa extraer el stremBuilder que hay en descriptionPlace al nivel del widget HomeTrips, para buscar ahà mismo el place a mostrar en la descripción y luego pasarlo a DescriptionPlace.
Sin embargo, veo muy engorroso hacerlo. Habrá una manera más elegante de hacerlo?? De repente algún método de la clase Stream o StreamSink que nos pueda ayudar??
Slds.
Me salio este error, Couldn’t read file LocalFile: (Dirección de algo de mi pc) Please verify that this file has read permission and try again. alguien sabe que esta pasando?
Está genial el que enseñen a usar Stream
Oh wait til’ I do what I do,
Hit you with that ddu-du ddu-du du
¿Quieres ver más aportes, preguntas y respuestas de la comunidad? Crea una cuenta o inicia sesión.