refresh data on home page using future builder on button click - flutter - flutter

i have an app with two screens, home and update page.
The home page displays a list of items and the update page updates the items.
I am having difficulties refreshing the home page to display current updates when I pop back to it.
How can I refresh the home page when I route back to it.
See the code to navigate to update page
// home page
// build the list widget
Widget _buildTaskWidget(task) {
return ListTile(
leading: Icon(Icons.assignment),
title: Text(task['name']),
subtitle: Text(task['created_at']),
onTap: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => UpdateTask(task: task),
),
);
await fetchAllTask();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
title: Text(widget.title),
),
body: FutureBuilder(
future: fetchAllTask(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List tasks = snapshot.data;
listItems = tasks;
return _buildTaskList(tasks);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
return Center(
child: ShowLoader(),
);
}),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context, MaterialPageRoute(builder: (context) => AddTask()));
},
tooltip: 'Add Task',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
// update page to navigate back to home page
onPressed: () async {
var res = await updateNewTask(_taskTextInput.text,
_vendorTextInput.text, _amountTextInput.text, id);
print(res);
Navigator.pop(context);
},

FutureBuilder only runs the asynchronous task when its parent is built. To force a rebuild, you can call setState() after Navigating to the next page. Doing so refreshes the current Screen before navigating to the next.
Navigator.of(context).push(...).then((_) => setState(() {}));
Another approach that you can also consider looking into is with the use of StreamBuilder - this Widget rebuilds when change in Stream is detected.

Related

FlutterFire does signs user out but does not navigate to signin screen

I am using FlutterFire AuthUI in my Flutter app.
In the app root I use AuthGate widget that listens to the FirebaseAuth.instance.authStateChanges() to decide to show the sign in page or the home page.
Everything works fine, but when I sign in from a screen other than the home page, the user is signed out but the screen does not switch to the sign in page again.
When I sign out from the home page it works as expected.
This is my AuthGate:
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: ((context, snapshot) {
if (!snapshot.hasData) {
return SignInScreen(
providerConfigs: [
EmailProviderConfiguration(),
],
);
}
return TimelineScreen();
}),
);
}
This is how I use it in the app root:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(ChangeNotifierProvider<AppUser>(
create: (context) => AppUser(),
child: MaterialApp(
home: AuthGate(),
),
));
}
Signing out from a page other than the home page:
SettingsTile(
title: "Sign out",
trailing: Icon(Icons.logout),
onPressed: (context) {
FirebaseAuth.instance.signOut();
}),
signOut() is async method, adding await should do the trick
SettingsTile(
title: "Sign out",
trailing: Icon(Icons.logout),
onPressed: (context) async {
await FirebaseAuth.instance.signOut();
}),

When to create new bloc?

I'm still learning bloc patter. I created two pages, ViewPage and DetailsPage using a single bloc.
This is my bloc:
getRecordEvent
deleteRecordEvent
LoadedState
LoadedErrorState
DeletedState
DeletedErrorState
The view page will only build a widget with list of records on a LoadedState. When the user taps any record, It will push the Details page and displays detailed record with a delete button. When user press the delete button, I listen to the DeletedState and call the getRecord event to populate the view page again with the updated record.
Its all working but my problem is when I encountered an error while deleting record. When the state is DeleteErrorState, my view page becomes empty since I don't call getRecord there because the error could be internet connection and two error dialog will be shown. One for the DeletedErrorState and LoadedErrorState.
I know this is the default behavior of bloc. Do I have to create a separate bloc with only deleteRecordEvent? And also if I create a new page for adding record, will this also be a separate bloc?
UPDATE:
This is a sample of ViewPage. The DetailsPage will only call the deleteRecordEvent once the button was pressed.
ViewPage.dart
void getRecord() {
BlocProvider.of<RecordBloc>(context).add(
getRecordEvent());
}
#override
Widget build(BuildContext context) {
return
Scaffold(
body: buildBody(),
),
);
}
buildBody() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: BlocConsumer<RecordBloc, RecordState>(
listener: (context, state) {
if (state is LoadedErrorState) {
showDialog(
barrierDismissible: false,
context: context,
builder: (_) {
return (WillPopScope(
onWillPop: () async => false,
child: ErrorDialog(
failure: state.failure,
)));
});
} else if (state is DeletedState) {
Navigator.pop(context);
getRecord();
} else if (state is DeletedErrorState) {
Navigator.pop(context);
showDialog(
barrierDismissible: false,
context: context,
builder: (_) {
return (WillPopScope(
onWillPop: () async => false,
child: ErrorDialog(
failure: state.failure,
)));
});
}
},
builder: (context, state) {
if (state is LoadedState) {
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
state.records.length <= 0
? noRecordWidget()
: Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: state.records.length,
itemBuilder: (context, index) {
return Card(
child: Padding(
padding: EdgeInsets.symmetric(
vertical: Sizes.s8),
child: ListTile(
title: Text(state.records[index].Name),
subtitle: state.records[index].date,
onTap: () {
showDialog(
barrierDismissible: false,
context: context,
builder: (_) {
return BlocProvider<RecordBloc>.value(
value: BlocProvider.of<RecordBloc>(context),
child: WillPopScope(
onWillPop: () async => false,
child:
DetailsPage(record:state.records[index]),
));
});
},
),
));
}),
),
],
),
);
}
return (Container());
},
),
),
);
}
About bloc
As a general rule of thumb, you need one bloc per ui. Of course, this is not always the case, as it depends on a few factors, the most important of which is how many events are you handling in your ui. For your case, where there is a ui that holds a list of items into an item-details ui, I would create two blocs. One will only handle loading items (ItemsBloc for instance), the other will handle actions to a single item (SingleItemBloc). I might only use the delete event for now, but as the app grows, I will be adding more events. This all facilitates the Separation of Concerns concept.
Applying that to your case, the SingleItemBloc will handle deleting, modifying, subscribing, etc to a single item, while ItemsBloc will handle loading the items from the different repositories (local/remote).
Since I don't have the code for your bloc I can't offer any modifications.
Solution specific to your case
It seems that you're losing the last version of your list of items every time a new state is emitted. You should keep a local copy of the last list you acquired from your repositories. In case there is an error, you just use that list; if not just save the new list as the last list you had.
class MyBloc extends Bloc<Event, State> {
.....
List<Item> _lastAcquiredList = [];
Stream<State> mapEventToState(Event event) async* {
try {
....
if(event is GetItemsEvent) {
var newList = _getItemsFromRepository();
yield LoadedState(newList);
_lastAcquiredList = newList;
}
....
} catch(err) {
yield ErrorState(items: _lastAcquiredItems);
}
}
}

How to dismiss AlertDialog after Navigator.push?

I am call Navigator.push() after user press button on AlertDialog. But when user press button AlertDialog remain open and on top of new page.
How to dismiss AlertDialog after user press button?
Future<void> _showMyDialog() async {
return showDialog<void>(
context: context,
barrierDismissible: false, // user must tap button!
builder: (BuildContext context) {
return AlertDialog(
title: Text('AlertDialog Title'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text('This is a demo alert dialog.'),
Text('Would you like to approve of this message?'),
],
),
),
actions: <Widget>[
FlatButton(
child: Text('Approve'),
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(builder: (context) => Page()),
);
Navigator.of(context).pop();
},
),
],
);
},
);
}
await _showMyDialog();
The comment saying to call pop is probably the easiest way to do this.
Another thing to consider next is if you want them to be able to stay on the same page. Here is a way to do both of these if you get beyond the => NewPage() style of navigation on your app. It's more commonly used for Drawers, of course.
Happy coding!
onTap: () {
newRouteName = "/form_check";
// if the current route is the exact location we're at (first on the stack), mark that
Navigator.popUntil(context, (route) {
if (route.settings.name == newRouteName) {
isNewRouteSameAsCurrent = true;
} else {
isNewRouteSameAsCurrent = false;
}
return true;
});
// if it isn't, go to the new route
if (!isNewRouteSameAsCurrent) {
Navigator.pushNamed(context, newRouteName);
}
// again if it is, just pop the drawer/dialog away
else {
Navigator.pop(context);
}
}

How to get the number of routes in Navigator's stack

Is there a way to know if the current page is the last page in the Navigator Stack and calling Navigator.pop() at this point will close the app?
You can use this code to check if the route is the first :
ModalRoute.of(context).isFirst
so the full code will be
if(! ModalRoute.of(context).isFirst)
Navigator.pop();
It doesn't close the app it destroys the last route shows a black screen.
you can close the app using this: Flutter how to programmatically exit the app
and you can't access the stack or history because it's private in Navigator class Navigator._history but you can use this workaround to check if the current route is the last one or not:
Future<bool> isCurrentRouteFirst(BuildContext context) {
var completer = new Completer<bool>();
Navigator.popUntil(context, (route) {
completer.complete(route.isFirst);
return true;
});
return completer.future;
}
I found this in the source of the AppBar widget.
final ModalRoute<dynamic>? parentRoute = ModalRoute.of(context);
final bool canPop = parentRoute?.canPop ?? false;
When canPop is false, you are on the root screen.
If you just want to handle something before the application exits. Like showing an confirm dialog you could use WillPopScope.
Example
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _showDialog,
child: Scaffold(
body: Center(
child: Text("This is the first page"),
),
),
);
}
Future<bool> _showDialog() {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("Are you sure?"),
content: Text("You want to exit app"),
actions: <Widget>[
FlatButton(
child: Text("Yes"),
onPressed: () => Navigator.of(context).pop(true),
),
FlatButton(
child: Text("No"),
onPressed: () => Navigator.of(context).pop(false),
)
],
);
}) ?? false;
}

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