Flutter FutureBuilder wait for database connection in SQFLite - flutter

I'm using SQFLite to store data in the flutter application. The database provider is
class MessageDatabaseProvider {
Database db;
Future open() async {
final path = join(await getDatabasesPath(), databaseName);
db = await openDatabase(
path,
version: 1,
onCreate: (Database db, int version) async {
await db.execute('''
create table $tableMessage (
$columnId integer primary key autoincrement,
$columnCountryCode text not null,
$columnPhoneNumber text not null,
$columnMessage text null,
$columnCreated integer null)
''');
}
);
}
Future<List<Message>> list() async {
List<Map> maps = await db.query(tableMessage,
columns: [columnId, columnCountryCode, columnPhoneNumber, columnMessage, columnCreated],
orderBy: columnId
);
List<Message> list = [];
maps.forEach((element) {
list.add(Message.fromMap(element));
});
return list;
}
}
and Building a ListView
class _MessageHistoryListState extends State<MessageHistoryList> {
final _database = MessageDatabaseProvider();
#override
Widget build(BuildContext context) {
return Center(
child: _buildMessageHistory(),
);
}
Widget _buildMessageHistory() {
_database.open();
return FutureBuilder<List>(
future: _getList(),
initialData: [],
builder: (context, snapshot) {
print(snapshot);
return snapshot.hasData ?
ListView.builder(
padding: EdgeInsets.all(16.0),
itemCount: snapshot.data.length,
itemBuilder: (context, i) {
return _buildRow(snapshot.data[i]);
},
)
: Center(
child: CircularProgressIndicator(),
);
},
);
}
}
It shows the list of items the first time, then after changing navigation it gives error
AsyncSnapshot<List<dynamic>>(ConnectionState.done, null, NoSuchMethodError: The method 'query' was called on null.

You cannot use openDatabase until you close it by using the following line
await db.close();
you should call _database.open(); from init() if you are not recreating the widget. Remove _database.open(); from _buildMessageHistory()

Related

Flutter FutureBuilder does not stop showing CircularProgressIndicator

I am trying to receive data using a FutureBuilder, but it hangs on the CircularProgressIndicator. I think it's remaining on ConnectionState.waiting but I'm not sure why.
#override
initState() {
_widgetList = getWidgetList();
}
Stream<List<String>> getFriendUUIDS() => Firestore.friends
.doc(gameManager.myself.uuid)
.snapshots()
.map((snapshot) => ((snapshot.data()?.keys)?.toList()) ?? []);
Future<List<MomentWidget>> getWidgetList() async{
List<MomentWidget> widgetList = [];
Set<String> momentIds = Set();
await for (final uuids in getFriendUUIDS()){
for (String uuid in uuids){
DocumentSnapshot<Map<String, dynamic>> values = await Firestore.users
.doc(uuid)
.get();
for (String momentId in values.data()?['moments'] ?? [] ){
momentIds.add(momentId);
}
}
}
for (String momentId in momentIds){
DocumentSnapshot<Map<String, dynamic>> values =
await Firestore.instance.collection('moments').doc(momentId).get();
Map<String, dynamic>? data = values.data()!;
String downloadURL = await storage.ref('moments/$momentId').getDownloadURL();
MomentWidget widget = MomentWidget(numLikes: data['liked_by'].length ,
location: data['location'],
date: data['timestamp'],
names: data['taggedFriends'].toString(),
shotBy: data['taken_by'], image: NetworkImage(downloadURL));
widgetList.add(widget);
}
return widgetList;
}
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Container(
height: size.height,
width: size.width,
child: FutureBuilder(
future: _widgetList,
builder: (context, AsyncSnapshot<List<MomentWidget>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
return ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemBuilder: (context, pos) {
return snapshot.data![pos];
},
);
}
case ConnectionState.waiting:
return Center(
child: CircularProgressIndicator(),
);
default:
return Text('Unhandled State');
}
}
),
);
}
I have tried to get the Future inside of initState(), and have tried to use snapshot.hasData instead, to no avail.
I have encountered a similar problem. When building an object from json , if the types don't match , it can quietly fail. I do not think your widgetList is ever returned. In my case I had a variable "cost" that I thought would be of type int , however in the database it was of type String. It always quietly failed and never showed the markers on the map widget
So:
Check how many times that loop of yours is executed. Probably only once and then it quietly fails
If the above happens:
Makes sure the types of your variables match the ones from the database. Comment out every variable one by one to find where the problem is.
Let me know if it works

Firestore how to fetch specific data with specific user id in Flutter

I have stream builder, and I fetch all the users. After that, using bloc (or any state management) I filter them. After filtering, I create a Set which has filtered user ids (I mean there is a set, and it has user ids).
Now, using with these uids I want to fetch filtered user datas. I did with FirebaseFirestore.instance.collection(...).doc(userId).get(), after that it gives Future<String?>. What should I do?
here is the codes:
class HomePageBody extends StatelessWidget {
HomePageBody({
Key? key,
required this.mapsState,
}) : super(key: key);
final MapsState mapsState;
final Set users = {};
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: firestoreStream,
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.waiting || snapshot.connectionState == ConnectionState.none) {
return const CustomProgressIndicator(
progressIndicatorColor: blackColor,
);
} else if (!snapshot.hasData) {
return const CustomProgressIndicator(
progressIndicatorColor: blackColor,
);
} else if (snapshot.hasData) {
final usersDatas = snapshot.data.docs;
for (var userDatas in usersDatas) {
if (userDatas["latitude"] == null || userDatas["longitude"] == null) {
} else {
users.add(userDatas);
}
}
context.read<MapsCubit>().filterUsersWithRespectToDistance(users: users);
final usersWithInTenKilometers = mapsState.usersWithInTenKilometers;
**// HERE WE HAVE FILTERED USERS, AND THIS SET HAS USER IDS.**
return ListView.builder(
padding: const EdgeInsets.only(top: 75),
itemCount: usersWithInTenKilometers.length,
itemBuilder: (context, index) {
final userId = usersWithInTenKilometers.elementAt(index);
final usersDatas = FirebaseFirestore.instance
.collection("users")
.doc(userId)
.get();
// I did like this, but it does not work.
return CustomListTile(
userImageUrl: "https://picsum.photos/200/300",
userStatus: "userStatus",
userName: "userName",
);
},
);
}
return const CustomProgressIndicator(
progressIndicatorColor: blackColor,
);
},
);
}
}
Consequently, I have a Set (or you can think like List), and it has user ids. Using these user ids, fetch user datas basically from the Firestore (email: ..., password: ... etc)
final userId = usersWithInTenKilometers.elementAt(index);
final users = FirebaseFirestore.instance
.collection("users")
.doc(userId)
.get()
.then((value) => value)
.then((value) => value.data());
return FutureBuilder(
future: users,
builder: (context, snapshot) {
if (snapshot.hasData) {
final convertUserDataToMap =
Map<String, dynamic>.from(snapshot.data as Map<dynamic, dynamic>);
final List userDataList = convertUserDataToMap.values.toList();
final userId = userDataList[0];
final userLong = userDataList[1];
....
I solved like this
Since you get back a Future<String?>, I'd typically first consider using a FutureBuilder to render that value.
If you have multiple values that each is loaded asynchronously separately (like is the case here with your multiple get() calls), I'd start with using a separate FutureBuilder for each Future. Only if I'd run into practical problems with that, would I start considering more complex options, such as Future.wait() to wait for all of them to complete before rendering any result.

Instant update of SQflite db when using Future methods in flutter

I was working on local storage in flutter using SQflite db and Used Future to extract the file from the db to display using ListTile but it does't update instantly like stream do when I insert a new value to the db.
// This method is from the database to get the tasks that has been entered!
{
Future<List<Model>> getTasks() async {
Database _db = await database();
List<Map<String, dynamic>> taskMap = await _db.query('tasks');
return List.generate(taskMap.length, (index) {
return Model(
id: taskMap[index]['id'],
name: taskMap[index]['name'],
fatherName: taskMap[index]['fatherName']);
});
}
}
```
// This is Future Builder to extract the data from the database
{
Expanded(
child: FutureBuilder(
future: _dbHelper.getTasks(),
builder: (context, snapshot) {
return ListView.builder(
itemCount: 3,
itemBuilder: (context, index) {
return ContactList(snapshot.data[index], index);
},
);
},
),
)
}
```
// This is the answer
{
// this is method in provider class to get the task inserted in the db
Future loadTaskList() async {
_isLoading = true;
notifyListeners();
_taskList = await db.getTasks();
_isLoading = false;
notifyListeners();
}
}
// call the provider class in the main.dart file like this
{
ChangeNotifierProvider(
create: (ctx) => InputData()..loadTaskList(),
),
}
// then just use Consumer or Provider when you access the methods.
**This work perfectly for me!**

Parameters from Stream<List> not being received in the MainPage

I'm trying to create a Stream, which will be called in the main page. This Stream returns me a list from my database. I will be using this list to create several cards in the main screen, and whenever there is a new card or a card removed, I will refresh the screen.
This is my Stream:
Stream<List> readData() async*{
Map<dynamic, dynamic> button_list = Map();
List lst = [];
final FirebaseUser user = await _auth.currentUser();
final lstValues = databaseReference.child(user.uid+"/buttons/").onValue.forEach((element) {
button_list = element.snapshot.value as Map;
lst = button_list.values.toList();
print(lst);
});
final lstStream = Stream.fromFuture(lstValues);
await for(var event in lstStream) {
yield lst;
}
}
This is the result from print(lst):
flutter: [{icon: delte, nome: Junior}, {icon: add, nome: Televisao}, {icon: bulb, nome: BAtata}]
This is the database:
This is the main screen with the main code:
body: StreamBuilder(
stream: _auth.readData(),
initialData: 0,
builder: (context, snapshot) {
if (snapshot.hasError || snapshot.hasError){
return Container(color: Colors.red);
}
if (!snapshot.hasData || !snapshot.hasData){
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasData || snapshot.hasData){
return GridView.count(
The problem is that the values are not being received in the Stream. In the main page. Whenever I try to use snapshot.data I get nothing. At the moment the only think is loading is the progress circular indicator, I'm not receiving the content from the Stream I have created.
Personally, I rather work with streams and rxdart than methods such as yield.
Within my firebase projects I use a construction like this:
// Get a database reference for the user
Future<DatabaseReference> _getUserRef() async {
final FirebaseUser user = await _auth.currentUser();
return FirebaseDatabase.instance
.reference()
.child('users')
.child(user.uid);
}
// Get a reference to a specific user node. In you cause buttons
Future<DatabaseReference> _getButtonsRef() async {
return (await _getUserRef()).child('buttons');
}
// Get the data as stream
Stream<List<MyButton>> getButtons() { // Not sure what data type you need
return _getButtonsRef().asStream()
.switchMap((ref) => ref.onValue) // Use on value to get new data if any changes
.map((event) => event.snapshot.value != null ? // Map the value to the object you want or return an empty list
MySnapshotMapper.buttonListFromSnapshot(event.snapshot.value) : List<MyButton>()
);
}
In case you wonder about the MySnapshotMapper:
class MySnapshotMapper {
static List<MyButton> buttonListFromSnapshot(Map snapshot) {
return List<MyButton>.from(snapshot.values.map((snap) => MyButton.fromSnapshot(snap)));
}
}
And of course the button:
class MyButton {
// Not sure which fields it should have
String name = '';
double width = 10.0, height = 10;
MyButton.fromSnapshot(Map snap) {
name = snap['name'] ?? ''; // Use the value in the Map or or use a default value if not found
width = snap['width']?.toDouble() || width;
height = snap['height ']?.toDouble() || height ;
}
}
Step 1:
class EmployeeRepository {
final CollectionReference collection =
FirebaseFirestore.instance.collection('employees');
Stream<QuerySnapshot> getStream() {
/// Based on Firebase.auth you can collect user data here and pass as
/// Stream<QuerySnapshot> like below.
return collection.snapshots();
}
Future<List<Employee>> buildData(AsyncSnapshot snapshot) async {
List<Employee> list = [];
/// Based on the user snapShot, you can convert into the List and return to
/// the futurebuilder
await Future.forEach(snapshot.data.docs, (element) async {
list.add(Employee.fromSnapshot(element));
});
return Future<List<Employee>>.value(list);
}
}
Step 2:
EmployeeRepository employeeRepository = EmployeeRepository();
#override
Widget build(BuildContext context) {
Widget loadProgressIndicator() {
return Container(
child: Center(child: CircularProgressIndicator()),
);
}
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('ListView'),
),
body: StreamBuilder<QuerySnapshot>(
stream: employeeRepository.getStream(),
builder: (context, snapShot) {
if (snapShot.hasError ||
snapShot.data == null ||
snapShot.data.docs.length == 0 ||
snapShot.connectionState == ConnectionState.waiting) {
return loadProgressIndicator();
} else {
return FutureBuilder(
future: employeeRepository.buildData(snapShot),
builder: (context, futureSnapShot) {
if (futureSnapShot.hasError ||
futureSnapShot.connectionState ==
ConnectionState.waiting ||
futureSnapShot.data.length == 0) {
return loadProgressIndicator();
} else {
return ListView.builder(
itemBuilder: (context, index) {
final employee = futureSnapShot.data[index];
return ListTile(
title: Text(employee.employeeName),
);
},
);
}
});
}
})));
}
This what I think has happened and which is why the code is not working as expected:
onValue function of the DocumentReference provides a Stream<Event> according to the latest documentation.
Stream<Event> onValue
But since the forEach returns a Future it is counted and used as a Future & then converted to a Stream by using Stream.fromFuture()
Future forEach(void action(T element))
While as forEach Returns a future, when completed it returns null as final value to the future.
Future forEach(void action(T element)) {
_Future future = new _Future();
StreamSubscription<T> subscription =
this.listen(null, onError: future._completeError, onDone: () {
future._complete(null);
}, cancelOnError: true);
subscription.onData((T element) {
_runUserCode<void>(() => action(element), (_) {},
_cancelAndErrorClosure(subscription, future));
});
return future;
}
Finally the lst being returned instead of the event in the final for loop.
await for (var event in lstStream) {
yield lst;
}
You can improve this code to make it work as following.
Stream<List> readData(user) async*{
final lstValues = databaseReference.child(user.uid+"/buttons/").onValue.map((element) {
Map button_list = element.snapshot.value as Map;
List lst = button_list.values.toList();
print(lst);
return lst;
}).toList();
final lstStream = Stream.fromFuture(lstValues);
await for(var event in lstStream) {
yield event;
}
}
Check that I have made following changes:
replaced forEach with map
[Optional change] taken Firebase user as method dependency as it is not required to be fetched on every iteration
[Optional change] moved lst & button_list inside the map execution block
I have not tested this code due to Firebase database dependency, but I have tested the theory on which this solution is based off of.
Here is the sample which I have tested:
Stream<List> readData() async* {
final list = Stream.fromIterable([
['a'],
['a', 'b'],
['a', 'b', 'c'],
['a', 'b', 'c', 'd']
]).map((element) {
print(element);
return element;
}).toList();
final listStream = Stream.fromFuture(list);
await for (var event in listStream) {
yield event;
}
}
I have replaced the Firebase document with a list of strings to make provide as much as resemblance as possible.
So in theory,
Stream.fromIterable([
['a'],
['a', 'b'],
['a', 'b', 'c'],
['a', 'b', 'c', 'd']
]) // Stream<List<String>> which can be similar to a list of documents
can replace
databaseReference.child(user.uid+"/buttons/").onValue // Stream<Event> which has a list of documents
Since FirebaseDatabase does not provide a stream of results you should use, Cloud FireStore
Here is the implementation of your code using cloud_firestore: ^0.16.0.
You will need to use subCollections for replicated the exact structure as RealTime Database.
1.Create a datamodel for the data you want to store and retrieve from firestore to made things easier.
class ButtonData{
final String name, icon;
ButtonData({this.name, this.icon});
}
Create a Stream that returns a list of documents from cloud firestore subCollection.
Stream<List<ButtonData>> getData(){
return users
.doc(FirebaseAuth.instance.currentUser.uid)
.collection('buttons').snapshots().map(buttonsFromQuerySnapshot);
}
Create a function that converts QuerySnapshot from firestore to a list of required objects. buttonsFromQuerySnapshot
List<ButtonData> buttonsFromQuerySnapshot(QuerySnapshot querySnapshot){
return querySnapshot.docs.map((DocumentSnapshot snapshot) {
return ButtonData(name: snapshot.data()['name'].toString(), icon: snapshot.data()['icon'].toString());
}).toList();
}
Use a streamBuilder to show results from the stream.
StreamBuilder<List<ButtonData>>(
stream: getData(),
builder: (context, snapshot){
if (snapshot.hasData){
final List<ButtonData> buttons = snapshot.data;
return ListView.builder(itemBuilder: (context, index){
return Column(
children: [
Text(buttons[index].name),
Text(buttons[index].icon),
],
);
});
}
return const Center(child: CircularProgressIndicator(),);
}),
I would recommend you to store icons as integer values. Here you can
find a list of Material Icons and their integer values.
You can then display icons using their retrieved integer values. See
this answer https://stackoverflow.com/a/59854460/10285344 (Haven't
tried this)
I solved a very similar problem about loading the functions a user can execute according to their profile to build the interface. It's basically handling an async and futures issue. For me, Provider made the deal. I will try to put everything in order and paste my code for reference, note I did not have to make changes in the state, I just needed the initial information:
Create a multiprovider for your app
Define the Provider to call your API to get the initial information of the cards.
Pass this information as a parameter to your widget using Provider.of
Use this provider info in InitState()
Options for managing changes... Copy the provider info into an object you can handle or define API calls to your provider to update changes dynamically (I did not went through this)
Check relevant parts of code you may be interested in:
Provider class and API call:
class UserFunctionProvider {
Future<List<UserFunction>> loadUserFunctions() async {
return await APICall.profileFunctions();
}
}
static Future<List<UserFunction>> profileFunctions() async{
List<UserFunction> functionList = [];
UserFunction oneFunction;
final cfg = new GlobalConfiguration();
final token = window.localStorage["csrf"];
var res = await http.get('${cfg.get('server')}:${cfg.get('port')}/get_user_functions',
headers: {
'Content-type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token'
}
);
int i = 0;
jsonDecode(res.body).forEach((element) {
oneFunction = new UserFunction.fromJson(element);
oneFunction.tabControllerIndex = i;
i++;
functionList.add(oneFunction);
});
return functionList;
}
Defining a Multiprovider and passing it to the relevant widget (it was home in my case)
void main() async {
GlobalConfiguration().loadFromMap(AppConfiguration.appConfig);
Logger.root.level = Level.ALL; // defaults to Level.INFO
Logger.root.onRecord.listen((record) {
print(
'${record.level.name}: ${record.time}: ${record.loggerName}: ${record.message}');
});
WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = (FlutterErrorDetails details) {
FlutterError.dumpErrorToConsole(details);
if (kReleaseMode)
exit(1);
};
runApp(
MultiProvider(
providers: [
FutureProvider(create: (_) => UserFunctionProvider().loadUserFunctions()),
],
child: MyApp()
)
);
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
String myLocale;
try{
myLocale = Platform.localeName;
}catch(e){
myLocale = 'es_ES';
print('Language set to Spanish by default.\n Error retrieving platform language: $e');
}
initializeDateFormatting(myLocale, null);
return MaterialApp(
title: 'Sanofi admin',
theme: ThemeData(primarySwatch: Colors.blue),
home: VerifySession().loadScreen(HomeScreen(Provider.of<List<UserFunction>>(context)))
);
}
}
Receiving the parameter from the provider into the Widget (as listOfUserFunction):
class HomeScreen extends StatefulWidget {
HomeScreen(this.listOfUserFunction);
final List<UserFunction> listOfUserFunction;
#override
HomeScreenState createState() => HomeScreenState();
}
class HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin {
final log = Logger('HomeScreenState');
TabController tabController;
int active = 0;
UserFunction oneFunction;
#override
void initState() {
super.initState();
tabController = new TabController(vsync: this, length: widget.listOfUserFunction.length, initialIndex: 0)
..addListener(() {
setState(() {
active = tabController.index;
});
});
}

Flutter - How do I use await inside the streambuilder?

I want to use await inside streambuilder. However, if you use async inside, you get an error. On the code below !!!!!!!! That's the part I want to solve. Thank you very much if I can tell you how.
class _MemoStreamState extends State<MemoStream> {
final _fireStore = Firestore.instance;
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _fireStore
.collection(widget.logInUsrEmail)
.orderBy('id', descending: false)
.snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
final memos = snapshot.data.documents;
List<MemoMaterial> memoList = [];
for (var memo in memos) {
final memoDocumentID = memo.documentID;
final memoTitle = await PlatformStringCryptor().decrypt(memo.data['title'], _key); !!!!!!!!!!
final memoUsrID = memo.data['usrID'];
final memoUsrPW = memo.data['usrPW'];
final memoText = memo.data['text'];
final memoCreateTime = memo.data['createTime'];
final memoMaterial = MemoMaterial(
logInUsrEmail: widget.logInUsrEmail,
doc: memoDocumentID,
title: memoTitle,
usrID: memoUsrID,
usrPW: memoUsrPW,
text: memoText,
createTime: memoCreateTime,
);
memoList.add(memoMaterial);
}
return Expanded(
child: new ListView.builder(
You should do something like this :
Stream<List<MemoMaterial>> memosStream;
Future<MemoMaterial> generateMemoMaterial(Memo memo) async {
final memoTitle =
await PlatformStringCryptor().decrypt(memo.data['title'], _key);
return MemoMaterial(
logInUsrEmail: widget.logInUsrEmail,
doc: memo.documentID,
title: memoTitle,
usrID: memo.data['usrID'],
usrPW: memo.data['usrPW'],
text: memo.data['text'];,
createTime: memo.data['createTime'],
);
}
#override
void initState() {
memosStream = _fireStore
.collection(widget.logInUsrEmail)
.orderBy('id', descending: false)
.snapshots()
.asyncMap((memos) => Future.wait([for (var memo in memos) generateMemoMaterial(memo)]));
super.initState();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<List<MemoMaterial>>(
stream: memosStream // Use memostream here
asyncMap() will "transform" every new set of Documents into a list of MemoMaterial, and emit this list into the stream when the action is performed.
Future.wait() allows to perform multiple async requests simultaneously.
You can do it using FutureBuilder inside StreamBuilder in following way.
Stream<List<int>> callme() async* {
yield [1, 2, 3, 4, 5, 6];
}
buildwidget() async {
await Future.delayed(Duration(seconds: 1));
return 1;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: StreamBuilder(
stream: callme(),
builder: (_, sna) {
if (sna.hasData) {
return FutureBuilder(
future: buildwidget(),
builder: (_, snap) {
if (snap.hasData) {
return ListView.builder(
itemCount: sna.data.length,
itemBuilder: (_, index) {
return Text("${sna.data[index]} and ${snap.data}");
},
);
} else {
return CircularProgressIndicator();
}
},
);
} else {
return CircularProgressIndicator();
}
}),
),
);
}
I will prefer to use Getx or Provider State management to Handle the UI if it depends on the async function.
Suppose you want to fetch data from firebase using StreamBuilder() which returns some docs which contains image links then you want to download these images and show from storage. Obviously downloading the image is async type of work. Then you will get error if you show the images with the links you get direct from StreamBuilder().
What you can do is set a variable in getx or provider to show or hide the image Widget. If the Image is being downloaded or not downloaded then set the variable to hide/show the image when the async type of function is completed.