LateInitializationError: Field 'database' has not been initialized - flutter

I’m building an app, so far everything works. Until I click on a button that calls this Staful Widget:
class ToDo1 extends StatefulWidget {
#override
_ToDo1State createState() => _ToDo1State();
}
class _ToDo1State extends State<ToDo1> {
var User;
late DatabaseService database;
Future<void> connectToFirebase() async{
await Firebase.initializeApp();
final FirebaseAuth auth = FirebaseAuth.instance;
UserCredential result = await FirebaseAuth.instance.signInAnonymously();
User = result.user;
database = DatabaseService(User.uid);
if (!(await database.checkIfUserExists())) {
database.setTodo('To-Do anlegen', false);
}
}
void toggleDone(String key, bool value) {
database.setTodo(key, !value);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Center(
child: Text(
'Stufe 1',
style: TextStyle(
fontStyle: FontStyle.italic,
decoration: TextDecoration.underline),
)),
backgroundColor: Color.fromRGBO(35, 112, 192, 1),
),
body: FutureBuilder(
future: connectToFirebase(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else {
return StreamBuilder<DocumentSnapshot> (
stream: database.getTodos(),
builder: (context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if(!snapshot.hasData) {
return CircularProgressIndicator();
} else {
Map<String, dynamic> items = snapshot.data!.data as Map<String, dynamic>;
return ListView.separated(
separatorBuilder: (BuildContext context, int index) {
return SizedBox(
height: 10,
);
},
padding: EdgeInsets.all(10),
itemCount: items.length,
itemBuilder: (BuildContext, i) {
String key = items.keys.elementAt(i);
return ToDoItem(
key,
items[key]!,
() => toggleDone(key, items[key]),
);
});
}
}
);
}
},
)
);
}
}
Then I am confronted with the following error:
The following LateError was thrown building FutureBuilder<void>(dirty, state: _FutureBuilderState<void>#bc115):
LateInitializationError: Field 'database' has not been initialized.
This is the class that interacts with the firebase:
class DatabaseService {
final String userID;
DatabaseService(this.userID);
final CollectionReference userTodos =
FirebaseFirestore.instance.collection('userTodos');
Future setTodo(String item, bool value) async {
return await userTodos.doc(userID).set(
{item:value}, SetOptions(merge: true));
}
Future deleteTodo(String key) async {
return await userTodos.doc(userID).update(
{key: FieldValue.delete(),}
);
}
Future checkIfUserExists() async {
if((await userTodos.doc(userID).get()).exists) {
return true;
}
else {
return false;
}
}
Stream<DocumentSnapshot> getTodos() {
return userTodos.doc(userID).snapshots();
}
}
I hope I have provided all the necessary data so that the problem can be solved. If not, just write it to me and I will try to send you the material you need.

Let try changing this code from
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else {
to
if (snapshot.connectionState != ConnectionState.done) {
return Center(child: CircularProgressIndicator());
} else {
In async.dart file you can see:
/// The state of connection to an asynchronous computation.
///
/// The usual flow of state is as follows:
///
/// 1. [none], maybe with some initial data.
/// 2. [waiting], indicating that the asynchronous operation has begun,
/// typically with the data being null.
/// 3. [active], with data being non-null, and possible changing over time.
/// 4. [done], with data being non-null.
///
/// See also:
///
/// * [AsyncSnapshot], which augments a connection state with information
/// received from the asynchronous computation.
enum ConnectionState {
/// Not currently connected to any asynchronous computation.
///
/// For example, a [FutureBuilder] whose [FutureBuilder.future] is null.
none,
/// Connected to an asynchronous computation and awaiting interaction.
waiting,
/// Connected to an active asynchronous computation.
///
/// For example, a [Stream] that has returned at least one value, but is not
/// yet done.
active,
/// Connected to a terminated asynchronous computation.
done,
}
You will miss ConnectionState.none and ConnectionState.active if you just compare with ConnectionState.waiting, so the Future isn't completed when you call .todos() in your stream and it will cause the issue.

I suggest you to check this answer in which they have the exact same issue as you do:
Future Builders are built even before getting the data. So, you should check whether it has data.
In that answer they suggest to use FutureBuilder in the following:
FutureBuilder<Position>(
future: getInitialPosition(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Map(snapshot.data);
}else{
return CircularProgressIndicator();
//Display loading, you may adapt this widget to your interface or use some state management solution
}
}
)

Related

How can I listen to changes of multiple providers from a future builder?

I want to display a BarChart where I use as source information two different providers.
My initial approach was to use a future builder where I show a loading icon while the data is being fetched, and then manipulate that data to suite my needs in the graph.
So I used a future builder from the widget where I display my graph and initialized it with a Future that will get the context from another reusable file.
my_wdiget.dart
...
class _MyWidgetState extends State<MyWidget> {
late Future<List<MyObject>> myFutureVariable;
Future<List<MyObject>> _getMyObjects() async {
return getMyObjects(context);
}
#override
void initState() {
super.initState();
myFutureVariable= _getMyObjects();
}
...
FutureBuilder<List<MyObject>>(
future: myFutureVariable,
builder: (BuildContext context,
AsyncSnapshot<List<MyObject>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator())),
);
default:
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'))),
);
} else if (snapshot.data == null) {
return Center(
child:
Text('You don\'t have any item yet.'))),
);
} else {
return BarChart(
_getChartData(snapshot.data!),
),
);
}
}
},
),
And this is the file where I generate the data:
my_object_utils.dart
Future<List<MyObject>> getMyObjects(BuildContext context) async {
await Future.delayed(Duration(seconds: 1)); // Simulation delayed query
var source1= Provider.of<MySource1>(context, listen: false).items;
var source2 = Provider.of<MySource2>(context, listen: false).otherItems;
List<MyObject> myObjects= [];
// Do some stuff to fill the myObjects using source1 and source2
return myObjects;
}
Problems:
This kind of works but I get the warning use_build_context_synchronously from the lines of the Provider.
I want to listen to changes, but if I set the default listen: true it will crash telling me to change that property.
So my question is, how can I have a FutureBuilder listening to changes of multiple providers?
Update using approach suggested #hydra:
If I have:
void test() {
print('a');
setState(() {});
}
Consumer2<MySource1, MySource1>(
builder: (context, sourceOne, sourceTwo, child) {
myFutureVariable = getMyObjects(sourceOne.items, sourceTwo.otherItems),
return FutureBuilder<List<MyObject>>(
future: myFutureVariable,
builder: (context, snapshot) {
...
else{
return child: ElevatedButton(
child: Text('a'),
onPressed: test,
);
}
}
),
},
),
Every time the button is pressed it will trigger the setState and and the circularProgressIndicator will appear although no changes were made in the consumers.
to solve both problems you can use Consumer2
the FutureBuilder will rebuild if either of the two provider changed
Consumer2<MySource1, MySource1>(
builder: (context, sourceOne, sourceTwo, child) {
return FutureBuilder<List<MyObject>>(
future: myFutureVariable(sourceOne.items, sourceTwo.otherItems),
builder: (context, snapshot) {
// ...
}
),
},
),
and update your function to:
Future<List<MyObject>> getMyObjects(final items, final otherItems) async {
// use your items and otherItems here.
await Future.delayed(Duration(seconds: 1)); // just for testing, right?
List<MyObject> myObjects= [];
// Do some stuff to fill the myObjects using source1 and source2
return myObjects;
}

Update StreamBuilder stream dynammically using a ChangeNotifier

I have the following build method of a statefull widget. I am trying to test updating a Firebase collection stream dynamically using a ChangeNotifier. The code is the following:
#override
Widget build(BuildContext context) {
final List<String> types = ["pop", "ballad", ''];
final SongStreamNotifier songStreamNotifier =
Provider.of<SongStreamNotifier>(context);
developer.log("Rebuilding Scaffold");
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
types.shuffle();
final songType = types.first;
songStreamNotifier.getSongsStream(songType);
},
),
body: StreamBuilder<QuerySnapshot>(
stream: songStreamNotifier.songs,
builder: (context, snapshot) {
developer.log("Rebuilding stream");
if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.size,
itemBuilder: (BuildContext ctx, int index) {
return Text(snapshot.data!.docs[index].get('name'));
});
} else {
return Container();
}
}),
);
}
Notifier is called correctly, I can see all log messages on how build is rerun, but the values do not change. The code of the ChangeNotifier is the following:
class SongStreamNotifier extends ChangeNotifier {
Stream<QuerySnapshot> stream = Stream.empty();
final SongService songService = SongService();
Stream<QuerySnapshot> get songs {
return stream;
}
getSongsStream(String type) {
developer.log('type: $type');
stream = songService.getSongs(type);
notifyListeners();
}
}
which is pretty straighforward: songService.getSongs filters the songs based on the songType.
I still cannot understand how providers work with updating the state of widgets. How come I see the builder method getting run but the StreamBuilder always fetches the same values?
Found what the issue was. I didn't call snapshots on the query but only on the initial collection reference. In my Repository i did this:
colRef = _db.collection("songs");
if (type != '') {
colRef.where('kind', isEqualTo: type);
}
return colRef.snapshots()
while I should have done
colRef = _db.collection("songs");
if (type != '') {
return colRef.where('kind', isEqualTo: type).snapshots();
}
return colRef.snapshots()
but I didn't know colRef was immutable.

Why does my async method run twice in Flutter?

I want to load a static list data when entering indexScreen,but the list sometimes show twice the same content,sometimes not.
This is my list setting:List<ListClass> listItems=List<ListClass>();,ListClass is a simple class with on different attributes and a constructor.
I use home:IndexScreen() in main.dart to show Index page.
return MaterialApp(
home: IndexScreen(),
debugShowCheckedModeBanner: false,
onGenerateRoute: router.generator,
builder: EasyLoading.init(),
);
And before this page build,it will update listItems using:
Future<bool> initUserAndIndex() async{
if (curUserEmail==null) sharedGetData(USER_EMAIL).then((value) => curUserEmail=value.toString());
print(curUserEmail);
await UserTable().getUserInfo(curUserEmail).then((value){print("user ok");});
await CollectionTable().getIndexList().then((value){print("Collection ok");return true;});
return null;
}
buildPage:
#override
Widget build(BuildContext context) {
return FutureBuilder<Object>(
future: initUserAndIndex(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState==ConnectionState.waiting)
{
EasyLoading.show(status: 'loading...');
// avoid no return,this cause a whiteborad transition,I don't know how to solve it,too.
return Container();
}
else
{
EasyLoading.dismiss();
return SafeArea(
child: Scaffold(
// the listItems is used in Body()
body: Body(),
),
);
}
},
);
}
}
I run this app,and it prints twice user ok and Collection ok.But when I use ROUTER.NAVIGETE,it only prints once.
User Information is OK,but the list is such a great problem--the page shows twice content
I put my code at an order of relevance of this prblom,I think.Next I put my the two awaited funtion here:
User:
Future<bool> getUserInfo(String userEmail) async{
await userCollection.where({'userEmail':userEmail}).get().then((res) async {
//assign to the static variables
return true;
});
return null;
}
Collection:
Future<bool> getIndexList() async {
listItems.clear();
await listCollection.get().then((value){
var v = value.data;
for (var data in v) {
//get data and package them,add after the listItems list.
listItems.add(ListClass(header, content, userId, favorCount, wordCount));
}
return true;
});
}
You probably want to assign your future in your widget class, but not in the build method as the documentation show, otherwise, everytime your build method is triggered, it will call again your FutureBuilder.
final Future<String> _calculation = Future<String>.delayed(
const Duration(seconds: 2),
() => 'Data Loaded',
);
#override
Widget build(BuildContext context) {
return DefaultTextStyle(
style: Theme.of(context).textTheme.headline2!,
textAlign: TextAlign.center,
child: FutureBuilder<String>(
future: _calculation, // a previously-obtained Future<String> or null
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
// ...
}
),
),
}

I am failing to get data from cloud firestore while using flutter

At first, when i started writing my calls to get data from firestore, it worked. But when i tried writing more docs to my collection, it failed to bring data for the docs i recently added. Then, when i deleted the first one i added, i stopped receiveing data from firestore all together. I have tried several methods, but have all ended in failure.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
class collect extends StatefulWidget {
#override
_collectState createState() => _collectState();
}
class _collectState extends State<collect>
{
Future _data;
void initState()
{
super.initState();
_data = getStuff();
}
Future getStuff()
async {
var firestore = FirebaseFirestore.instance;
QuerySnapshot qn = await firestore.collection("buses").get();
return qn.docs;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: _data,
builder: (_, snapshot)
{
if(snapshot.connectionState == ConnectionState.waiting)
{
return Center(
child:Text("Loading")
);
}
else if(snapshot.connectionState == ConnectionState.done)
{
return ListView.builder(itemCount: snapshot.data.length,itemBuilder:(_, index)
{
return Container(
child: ListTile(
title: Text(snapshot.data[index].data()["name"].toString()),
subtitle: Text(snapshot.data[index].data()["price"].toString()),
),
);
});
}
},
),
);
}
}
```![enter image description here](https://i.stack.imgur.com/L7FqF.jpg)
Define your database call as,
Future getStuff() async {
var docs;
await FirebaseFirestore.instance
.collection("buses")
.get()
.then((querySnapshot) {
docs = querySnapshot.docs;
});
return docs;
}
Then use the FutureBuilder in the build() function as,
return Scaffold(
body: Center(
child: FutureBuilder<dynamic>(
future: getStuff(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (_, index) {
return Container(
child: ListTile(
title: Text(
snapshot.data[index].data()["name"].toString()),
subtitle: Text(
snapshot.data[index].data()["price"].toString()),
),
);
});
} else {
return CircularProgressIndicator();
}
},
),
),
);
I wrapped the FutureBuilder inside a Center just for clarity, you may remove that Center widget.

how to convert Stream<T> to return only T?

I have a function named checkAuth() in my APIService class which checks whether there is token in my SharedPreferences. if there is a token it returns AuthenticatedState or else it returns NotAuthenticatedState. Running the below code doesn't have any AuthenticationState at the start. so I tried to add the checkAuth() in the seeded but it throws an error that Stream<AuthenticationState> can't be assigned to AuthenticationState.
How can I convert Stream<AuthenticationState> to AuthenticationState?
BehaviorSubject<AuthenticationState> _authStatus =
BehaviorSubject<AuthenticationState>();
Stream<AuthenticationState> get loginStream => _authStatus;
submit() async {
final String validEmail = _email.value;
final String validPassword = _password.value;
APIService.login(validEmail, validPassword)
.then((onValue) {
if (onValue is Success) {
_authStatus.add(AuthenticatedState());
} else {
_authStatus.add(NotAuthenticatedState());
}
});
}
This is for UI
return StreamBuilder(
stream: stream.loginStream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Container(
width: double.infinity,
height: double.infinity,
color: Theme.of(context).cardColor,
child: Center(
child: CircularProgressIndicator(
valueColor: const AlwaysStoppedAnimation(Colors.white),
),
),
);
}
if (snapshot.hasError || snapshot.data is NotAuthenticatedState) {
return Login();
}
if (snapshot.data is AuthenticatedState) {
return App();
}
return Container(width: 0, height: 0);
},
);
It doesn't display any thing on the screen because it doesn't have a value at start , I think so ...
I think you have to user StreamBuilder to get T form Stream<T>, because of the time cost, It's take a time when read data with SharedPreferences.
StreamBuilder<T>( /// T represent the type of fetched data, Assuming it's String
stream: /// put the stream var in there or function with stream as a return
builder:(BuildContext context, AsyncSnapshot<T> snapshot){
if(snapshot.hasData) /// Check If it finishes fetching the data
retrun Text( snapshot.data ); /// snapshot.data variable store the data that fetched
}
);
Check this page form more:
https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html
/// Update answer
You have to initial the data on SharedPreferences with:
SharedPreferences.getInstance().then((SharedPreferences sp) {
bool _testValue = sharedPreferences.getBool(spKey);
// will be null if never previously saved
if (_testValue == null) {
_testValue = false;
// set an initial value
}
});