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:
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.
The problem is here:
StreamBuilder(
stream: _database.collection("dorms").doc(dorm).snapshots(),
...
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
s initState
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.