I've got a few photo's I've uploaded into my firebase storage under a file called 'photos' and I want to be able to retrieve them onto my app through a stream. I have done this before through Firebase cloud database by tapping into the Firestore.instance.collection('messages').snapshots() property in my StreamBuilder, but I don't know how to access the firebase storage snapshots and upload them as a stream into my app.
This was my code for the messages snapshot, I hope it helps:
final _firestore = Firestore.instance;
void messagesStream() async {
await for (var message in _firestore.collection('messages').snapshots()){
for (var snapshot in message.documents){
print(snapshot.data);
}
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('messages').snapshots(),
builder: (context, snapshot){
if (!snapshot.hasData){
return Center(
child: CircularProgressIndicator(backgroundColor: Colors.lightBlueAccent,),
);
} else {
final messages = snapshot.data.documents;
List<Text> messageWidgets = [];
for (var message in messages){
final messageText = message.data['text'];
final messageSender = message.data['sender'];
final messageWidget = Text('$messageText from $messageSender');
messageWidgets.add(messageWidget);
}
return Column(children: messageWidgets,);
}
}
),
),
},
So I figured out you can't create a stream from the firebase storage, but what I could do was, in my firebase cloud database, start a new collection called 'my_collection' and in a new document, create an auto-ID, with a field called 'image' which is a string, with an http reference to an image that is on the internet, or one you can upload to the internet (this is what I did on imgur.com, credit to them)! Here is my code below, I hope it helps others! If it doesn't, have a look at this code written by iampawan, he helped me a tonne!
https://github.com/iampawan/FlutterWithFirebase
class MyList extends StatefulWidget {
#override
_MyListState createState() => _MyListState();
}
class _MyListState extends State<MyList> {
StreamSubscription<QuerySnapshot> subscription;
List <DocumentSnapshot> myList;
final CollectionReference collectionReference = Firestore.instance.collection('my_collection');
final DocumentReference documentReference = Firestore.instance.collection('my_collection').document('GFWRerw45DW5GB54p');
#override
void initState() {
super.initState();
subscription = collectionReference.snapshots().listen((datasnapshot) {
setState(() {
myList = datasnapshot.documents;
});
});
}
#override
void dispose() {
subscription?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return myList != null ?
ListView.builder(
itemCount: myList.length,
itemBuilder: (context, index){
String imgPath = myList[index].data['image'];
return MyCard(assetImage: Image.network(imgPath), function:
(){
if (imgPath == myList[0].data['image']){
Navigator.pushNamed(context, MyMenu.id);
} else if (imgPath == myList[1].data['image']){
Navigator.pushNamed(context, YourMenu.id);
} else if (imgPath == myList[2].data['image']){
Navigator.pushNamed(context, HisMenu.id);
} else if (imgPath == myList[3].data['image']){
Navigator.pushNamed(context, HerMenu.id);
}
},);
})
: Center(child: CircularProgressIndicator(),
);
}
}
Just to note, MyCard is it's own page with it's own constructor that requires an assetImage and a function for the user to be pushed to a new screen:
MyCard({#required this.assetImage, #required this.function});
final Image assetImage;
final Function function;
Related
I'm trying to retrieve all the courses that the user has enrolled in, these courses are present in an array within the document.
After retrieving the course ID from the users collection, I'm trying to retrieve the course details from the courses collection.
But before the courses variable is populated, the coursesCollection statement is executed and throwing the below error.
======== Exception caught by widgets library =======================================================
The following assertion was thrown building _BodyBuilder:
'in' filters require a non-empty [List].
'package:cloud_firestore/src/query.dart':
Failed assertion: line 706 pos 11: '(value as List).isNotEmpty'
Here is the error causing code:
List courses = [];
var coursesCollection;
void fetchCourses() async {
final loggedInUser = FirebaseAuth.instance.currentUser;
if (loggedInUser != null) {
final userCollection = await FirebaseFirestore.instance.collection('users').doc(loggedInUser.uid).get();
courses = userCollection.get('coursesEnrolled');
}
}
#override
void initState() {
fetchCourses();
coursesCollection = FirebaseFirestore.instance.collection('courses').where('courseID', whereIn: courses);
super.initState();
}
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: coursesCollection.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(backgroundColor: kBrandColor),
);
}
}
final courseListStream = snapshot.data!.docs.map((course) {
return CourseData.fromDocument(course);
}).toList();
List<BadgedCourseCard> courseCards = [];
for (var course in courseListStream) {
final courseDocID = course.courseDocID;
final courseID = course.courseID;
final courseTitle = course.courseTitle;
final courseImage = course.courseImage;
final courseBgColor = hexToColor(course.courseBackgroundColor.toString());
hexToColor(course.courseFgColor.toString());
final badgedCourseCard = BadgedCourseCard(
courseTitle: courseTitle.toString(),
courseTitleTextColor: courseFgColor,
cardBackgroundColor: courseBgColor,
courseImage: courseImage.toString(),
courseCardTapped: () {
Provider.of<CourseProvider>(context, listen: false).currentCourseDetails(
currentCourseDocID: courseDocID,
currentCourseID: courseID,
);
Navigator.of(context).push(ScaledAnimationPageRoute(CourseLandingPage(courseID: courseID.toString())));
},
courseBookmarkTapped: () => print("Course Bookmark Tapped"),
rightPadding: 3,
bottomPadding: 0.5,
cardWidth: 80,
);
courseCards.add(badgedCourseCard);
}
return SizedBox(
height: 20.5.h,
child: ListView(
physics: BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
children: courseCards,
),
);
},
);
}
How can I fix this issue?
Here,
coursesCollection = FirebaseFirestore.instance.collection('courses').where('courseID', whereIn: courses);
courses would be [] because fetchCourses is an async call.
Change the return type of fetchCourses from void to Future<void> & try using a then callback:
#override
void initState() {
super.initState();
fetchCourses().then((val) {
coursesCollection = FirebaseFirestore.instance.collection('courses').where('courseID', whereIn: courses);
setState(() {});
});
}
I would also recommend to use FutureBuilder as a better alternative.
coursesCollection is null that's why you're getting another error. Render StreamBuilder only when coursesCollection is not null.
coursesCollection != null ? StreamBuilder(...) : SizedBox(),
For listening to the user's enrolled courses, another StreamBuilder can be used. It would be a nested StreamBuilder setup.
StreamBuilder<DocumentSnapshot<Map<String, dynamic>>>(
stream: FirebaseFirestore.instance.collection('users').doc(FirebaseAuth.instance.currentUser.uid).snapshots(),
builder: (context, snapshot) => snapshot.hasData ? StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
stream: FirebaseFirestore.instance.collection('courses').where('courseID', whereIn: snapshot.data!.data()!['coursesEnrolled']).snapshots(),
builder: (context, snapshotTwo) {},
) : Text('Loading...'),
),
I have an application and In this i'm making a query for get user details by the e-mail account.
I'm using Future class to get data and fill my variable but the widget Text always show null value.
Please let me now if i am doing something wrong.
class _HomePageAppState extends State<HomePageApp> {
String _emailUsuario;
Usuario usuario;
void initState() {
super.initState();
Autenticacao().getCurrentUser().then((user) {
setState(() {
if (user != null) {
_emailUsuario = user.email.toString(); //the user email is returnig correctly
recuperarDadosUsuarioFirebase().then((ds) {
usuario = Usuario(
email: _emailUsuario,
nome: ds['nome'] != null ? ds['nome'] : null,
);
});
}
});
});
}
Future<DocumentSnapshot> recuperarDadosUsuarioFirebase() async {
DocumentSnapshot ds;
await Firestore.instance
.collection('usuarios')
.document(_emailUsuario)
.get()
.then((DocumentSnapshot _ds) {
ds = _ds;
});
return ds;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.white10,
child: ListView(
children: <Widget>[
Text('Bem vindo ${usuario.nome} !!!'),
],
),
),
);
}
}
U might want to use Future Builder for such async work cause build method was called before usuario is assign so like this :
FutureBuilder(
future: getCurrentUser(),
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.data == null) {
return Center(child: CircularProgressIndicator());
}
// after getting data
},
);
Method getCurrentUser() needs to be created :)
Every example about StreamBuilder starts with a StatelessWidget even in flutter example but how do you cancel the subscription in a StatelessWidget widget? For example, I was going through firestore example.
class MessageList extends StatelessWidget {
MessageList({this.firestore});
final Firestore firestore;
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: firestore.collection('messages').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return const Text('Loading...');
final int messageCount = snapshot.data.documents.length;
return ListView.builder(
itemCount: messageCount,
itemBuilder: (_, int index) {
final DocumentSnapshot document = snapshot.data.documents[index];
return ListTile(
title: Text(document['message'] ?? '<No message retrieved>'),
subtitle: Text('Message ${index + 1} of $messageCount'),
);
},
);
},
);
}
}
Now how do I cancel listening to firestore.collection('messages').snapshots() stream?
I use realtime database in my app and this is how I do it
class MessgaeView extends StatefulWidget {
final String _chatId;
MessgaeView(this._chatId);
#override
_MessgaeViewState createState() => _MessgaeViewState();
}
class _MessgaeViewState extends State<MessgaeView> {
Stream<Event> _messageStream;
#override
void initState() {
_messageStream = _database
.reference()
.child("message/${widget._chatId}")
.limitToLast(1)
.onChildAdded;
super.initState();
}
#override
void dispose() {
_messageStream.drain();
super.dispose();
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _messageStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return CupertinoActivityIndicator();
final message =
(snapshot.data.snapshot as DataSnapshot).value['message'];
return Text(message);
});
}
}
Simply replace the previous stream instance with null.
This will require a code similar to the following somewhere:
setState(() {
_messageStream = null;
});
Doing so will stop listening to the stream. But StreamBuilder will still hold the previous value.
I hava same problem and solved by StreamSubscription
For example define StreamSubscription as global
StreamSubscription<Event> _counterSubscription;
then in your place you want to listen to data register your Subscription like this
_counterSubscription = _counterRef.onValue.listen((Event event) {
setState(() {
_counter = event.snapshot.value ?? 0;
});
});
and when you want to remove listener just make this code
if(_counterSubscription !=null)
_counterSubscription .cancel();
What is the correct way to handle this, I have done a lot of searching and most samples which use future builders use them to draw lists so maybe I should be avoiding them all together here.
I want to submit a login form, perform the network request and draw a progress bar while the login is happening, and if successful navigate to a home page. If unsuccessful it should just kill the progress bar and redraw the home page. That part seems to be working, unsure if I am using the Navigator correctly.
The login call returns a user and access token object. The Homepage needs to retrieve the access token which was written to the db by the successful login response. From what I can tell the navigation is happening too quickly and the retrieval of the access token appears to happen before the navigation to the home page.
class LoginPage extends StatefulWidget {
LoginPage({Key key, this.title}) : super(key: key);
final String title;
#override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
bool _isValidForm = true;
Future<LoginResponse> _user;
void _submitLogin() {
setState(() {
if (_isValidForm) {
_user = login().then((_) => Navigator.push(context, MaterialPageRoute(builder: (context) => HomePage())));
}
});
}
Widget _buildLoginForm(AsyncSnapshot<LoginResponse> snapshot) {
if (snapshot.connectionState != ConnectionState.none && !snapshot.hasData) {
return new Center(child: new CircularProgressIndicator());
} else {
return SafeArea(
child: Center(
child: new ListView(
children: <Widget>[
//..more views
Padding(
padding: EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
//..email and password fields
FlatButton(
child: new Text(
'SIGN IN',
),
onPressed: _submitLogin),
]),
)
],
),
),
);
}
}
#override
Widget build(BuildContext context) {
return new FutureBuilder(
future: _user,
builder: (context, AsyncSnapshot<LoginResponse> snapshot) {
return new Scaffold(
backgroundColor: kMyGreen,
body: _buildLoginForm(snapshot),
);
},
);
}
Future<LoginResponse> login() async {
final response = await http.post(...);
if (response.statusCode == 200) {
var loginResponse = LoginResponse.fromJson(json.decode(response.body));
//Write the user details to local db
DBProvider.db.newUser(loginResponse.user);
//Write the tokens to local db
DBProvider.db.newToken(loginResponse.tokens);
return loginResponse;
} else {
throw Exception('Failed to login');
}
}
}
Database methods:
newUser(User newUser) async {
final db = await database;
//get the biggest id in the table
var table = await db.rawQuery("SELECT MAX(id)+1 as id FROM User");
int id = table.first["id"];
//insert to the table using the new id
var raw = await db.rawInsert(
"INSERT Into User (id,first_name,last_name)"
" VALUES (?,?,?)",
[id, newUser.firstName, newUser.lastName]);
return raw;
}
newToken(Tokens newTokens) async {
final db = await database;
//await db.rawDelete("DELETE FROM Token");
//get the biggest id in the table
var table = await db.rawQuery("SELECT MAX(id)+1 as id FROM Token");
int id = table.first["id"];
//insert to the table using the new id
var raw = await db.rawInsert(
"INSERT Into Token (id,access_token,refresh_token)"
" VALUES (?,?,?)",
[id, newTokens.accessToken, newTokens.refreshToken]);
return raw;
}
Future<Tokens> getToken() async {
final db = await database;
var res = await db.query("Token", limit: 1);
return res.isNotEmpty ? Tokens.fromJson(res.first) : null;
}
Home page
class HomePage extends StatefulWidget {
HomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>{
#override
void initState() {
super.initState();
getHomePageStuff();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Home Page"),
),
body: Center(
child: RaisedButton(
onPressed: () {},
child: Text('Go back!'),
),
),
);
}
}
Future<HomePageStuffResponse> getHomePageStuff() async {
Tokens token = await DBProvider.db.getToken();
//Accessing the token here throws an NPE
var accessToken = token.accessToken;
debugPrint("token = " + accessToken);
final response = await http.get(..);
if (response.statusCode == 200) {
debugPrint("FETCH SUCCESS");
return stuff;
} else {
throw Exception('Failed to fetch home page stuff');
}
}
You can simply wrap Scaffold's body in FutureBuilder like this
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Home Page"),
),
body: FutureBuilder<HomePageStuffResponse>(
future: getHomePageStuff(),
builder: (context, snap) {
if(snap.hasError) {
return ErrorWidget('Error occurred while fetching data');
}
if(snap.hasData) {
return Center(
child: RaisedButton(
onPressed: () {},
child: Text('Go back!'),
),
);
}
}
),
);
}
}
Future<HomePageStuffResponse> getHomePageStuff() async {
Tokens token = await DBProvider.db.getToken();
//Accessing the token here throws an NPE
var accessToken = token.accessToken;
debugPrint("token = " + accessToken);
final response = await http.get(..);
if (response.statusCode == 200) {
debugPrint("FETCH SUCCESS");
return stuff;
} else {
throw Exception('Failed to fetch home page stuff');
}
}
Okay I was pretty close. Navigation is fine the way it is, the issue was the writing to the db was not being awaited on so that would happen simultaneously to the navigation (the newUser and newToken calls). As I would navigate to the home screen and try and read the access token the call would fail because it did not exist yet.
This was made a little harder to figure out because the debugger is a little strange in Android Studio for flutter so I just had to log everything to the console to see the issue.
If you read my question thank you for your time :)
I am getting an error that says that the method .length is calling on a null object _genreList.
I am using an async method to get data from a local asset sqlite database to which is a list of genre's. Which then I use ListView.builder in order to display that list on the screen. This is the code to obtain the data...
Future getGenreData() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, "asset_sample_sqlite.db");
ByteData data = await rootBundle.load(join("assets", "sample_sqlite.db"));
List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await new File(path).writeAsBytes(bytes);
Database db = await openDatabase(path);
_genreList = await db.rawQuery('SELECT genre_name[] FROM tbl_genres');
print(_genreList);
await db.close();
}
How do I use this method inside the build Widget method so that I can access the _genreList when I use ListView.builder? like so..
#override
Widget build(BuildContext context) {
return Scaffold(
body: new ListView.builder(
itemCount: _genreList.length, //need to access the genreList here
itemBuilder: (BuildContext context, int index) {
return new Card(
child: new ListTile(
title: new Text("${_genreList[index]}"),
onTap: () {
Navigator.push(context,
MaterialPageRoute(
builder: (context) => BookPage(id: index),
),
);
}
),
);
}
),
);
}
The end goal here is to display a list of genres (from the tbl_genres in my sqlite database) that will be able to pass through data to the next page to show a list of books (from the tbl_books in my sqlite database) related to that genre.
The whole point of programming asynchronously is that your user interface can stay alive while you are doing time consuming work in the background. So you need (and want) to display something like a CircularProgressIndicator or even a blank page (e.g. a Container), while the application is loading.
There are at least these two ways of doing that:
Make the widget stateful and introduce a state field loading, that you initialize to true and set to false when your data (in another field) is ready. Your code would look like that:
import 'package:flutter/material.dart';
class GenresPage extends StatefulWidget {
#override
_GenresPageState createState() => _GenresPageState();
}
class _GenresPageState extends State<GenresPage> {
bool loading;
List<String> genreNames;
#override
void initState() {
super.initState();
loading = true;
getGenreData();
}
Future getGenreData() async {
final genreData = await actuallyGetThoseNames();
setState(() {
genreNames = genreData;
loading = false;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: !loading ? new ListView.builder(
itemCount: genreNames.length,
itemBuilder: (context, index) {
return new Card(
child: new ListTile(
title: new Text("${genreNames[index]}"),
),
);
},
) : CircularProgressIndicator(), // or Container()
);
}
}
Use a FutureBuilder. Therefore you would need to refactor your getGenreData method to return the list as a Future<List<String>>.