Load Data lazily from firestore document in Flutter - flutter

For performance increase I want to load the data from a firestore document lazily. Therefore I use a PaginatedDataTable. In the rowList I have all the data from a firestore document which I want to show in the PaginatedDataTable. My question is does the lazy Loading from PaginatedDataTable work with Firestore?
/// DataTable Source
class DTS extends DataTableSource {
List<DataRow> rowList;
DTS(List<DataRow> rowList) {
this.rowList = rowList;
}
#override
DataRow getRow(int index) {
return this.rowList[index];
}
#override
int get rowCount =>
this.rowList.length;
#override
bool get isRowCountApproximate => false;
#override
int get selectedRowCount => 0;
}

From what I can quickly tell the standard PaginatedDataTable/DataTableSource classes don't natively support Firestore or any specific data source, but you'll have to implement those bindings yourself.
Explaining how to do that is a bit beyond what we can typically do here on Stack Overflow, but this search shows some promising results.

Related

Flutter using Provider - context.watch<T>() for specific items in a list and ignores the other items updates

I am a newbie in Flutter and I am trying to build an app using Provider. I will try to provide an oversimplified example here. My app includes a model of a room.
class Room {
String roomDisplayName;
String roomIdentifier;
Image image;
List<IDevices> devices = [];
Room(this.roomDisplayName, this.roomIdentifier, this.image, this.devices);
}
Rooms have list of devices like a temperature sensor
class TempSensor implements IDevices {
late String tempSensorName;
late double temperatureValue;
late double humidityValue;
late int battery;
TempSensor(this.displayName, this.zigbeeFriendlyName);
UpdateTempSensor(double temperature, double humidiy, int battery) {
this.temperatureValue = temperature;
this.humidityValue = humidiy;
this.battery = battery;
}
I have a RoomProvider class that implements ChangeNotifier that is responsible for updating devices in List<Room> rooms
class RoomsRepositoryProvider with ChangeNotifier {
List<Room> get rooms {
//return _rooms;
return _rooms;
}
UpdateTemperatureSensor(TempSensor tempSensor) {
TempSensor? foundTempSensor = null;
_rooms.forEach((room) {
room.devices.forEach((element) {
if (element.displayName == tempSensor.displayName) {
foundTempSensor = element as TempSensor;
}
});
});
if (foundTempSensor != null) {
foundTempSensor?.UpdateTempSensor(tempSensor.temperatureValue,
tempSensor.humidityValue, tempSensor.battery);
notifyListeners();
}
}
I also have a Stateful widget page to show Room information like temperature/humidity value.
class DetailPage extends StatefulWidget {
final Room room;
DetailPage({required this.room});
#override
_DetailPageState createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
#override
Widget build(BuildContext context) {
context.watch<RoomsRepositoryProvider>().rooms;
return Text ("Temperature is ${widget.room.devices[0].temperatureValue}");
}
Here is question:
The problem I am facing is that, if I am showing the Living Room in DetailPage and the temperature sensor from Bedroom gets updated in the List<Room> rooms, the whole DetailPage gets rebuild. Since it is not an issue in the flutter and the app works good. I would still like to know how to solve this architecture problem, that the DetailPage only gets build for the room updates related to the room being shown?
PS: please ignore any build, indentation or naming convention mistakes.
To only rebuild the specific widget, you can wrap that widget inside Consumer widget provider by Provider in flutter. Consumer takes a builder function and will build the widget returned by this builder function only when the data changes.
Consumer(
builder:(context,_,__){
return Container();
},
),
To implement this, you can use a Comsumer widget
Consumer<RoomsRepositoryProvider>(
builder:(context,value,child) => Text("Temperature is ${value.room}");
),
A StatelessWidget is also sufficient. Don't forget the index by room. It should work like this
So, I solved my problem by creating a separate provider DevicesProvider that contains the list of devices modified in the room. I provide the current room by calling the method SetCurrentRoom(String currentRoomIdentifier) from the DetailPage and the provider does its job whenever the devices list in the current room updates.
class DevicesProvider with ChangeNotifier {
String _currentRoomIdentifier = "";
List<IDevices> _listCurrentRoomDevices = [];
List<IDevices> get ListCurrentRoomDevices => _listCurrentRoomDevices;
void SetCurrentRoom(String currentRoomIdentifier) {
_currentRoomIdentifier = currentRoomIdentifier;
}
UpdateDevicesList(IDevices device) {
if (serviceLocator<RoomProviderService>()
.rooms
.any((room) => room.roomIdentifier == _currentRoomIdentifier)) &&
IsDeviceUpdateComingFromCurrentRoom(device)
{
_listCurrentRoomDevices.clear();
var devices = serviceLocator<RoomProviderService>()
.rooms
.firstWhere(
(room) => room.roomIdentifier == _currentRoomIdentifier)
.devices;
_listCurrentRoomDevices.addAll(devices);
notifyListeners();
}
}
bool IsDeviceUpdateComingFromCurrentRoom(IDevices device) {
bool isUpdateFromCurrentRoom = false;
if (device.Name.contains(_currentRoomIdentifier)) {
isUpdateFromCurrenRoom = true;
}
return isUpdateFromCurrentRoom;
}
}
Maybe this can be solved in a different way which is more elegant or efficient, but for now my problem is solved with this approach.

Flutter: Realtime database appends list when listens changes

I'm using flutter and firebase realtime database.I'm trying to read data from a specific node.I'm saving the data that I am collecting in the Orderlist class and then I return a Future List of Ordelist.This Future function I am trying to use on another widget.I want to display on screen every time data is updated.
Future<List<Orderlist>> order() async{
String business =await businessname();
List table = await tables();
List<Orderlist> list = [];
table.forEach((element) async{
String payment_method = '';
String payment_state ='';
var snapshot = ref.child(business).child(element.toString()).onValue.listen((event) {
event.snapshot.children.forEach((method) {
if(method.key=='payment_method') payment_method=method.value.toString();
if(method.key=='payment_state') payment_state = method.value.toString();
});
final order = Orderlist(payment_method: payment_method,payment_state: payment_state);
list.add(order);
});
});
return list;
}
The problem is that at first place the data are loaded on screen but when I am trying to update the data for some reason the list is appended whereas I just want to replace the previous data with the updated data.To be more specific if I want to listen to 2 nodes to be updated I will have a list with 2 Orderlist items.But the problem is when I update one of them the list is expanded to 3 Orderlist items.
Here is the widget where I am trying to use the Future function
first data loaded Updated da
class TempSettings extends StatefulWidget {
const TempSettings({super.key});
#override
State<TempSettings> createState() => _TempSettingsState();
}
class _TempSettingsState extends State<TempSettings> {
String? business;
List<Orderlist> list=[];
final user = FirebaseAuth.instance.currentUser;
#override
void initState() {
// TODO: implement initState
g();
super.initState();
}
void g() async{
list = await DatabaseManager(user_uid: user!.uid).order();[![[![enter image description here](https://i.stack.imgur.com/hn2NQ.png)](https://i.stack.imgur.com/hn2NQ.png)](https://i.stack.imgur.com/SJ4M1.png)](https://i.stack.imgur.com/SJ4M1.png)
}
#override
Widget build(BuildContext context) {
return Column(children: list.map((e) => ListTile(title: Text(e.payment_method!),)).toList(),);
}
}
When you listen to data in Firebase with onValue, the event you get contains a snapshot of all data. Even when only one child node was added/changed/removed, the snapshot will contain all data. So when you add the data from the snapshot to list, you are initially adding all child nodes once - but then on an update, you're adding most of them again.
The easiest way to fix this is to empty the list every time onValue fires an event by calling list.clear().

Why can't I get last document from cloud firestore in flutter using provider package?

I am banging my head with this and I need your help guys. Please help me with this.
I am currently getting streams from firestore and it's working fine, but the problem is I want to implement pagination now and currently, I can't get the value of the last document which is why I can't use startAfter feature. Have a look into my code
Code on parent page i.e. homepage.dart
StreamProvider<List<Cars>>.value(
value: DatabaseService().getCars(),
catchError: (ctx, err) => null,
child: ChangeNotifierProvider(
create: (context) => LastDocumentTracker(),
child: Scaffold()
Code on database Service page:
getCars({bool getMore = false}) {
var collection = carsCollection.orderBy('dueDate').limit(15);
if(!getMore ) {
return collection.snapshots().map((event) {
LastDocumentTracker().changeLastDocument(event.docs.last);
return _carsListFromSnapshot(event);
});
}
}
Now I got a class with ChangeNotifier
class LastDocumentTracker with ChangeNotifier{
List <QueryDocumentSnapshot> _snapshot = [];
QueryDocumentSnapshot get getLastDocument {
return _snapshot.last;
}
void changeLastDocument (QueryDocumentSnapshot doc){
print('Snapshot $_snapshot'); // here I can see the snapshot on console but on other pages where I am listinig its null.
_snapshot.add(doc);
notifyListeners();
}
}
I was thinking to get the value of the last document from the getter getLastDocument however I am unable to get it because it's always null.
Please help me to implement pagination because I don't want a whole bunch of data to be accessed by users at once.
Every time you do LastDocumentTracker(), you are creating a new instance of LastDocumentTracker with _snapshot = []. Hence, you are getting the last element as null. Convert LastDocumentTracker into a singleton:
class LastDocumentTracker with ChangeNotifier{
static LastDocumentTracker _instance;
List <QueryDocumentSnapshot> _snapshot;
LastDocumentTracker._construct() {
_snapshot = [];
}
factory LastDocumentTracker() {
if(_instance == null) _instance = LastDocumentTracker._construct();
return _instance;
}
QueryDocumentSnapshot get getLastDocument {
return _snapshot.last;
}
void changeLastDocument (QueryDocumentSnapshot doc) {
_snapshot.add(doc);
notifyListeners();
}
}
Edit
As you mentioned about the providers, it is better not to go with the singleton answer I provided. Instead, you can replace this:
LastDocumentTracker().changeLastDocument(event.docs.last);
with
final tracker = Provider.of<LastDocumentTracker>(context, listen: false);
tracker.changeLastDocument(event.docs.last);
This way, you are accessing the tracker instance that your provider holds. This is better than the singleton pattern I mentioned as it makes the class reusable using the provider.
Note:
You need context to access provider of that context so pass the context to the getCars method from wherever you are calling it.
set listen to false otherwise, you won't be able to access getCars from methods like buttonPress callbacks or initState etc.

How to Stream data and add to list outside of Build Function using Flutter

I have a Map() called myData that holds multiple lists. I want to use a Stream to populate one of the lists in the Map. For this StreamBuilder will not work as it requires a return and I would like to use List.add() functionality.
Map<String, List<Widget>> myData = {
'list1': [],
'list2': [],
'list3': [],
'list4': []
};
How can I fetch information from FireStore but add it to the list instead of returning data?
Like this but this wouldn't work.
StreamBuilder<QuerySnapshot>(
stream: // my snapshot from firestore,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
snapshot.data.documents.map((DocumentSnapshot doc) {
myData['list1'].add(Text(doc['color']));
});
},
),
Any help would be appreciated!
StreamBuilder does not fit for this task. Even if you manage to do it (actually there is a way :) )- it might be rebuilt by higher level widgets without new data and you will end up with duplicates in list.
All the WidgetBuilders and build methods in widgets serve only for displaying UI
You need to subscribe to a stream. If you want to do it using widget, then you need to create a custom widget extending StatefulWidget. StatefulWidget state has lifecycle methods (initState and dispose) so it will allow to correctly manage StreamSubscription.
Here is example code:
class StreamReader extends StatefulWidget {
#override
_StreamReaderState createState() => _StreamReaderState();
}
class _StreamReaderState extends State<StreamReader> {
StreamSubscription _subscription;
#override
void initState() {
super.initState();
_subscription = myStream.listen((data) {
// do whatever you want with stream data event here
});
}
#override
void dispose() {
_subscription?.cancel(); // don't forget to close subscription
super.dispose();
}
#override
Widget build(BuildContext context) {
// return your widgets here
}
}

How to retain data while using ScopedModel in Flutter?

I am using ScopedModel to fetch some data from a Firebase database.
I am fetching a list of events.
I fetch the events from the endpoint inside the Model;
I store the events into a List<Event> inside the model;
I use that list to build my ListView.
mixin EventModel on Model {
List<Event> _events = [];
Future<http.Response> fetchEvents() async {
http.Response response = await http.get(//Url);
final List<Event> fetchedEvents = [];
... // decode response data into fetchedEvents
// Add the loaded data to my List
_events = fetchedEvents;
notifyListeners();
...
}
}
So, when opening the EventsPage the first thing I do is to fetch the data in initState().
class _EventPageState extends State<EventPage> {
#override
void initState() {
super.initState();
widget.model.fetchEvents();
}
}
}
After fetching the network data, my List inside my app has the network data so I can use it to build my ListView.
EventsPage.dart
Widget _buildListView(MainModel model) {
return Center(
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ItemEventBig(model.events[index], index);
},
itemCount: model.events.length,
),
);
}
My problem is that, if I navigate to another page and then come back to EventsPage, initState() will be called again, so fetchEvents. Making the app reload all the events list again.
I would like to retain the downloaded data while my app is alive, so If the user go and come back to EventsPage the data will not be lost.
I was used to do it in Android using ViewModel, how to do it in Flutter?
I want to keep using ScopedModel to do my State Management in Flutter.
Possible Solution
I thought that a solution would be to store the events in a List<Event> as I am doing. Then, when calling fetchEvents() I could first check if my List<Event> is not null if so, I don't need to call it again because data was already loaded.
This seems a bad solution for me, especially when I have multiple pages fetching the data. Suppose I load the first one, when I go to the second one it will assume the data was already loaded because List<Event> is non null and it will not load again.
See Flutter Documentation - https://api.flutter.dev/flutter/widgets/AutomaticKeepAliveClientMixin-mixin.html
class _EventPageState extends State<EventPage>
with AutomaticKeepAliveClientMixin<EventPage> {
#override
void initState() {
super.initState();
widget.model.fetchEvents();
}
}
#override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
}