Pass data from class to class - Flutter - flutter

I have a List Builder that creates a list based off of the documents listed in Firestore. I am trying to take the value generated from a Firestore snapshot and pass it out of the class to a variable that is updated every time the user clicks on a different entry from the List Builder.
Here is the class making the Firestore interaction and returning the ListBuilder:
class DeviceBuilderListState extends State<DeviceBuilderList> {
final flutterWebviewPlugin = new FlutterWebviewPlugin();
#override
void initState() {
super.initState();
// Listen for our auth event (on reload or start)
// Go to our device page once logged in
_auth.onAuthStateChanged
.where((user) {
new MaterialPageRoute(builder: (context) => new DeviceScreen());
});
// Give the navigation animations, etc, some time to finish
new Future.delayed(new Duration(seconds: 1))
.then((_) => signInWithGoogle());
}
void setLoggedIn() {
_auth.onAuthStateChanged
.where((user) {
Navigator.of(context).pushNamed('/');
});
}
#override
Widget build(BuildContext context) {
return new FutureBuilder<FirebaseUser>(
future: FirebaseAuth.instance.currentUser(),
builder: (BuildContext context, AsyncSnapshot<FirebaseUser> snapshot) {
if (snapshot.data != null)
return new StreamBuilder(
stream: Firestore.instance
.collection('users')
.document(snapshot.data.uid)
.collection('devices')
.snapshots,
builder: (context, snapshot) {
if (!snapshot.hasData)
return new Container();
return new Container(
margin: const EdgeInsets.symmetric(vertical: 10.0),
child: new ListView.builder(
itemCount: snapshot.data.documents.length,
padding: const EdgeInsets.all(10.0),
itemBuilder: (context, index) {
DocumentSnapshot ds =
snapshot.data.documents[index];
return new Card(
child: new GestureDetector(
onTap: () {
var initialStateLink = "${ds['name']}";
Navigator.of(context).pushNamed("/widget");
},
child: new Text(
" ${ds['name']}",
style: new TextStyle(fontSize: 48.0),
),
));
}),
);
},
);
else return new Container();
}
);}
}
Then I want to send the var initialStateLink to a different function in the same dart file:
Future<String> initialStateUrl() async {
final FirebaseUser currentUser = await _auth.currentUser();
Firestore.instance.collection('users')
.document(currentUser.uid).collection('devices').document(initialStateLink).get()
.then((docSnap) {
var initialStateLink = ['initialStateLink'];
return initialStateLink.toString();
});
return initialStateUrl().toString();
}
So that it returns me the proper String. I am at a complete loss on how to do this and I was unable to find another question that answered this. Thanks for the ideas.

You can use Navigator.push(Route route) instead of Navigator.pushNamed(String routeName)
And I don't encourage you to place navigation code deeply inside the widget tree, it's hard to maintain your logic of application flow because you end up with many pieces of navigation code in many classes. My solution is to place navigation code in one place (one class). Let's call it AppRoute, it looks like:
class AppRoute {
static Function(BuildContext, String) onInitialStateLinkSelected =
(context, item) =>
Navigator.of(context).push(
new MaterialPageRoute(builder: (context) {
return new NewScreen(initialStateLink: initialStateLink);
}
));
}
and replace your code in onTap:
onTap: () {
var initialStateLink = "${ds['name']}";
AppRoute.onInitialStateLinkSelected(context, initialStateLink);
}
Now, you not only can pass data from class to another class but also can control your application flow in ease (just look at AppRoute class)

Just make initialStateLink variable Global and send it as an argument to the another class like below,
a) Specify route as follows
'/widget' : (Buildcontext context) => new Anotherscreen(initialStateLink)
b) Navigate to the Anotherscreen()
c) Then the Anotherscreen () will be like this,
class Anotherscreen () extends StatelessWidget {
var initialStateLink;
Anotherscreen (this.initialStateLink);
......
}

I ended up finding a different solution that solved the problem (kind of by skirting the actual issue).
A MaterialPageRoute allows you to build a new widget in place while sending in arguments. So this made it so I didn't have to send any data outside of the class, here is my code:
return new Card(
child: new GestureDetector(
onTap: () {
Navigator.push(context,
new MaterialPageRoute(
builder: (BuildContext context) => new WebviewScaffold(
url: ds['initialStateLink'],
appBar: new AppBar(
title: new Text("Your Device: "+'${ds['name']}'),
),
withZoom: true,
withLocalStorage: true,)
));},

Related

How to reload data in initState() flutter while using pop

I have two screens, the first one fetches the data from the database and shows it on the screen. And the second screen creates a new user/course and sends it to the database. But when I use a pop to go back to the first screen, I want to update its data. How can I do this?
How can I update the screen data when returning from the second screen?
First Screen:
class CourseList extends StatefulWidget {
const CourseList({super.key});
#override
State<CourseList> createState() => _CourseListState();
}
class _CourseListState extends State<CourseList> {
Future<List<Course>?> listCourses = Future.value([]);
#override
initState() {
super.initState();
Future.delayed(Duration.zero, () {
setState(() {
var course = CoursesData();
listCourses = course.getCourses();
});
});
}
#override
Widget build(BuildContext context) {
var futureBuilder = FutureBuilder(
future: listCourses,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return createScreen(context, snapshot);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
return const Center(child: CircularProgressIndicator());
});
return futureBuilder;
}
Widget createScreen(BuildContext context, AsyncSnapshot snapshot) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context)
.pushNamed('/courseForm', arguments: null);
},
child: const Text("Adicionar Curso"))
],
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.length,
itemBuilder: ((context, index) {
return CourseTile(snapshot.data[index]);
}),
))
],
);
}
}
Second screen:
IconButton(
onPressed: () {
final isValid = _fomr.currentState!.validate();
Navigator.pop(context, true);
},
icon: const Icon(Icons.save))
EDITED:
ElevatedButton(
onPressed: () async {
var result = await Navigator.of(context)
.pushNamed('/courseForm', arguments: null);
if (result != null) {
setState(() {
var course = CoursesData();
listCourses = course.getCourses();
});
}
},
child: const Text("Adicionar Curso"))
You can navigate to second screen like this:
var result = await Navigator.of(context).push(MaterialPageRoute(builder: (context) => SecondScreen()));
if(result != null && result){
setState(() {
var course = CoursesData();
listCourses = course.getCourses();
});
}
then in your second screen when you pop like this
Navigator.pop(context, true);
as you are doing right now you pass back a bool variable to first screen which act like a flag and with that you can find when it is the time to reload the data. Also don forgot to await for the Navigator because of that you can receive the data from your second screen. Now when you come back from second screen, its going to update the ui.
The documentation covers this example in detail. So I'll paste in the relevant part:
Future<void> _navigateAndDisplaySelection(BuildContext context) async {
// Navigator.push returns a Future that completes after calling
// Navigator.pop on the Selection Screen.
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SelectionScreen()),
);
The key is in this line:
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SelectionScreen()),
);
The result of navigator.push will be stored in result.
So in your case, you should do this after getting the result (as #eamirho3ein has answered first, so I'm explaining) :
setState(() {
var course = CoursesData();
listCourses = course.getCourses();
});
Take a look at the Flutter example for returning data

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;
}

Using GetX in a FutureBuilder to build a list - the UI is not updated

I'm creating a todo list (sort of) app.
I have a view where I use a FutureBuilder, it calls a function to fetch the articles from the SQLite db and show as Card.
I have a getx_controller.dart which contains the list as observable:
class Controller extends GetxController {
// this is the list of items
var itemsList = <Item>[].obs;
}
and I have a items_controller.dart
// function to delete an item based on item id
void deleteItem(id) {
final databaseHelper = DatabaseHelper();
// remove item at item.id position
databaseHelper.deleteItem(tableItems, id);
print('delete item with id $id');
this.loadAllItems();
}
the loadAllItems is a function that queries the DB
And finally, the list_builder.dart which contains the FutureBuilder
final itemController = Get.put(ItemsController());
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Item>>(
future: itemController.loadAllItems(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasData) {
print(snapshot.data.toString());
return Obx(() => ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
return Slidable(
actionExtentRatio: 0.25,
actionPane: SlidableDrawerActionPane(),
child: _itemCard(index, snapshot.data![index]),
actions: <Widget>[
IconSlideAction(
caption: 'Elimina',
color: Colors.red,
icon: Icons.delete,
onTap: () {
itemController.deleteItem(snapshot.data![index].id);
itemController.loadAllItems();
}),
],
secondaryActions: <Widget>[
IconSlideAction(
caption: 'Modifica',
color: Colors.blue,
icon: Icons.edit,
onTap: () {},
),
],
);
}));
} else {
return Container(
child: Text('Loading'),
);
}
});
// });
}
My issue is that when I tap on "Elimina" (or I create a new item, same procedure), the ListBuilder doesn't refresh the list on the screen, despite of having the itemsList recreated (using getxController.itemsList = RxList.generate(...)) and observable.
You don't need to use FutureBuilder when using observables. This is redundant and makes your code complicated.
Instead you should assign the awaited Future (the actual data/items) to your observable list (itemList). And your UI should update automagically.
So your controller should look something like this:
class Controller extends GetxController {
// this is the list of items
var itemsList = <Item>[].obs;
#override
onInit()async{
await loadAllItems();
}
loadAllItems()async{
var databaseResponse= await dbHelper.getAll();
itemList.assignAll(databaseResponse);
}
}
And your UI build method should just return the observing (Obx) ListView.builder like the following and you are done:
#override
Widget build(BuildContext context){
return Obx(() => ListView.builder(
itemCount: controller.itemList.length,
itemBuilder: (context, index) {
var item = comtroller.itemList[index]; // use this item to build your list item
return Slidable(

Flutter: What is the best way to filter Firestore collection items based on user input?

[CASE]
I have a list of activities stored in a FireStore collection. Each activity has a type. In a dropdown, the user can filter the activities based on their type. There is also an option to select all items.
I currently manage this by passing the activity type to a new widget each time the user changes the value from the dropdown. However, I was wondering if this is the most efficient way of doing this (mainly when scaling up the number of activities in the collection), or if there is a different best practice I should use?
[CODE]
Screen 1: Dropdown and list of activities
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
String eventClass;
final items = ["All", "Music", "Drinks"];
void _changeClass(selectedClass) {
eventClass = selectedClass;
setState(() {});
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
SizedBox(height: 10),
DropdownSearch<String>(
mode: Mode.BOTTOM_SHEET,
showSelectedItem: true,
items: items,
label: "Change event type",
onChanged: (String selectedClass) => _changeClass(selectedClass),
selectedItem: items[0]),
Expanded(
child: EventList(eventClass),
)
],
);
}
}
Screen2: EventList
class EventList extends StatelessWidget {
final String eventClass;
EventList(this.eventClass);
#override
Widget build(BuildContext context) {
final Stream<QuerySnapshot> _usersStream = FirebaseFirestore.instance
.collection('events')
.where('eventClass', isEqualTo: eventClass.toLowerCase())
.snapshots();
return StreamBuilder<QuerySnapshot>(
stream: _usersStream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("Loading");
}
return ListView(
children: snapshot.data.docs.map((DocumentSnapshot document) {
Map<String, dynamic> data = document.data();
return ListTile(
leading: CircleAvatar(
child: Icon(Icons.access_alarm),
foregroundColor: Colors.white,
backgroundColor: Theme.of(context).primaryColor,
),
title: Text(data['eventName']),
subtitle: Text(data['eventDescription']),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EventItem(data),
),
);
},
);
}).toList(),
);
},
);
}
}
ListView constructor is generally used for creating a limited number of items, but in order to create a large list you can use ListView.builder constructor. This constructor is appropriate in this scenario as the builder is called only for those items that are actually visible.
The ListView.builder() constructor creates items as they’re scrolled onto the screen as opposed to the default ListView constructor, which requires creating all items at once. Please refer to this document for more details on this with an interactive example.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index]),
);
},
)

Flutter: Refreshing a list API on select of a filter in the same page

I have a use case in Flutter, where I need to display a list from an API. Once a particular filter is selected in the page, the displayed list should be refreshed by firing a new API. Below is my source code where I am displaying a list from the API.
class _MyFieldReportForm extends State<MyFieldReport> {
var myContext;
#override
Widget build(BuildContext context) {
myContext = context;
// TODO: implement build
return Scaffold(
body: new FutureBuilder<List<ReportData>>(
future: fetchProducts(new http.Client()),
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return snapshot.hasData
? createListView(snapshot, myContext)
: new Center(child: new CircularProgressIndicator());
},
),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.add),
backgroundColor: Colors.green,
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => FieldvisitForm()));
},
),
);
}
}
After pressing any filter button I need to refresh this API with the new one. It would be great if someone could help me with a sample code.
Finally got to know the solution. It can be done by getting the list in
void initState() {
super.initState();
listFuture = fetchProducts(new http.Client());
}
and in setState, I am updating the list. Below is the complete code:
Future<List<ReportData>> listFuture;
body: new FutureBuilder<List<ReportData>>(
future: listFuture,
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return snapshot.hasData
? createListView(snapshot, myContext)
: new Center(child: new CircularProgressIndicator());
},
),
onPressed: () {
setState(() {
refreshList();
});
void refreshList() {
listFuture = fetchProductsUpdate(new http.Client());
}
checkout my repo, i've created a App class extending a statefull, and a widget stateless, it maybe will help you