This how my database looks like
I’m making a laundry timer for dormitory students in my school. But I’m facing some kind of weird runtime errors.
I was trying to make my MachineCard
widget automatically update when the data changes in firestore.
And it worked pretty well until I implemented NFC features in my app.
My app works like this:
- Select the dorm you live in
- If you are currently not using a machine (rather its washer or dryer), you need to scan an NFC tag which is pasted on the machine.
- A bottom sheet comes out when the phone scans the NFC tag, and you need to chose the duration.
- Pressing start button, you will see the timer turned on in the main page.
Since it is organically connected with many files, I’ll share my github repo. my github repo
The main error causing code is below.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:laundryminder/utils/prefs.dart';
import 'package:laundryminder/widgets/machine_card.dart';
import 'package:laundryminder/widgets/title_text.dart';
class MainPage extends StatefulWidget {
const MainPage({
super.key,
});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
final _database = FirebaseFirestore.instance;
String dorm = Prefs.getStringValue("dorm");
String current = Prefs.getStringValue("current");
@override
Widget build(BuildContext context) {
double screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
body: Column(children: [
SizedBox(
height: screenWidth * 0.25,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: screenWidth * 0.08),
child: TitleText(
data: "Currently using",
fontSize: screenWidth * 0.07,
),
),
],
),
StreamBuilder(
stream: _database.collection("dorms").doc(dorm).snapshots(),
builder: (context, snapshot) {
return MachineCard(widthArg: screenWidth, machine: const {});
}),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: screenWidth * 0.08),
child: TitleText(
data: "Machines",
fontSize: screenWidth * 0.07,
),
),
],
),
StreamBuilder(
stream: _database.collection("dorms").doc(dorm).snapshots(),
builder: (context, snapshot) {
if (snapshot.hasData) {
int len = snapshot.data!["machines"].length;
List<dynamic> data = snapshot.data!["machines"];
return Expanded(
child: ListView.builder(
itemCount: len,
itemBuilder: (context, index) {
print("test");
Map<String, dynamic> machineData = data[index];
if (machineData["type"] + '${machineData["code"]}' ==
current) {
return Container();
}
int remainingTime;
bool isRunning = machineData["isRunning"];
Timestamp timestamp = machineData["startedAt"];
DateTime startedAt = timestamp.toDate();
switch (machineData["option"]) {
case 0:
remainingTime = 45 * 60 -
(DateTime.now().difference(startedAt)).inSeconds;
if (remainingTime <= 0) {
remainingTime = 0;
isRunning = false;
}
break;
case 1:
remainingTime = 50 * 60 -
(DateTime.now().difference(startedAt)).inSeconds;
if (remainingTime <= 0) {
remainingTime = 0;
isRunning = false;
}
break;
case 2:
remainingTime = 80 * 60 -
(DateTime.now().difference(startedAt)).inSeconds;
if (remainingTime <= 0) {
remainingTime = 0;
isRunning = false;
}
break;
default:
return Container();
}
Map<String, dynamic> machine = {
"type": machineData["type"],
"code": machineData["code"],
"isCurrent": false,
"isDisabled": machineData["isDisabled"],
"isRunning": isRunning,
"remainingTime": remainingTime,
};
return Padding(
padding: EdgeInsets.symmetric(
horizontal: screenWidth * 0.08),
child: MachineCard(
widthArg: screenWidth, machine: machine),
);
},
),
);
} else {
return Container();
}
}),
]),
);
}
}
Also, it didn’t give me errors when I was manually parsing the "startedAt" part, since its db format was just string.
(Yet it was not successful because it required to rebuild "main-page" again to reflect the change even though it works fine without refreshing when I change other fields in db like "isDisabled" or "isRunning" and the machine types)
However when I changed it into Timestamp, it started to make crazy many queries.
EDIT:
class _MainPageState extends State<MainPage> {
final _database = FirebaseFirestore.instance;
String dorm = Prefs.getStringValue("dorm");
String current = Prefs.getStringValue("current");
late Stream stream;
@override
void initState() {
stream =
FirebaseFirestore.instance.collection("dorms").doc(dorm).snapshots();
super.initState();
}
@override
Widget build(BuildContext context) {
double screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
body: Column(children: [
SizedBox(
height: screenWidth * 0.25,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: screenWidth * 0.08),
child: TitleText(
data: "Currently using",
fontSize: screenWidth * 0.07,
),
),
],
),
StreamBuilder(
stream: stream,
builder: (context, snapshot) {
return MachineCard(widthArg: screenWidth, machine: const {});
}),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: screenWidth * 0.08),
child: TitleText(
data: "Machines",
fontSize: screenWidth * 0.07,
),
),
],
),
StreamBuilder(
stream: stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
int len = snapshot.data!["machines"].length;
List<dynamic> data = snapshot.data!["machines"];
...
I changed as the first person told me, but still its sending extraordinary requests.
Moreover, I found that the trigger of this situation is in the submit button widget.
void onPressed() {
Map<String, dynamic> current = Prefs.getMapValue("current");
String currentDorm =
["Men A", "Men B", "Women A", "Women B"][current["dorm"]];
bool matches = Prefs.getStringValue("dorm") == currentDorm;
if (matches) {
database
.collection("dorms")
.doc(currentDorm)
.snapshots()
.listen((event) {
List<dynamic> response = event.data()!["machines"];
for (int i = 0; i < response.length; i++) {
if (response[i]["type"] == ["Washer", "Dryer"][current["type"]] &&
response[i]["code"] == current["code"]) {
response[i]["option"] = Prefs.getIntValue("option");
Timestamp now = Timestamp.fromDate(DateTime.now());
response[i]["startedAt"] = now;
response[i]["isRunning"] = true;
response[i]["isDisabled"] = false;
break;
}
}
database
.collection("dorms")
.doc(currentDorm)
.set({"machines": response}, SetOptions(merge: true)).onError(
(error, stackTrace) => print(error));
});
}
}
This is my onpressed function in my submit button widget. For more information, you can look at my MachineCard
widget and SubmitButton
widget in my github repo.
I guess that I did something wrong on my database connection code in the function, but the weird thing is it triggers the StreamBuilder
widget, which I fixed, to malfunction.
2
Answers
after that, changing the onPressed method like this fixed everything. I guess the snapshot listening triggered the loop as the process for setting data was inside the code block of listening.
The problem is here:
The
StreamBuilder
is re-built every time the UI is rendered, so every time that happens your code goes to the database and requests all dorms. There’s a lot of situations that cause the UI to re-render, and in many of those cases the dorm data likely hasn’t changed.So you’ll want to:
State
sinitState
methodWith these steps, the stream will be kept between renders – and the additional rendering operations will end up rendering the documents that were already loaded.