Flutter close child screen and navigate to another screen automatically - flutter

I'm using bottom tab-navigation and my screens have the following logic:
Screen A
-> Button Open Screen B
-> Button Open Screen C
Screen B
-> Button Open Child Screen B
-> Button Open Screen C
I can navigate from Screen A to Screen B through code using widget.mainTabController.animateTo(1); and it works great.
What I need now is to navigate from Screen B to Screen A after the Child Screen B has been closed.
class ScreenB extends StatefulWidget {
final TabController mainTabController;
ScreenB(TabController mainTabController)
: this.mainTabController = mainTabController;
#override
_State createState() => _State();
}
class _State extends State<ScreenB>
{
#override
Widget build(BuildContext context) {
// ...
GridView.builder(
// ...
itemBuilder: (BuildContext context, int index)
{
Entry entry = this.entries[index];
return entry.build(context);
}
);
}
}
class Entry extends State<ScreenB>
{
Widget build(BuildContext context)
{
// ...
onTap: () async {
// Opens the Screen B Child
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ScreenBChild(),
),
);
// Should go back to Screen A but does nothing
if (result)
widget.mainTabController.animateTo(0);
);
}
}
After the screen "Child Screen B" has been closed, I'm back to Screen B and the code widget.mainTabController.animateTo(0), although is being triggered, nothing happens, the screen doesn't change. Also no error is thrown in the console.

Solved.
The Entry class must be converted into a Stateless (or Stateful, as you wish) class
The class should receive the mainTabController upon its initialization
We have to apply a Delay before trigger the animation to move to another screen
With that said, the Entry is called like this:
Entry entry = new Entry(mainTabController: widget.mainTabController);
And the class was converted to:
class Entry extends StatelessWidget {
final TabController mainTabController;
Entry({Key key, this.mainTabController}) : super(key: key);
#override
Widget build(BuildContext context)
{
// ...
onTap: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ScreenBChild(),
),
);
if (result)
{
await Future.delayed(Duration(milliseconds: 300), () =>
{
this.mainTabController.animateTo(0);
});
}
}
}
}

Related

Provider to be initialized asynchronously from `initState()` but get `could not find the correct Provider`

I develop an ad app, with a message button on the detailed view.
When the user tap on it, the chats view (stateful widget) is pushed to the screen.
The initState() is there to call the asyncInitMessages() which asynchronously fetches the chats and related message from the distant database. The asyncInitMessages() belongs to the Chats class which extends ChangeNotifier.
/// A chat conversation
class Chats extends ChangeNotifier {
/// Internal, private state of the chat.
void asyncInitMessages(
{required ClassifiedAd ad,
required String watchingUserId,
required bool isOwner}) async {
// blah blah
}
}
The ClassifiedAdMessagesViewstateful widget class implementation is as follows (snipet):
#override
void initState() {
// == Fetch conversation and messages
asyncInitMessages();
}
void asyncInitMessages() async {
// === Update all messages
try {
Provider.of<Chats>(context, listen: false).asyncInitMessages(
ad: widget.ad,
watchingUserId: widget.watchingUser!.uid,
isOwner: _isOwner);
} catch (e) {
if (mounted) {
setState(() {
_error = "$e";
_ready = true;
});
}
}
}
#override
Widget build(BuildContext context) {
// <<<<<<<<<<< The exception fires at the Consumer line right below
return Consumer<Chats>(builder: (context, chats, child) {
return Scaffold(
// ... blah blah
Finally, when running ll that, I got the exception in the build at the Consumer line:
could not find the correct Provider<chats>
Help greatly appreciated.
[UPDATED]
Here is the main (very far up from the messages screen)
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
//if (Firebase.apps.isEmpty) {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// } else {
// Firebase.app(); // if already initialized, use that one
// }
if (USE_DATABASE_EMULATOR) {
FirebaseDatabase.instance.useDatabaseEmulator(emulatorHost, emulatorPort);
}
runApp(RootRestorationScope(
restorationId: 'root',
child: ChangeNotifierProvider(
create: (context) => StateModel(),
child: const App())));
}
class App extends StatefulWidget {
const App({super.key});
#override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
#override
Widget build(BuildContext context) {
return PersistedAppState(
storage: const JsonFileStorage(),
child: MultiProvider(
providers: [
ChangeNotifierProvider<ThemeModel>.value(value: _themeModel),
//ChangeNotifierProvider<AuthModel>.value(value: _auth),
],
child: Consumer<ThemeModel>(
builder: (context, themeModel, child) => MaterialApp(
// blah blah
}
}
}
And the component just on top of the
/// Classified ad detail view
class ClassifiedAdDetailView extends StatefulWidget {
final User? watchingUser;
final ClassifiedAd ad;
const ClassifiedAdDetailView(
{Key? key, required this.watchingUser, required this.ad})
: super(key: key);
#override
State<ClassifiedAdDetailView> createState() => _ClassifiedAdDetailViewState();
}
class _ClassifiedAdDetailViewState extends State<ClassifiedAdDetailView>
with TickerProviderStateMixin {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Chats(),
builder: ((context, child) => Scaffold(
// blah blah
ElevatedButton(
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ClassifiedAdMessagesView(
ad: ad,
watchingUser: widget.watchingUser)));
}),
Providers must be located in the widget tree above the widget where you want to use them with Consumer or Provider.of. When you push a new route with Navigator, it won't be add the pushed route below the widget from where you push, it will add it at the same level where home of MaterialApp is located.
(I think the error message you get also states that you can't access the providers between routes.)
In general the tree will look like this if you push some routes (check it with the Flutter Widget Inspector):
MaterialApp
home
widget1
widget2
widget21
widget22
page1
widget1
widget2
page2
page3
In your code you create the provider in ClassifiedAdDetailView and then push
ClassifiedAdMessagesView from this in the onPressed method. You won't be access this provider from ClassifiedAdMessagesView because the tree will be like (simplified):
MaterialApp
home
ClassifiedAdDetailView
ClassifiedAdMessagesView
The solution is to "lift the state up" and place the provider above every widget from where you need to access it. It can be a part of your existing Multiprovider above MaterialApp but if it is too far, you need to find a proper place that is above both ClassifiedAdDetailView and ClassifiedAdMessagesView.

Flutter Provider: changes to List doesn't propagate

I have a app with a List<Match> as state, which is kept in
class MatchesChangeNotifier extends ChangeNotifier {
List<Match> matches;
refresh() async {
matches = await MatchesFirestore.fetchMatches(); // goes to db
notifyListeners();
}
}
then I have the following widgets (simplified)
class AvailableMatches extends StatelessWidget {
#override
Widget build(BuildContext context) {
var matches = context.watch<MatchesChangeNotifier>().matches;
return Column(children: matches.map(e => MatchInfo(e)).toList());
}
}
class MatchInfo extends StatelessWidget {
Match match;
MatchInfo(this.match);
Widget build(BuildContext context) {
// use match for a bunch of stuff
return InkWell(
child: ...,
onTap: () async {
await Navigator.push(context,
MaterialPageRoute(builder: (context) => MatchDetails(match)));
await context.read<MatchesChangeNotifier>().refresh();
}
}
In MatchDetails I have a bunch of buttons. One of them has this callback
onTap() async => await context.read<MatchesChangeNotifier>().refresh()
When I tap this button I see (debugging) that
AvailableMatches gets rebuilt (since it watches matches)
MatchInfo gets rebuilt (since it's down the tree w.r.t. AvailableMatches)
MatchDetails doesn't get re-built
Why is that? Is it because it is somehow called in the onTap function?
After I press the button I would like to see MatchDetails change when it's on top of the screen (i.e. the user is seeing it). Instead this doesn't happen
If I modify the way I push MatchDetails from MatchInfo with this ugly trick the thing works but I would like to avoid it
await Navigator.push(context,
MaterialPageRoute(builder: (context) =>
MatchDetails(context.watch<MatchesChangeNotifier>().matches.firstWhere((m) => m.id == match.id))));

Flutter - Show only one Dialog at the time

I am working on a flutter application using several dialogs for several purposes.
In our code, there are some cases where the user can open a Dialog. Inside this dialog, there are some buttons that will also open another dialog. It results with 2 dialogs on top of each other and with a very dark background screen.
What we would like to do is to only display one dialog at the time. How can we achieve that ?
Here is a simple code to illustrate our issue:
class MyScreen extends StatelessWidget {
#override
build(BuildContext context) {
return FlatButton(
child: Text('Button'),
onPressed: () async {
final resultDialog = await showDialog<ResultDialog1>(
context: context,
builder: (BuildContext context) => MyFirstDialog(),
);
// Do some stuff with the result, so this part of the tree cannot be destroyed
},
);
}
}
class MyFirstDialog extends StatelessWidget {
#override
build(BuildContext context) {
return FlatButton(
child: Text('Button in first dialog'),
onPressed: () async {
final resultDialog = await showDialog<ResultDialog2>(
context: context,
builder: (BuildContext context) => MySecondDialog(), // <- This will appear on top of the first dialog
);
// Do some stuff with the result, so this part of the tree cannot be destroyed
},
);
}
}
class MySecondDialog extends StatelessWidget {
#override
build(BuildContext context) {
return Text('Second Dialog');
}
}
let me give you a widget for that
class MultiDialog extends StatefulWidget {
final Widget child;
const MultiDialog({Key key, this.child}) : super(key: key);
#override
_MultiDialogState createState() => _MultiDialogState();
static void addDialog(
{#required BuildContext context, #required Widget dialog}) {
assert(context != null, "the context cannot be null");
assert(dialog != null, "the dialog cannot be null");
context.findAncestorStateOfType<_MultiDialogState>()._addDialog(dialog);
}
static void remove({#required BuildContext context}) {
assert(context != null, "the context cannot be null");
context.findAncestorStateOfType<_MultiDialogState>()._remove();
}
}
class _MultiDialogState extends State<MultiDialog> {
final _allDialogs = <Widget>[];
void _addDialog<T>(Widget dialog) {
assert(dialog != null, "The dialog cannot be null");
setState(() {
_allDialogs.add(dialog);
});
}
void _remove() {
if (_allDialogs.isEmpty) {
print("No dialogs to remove");
return;
}
setState(() {
_allDialogs.removeLast();
});
}
#override
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
if (_allDialogs.isNotEmpty) _allDialogs.last,
],
);
}
}
and when you want to add a dialog just call
MultiDialog.addDialog(
context: context,
dialog: AlertDialog(),
);
call Navigator.pop to remove the dialog, if there is another dialog which you pushed exist it will be shown, you can further pop them all with results, PS:this code isn't tested, let me know in the comments if this works for you
call MultiDialog.remove(context:context) to pop the visible dialog and bring back the previous dialog,
and if you receive a error that the addDialog is called on null, its because how flutter works, after MultiDialog use a Builder to introduce a new context use it call showDialog,
PS:ABOVE CODE IS TESTED
i made a stream out of the events that cause the dialog to pop up and used rx darts exhaust map to wait for the result (i was already using rxdart)
dialogEventStream
.exhaustMap((_) => maybeShowDialog().asStream())
.listen((_) {});
Future<bool> maybeShowOfflineModeDialog() async {
final isOfflineModeEnabled = await _sharedPreferencesService.isOfflineModeEnabled();
if (!isOfflineModeEnabled) {
final isLoginOffline = await _navigationService.showDialog(NoConnectionDialog());
if (isLoginOffline == true) {
await _sharedPreferencesService.setIsOfflineModeEnabled(isOfflineModeEnabled: true);
return await _navigationService.pushReplacement(AppShellOffline.routeName) ?? true;
} else {
return false;
}
}
return false;
}
something like this

Root widget not rebuilding when popping from child

I have been working on a flutter app where the user starts on a stateful widget with a ListView of items from a SQLite database. The user can tap on an item in the list which navigates to a page where the item can be modified and saved.
When Navigator.pop(context) is used, the app returns to the ListView but doesn't rebuild. The changes made do not show until I force a rebuild (hot reload)
This is a new issue in flutter 1.17.
Root view
class ItemsView extends StatefulWidget {
#override
_ItemsView State createState() => _ItemsViewState();
}
class _ItemsViewState extends State<ItemsView> {
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Story>>(
future: DBProvider.db.getAllItems(),
builder: (BuildContext context, AsyncSnapshot<List<Story>> snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data[0])
}
}
)
}
}
Second View
class ModifyView extends StatefulWidget {
#override
_ModifyView State createState() => _ItemsViewState();
}
class _ItemsViewState extends State<ItemsView> {
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Story>>(
future: DBProvider.db.getAllItems(),
builder: (BuildContext context, AsyncSnapshot<List<Story>> snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data[0])
}
}
)
}
}
How can I force the widget to reload?
You can use the Navigator's method then on which you can reload the page or do any other stuff. In below example I am using the screen A to to screen B navigation and When user navigate the from the B to A , we will refresh the view or do any other stuff like below.
From Screen A -> B
Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(
name: B), ///// HERE "B" IS THE CLASS NAME
builder: (context) =>
B(),
),
).then((value) {
//// THIS METHOD IS ENVOKE WHEN SCREEN COME FROM B->A, YOU CAN PERFROM CAN TASK HERE
});
Inside the B screen, we need to create the constructor like below
class B extends StatefulWidget {
B () ;
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return _B();
}
}
Navigation from B to A
Navigator.pop(context, 1); //// HERE WE ARE PUSHING THE ANY VALUE "1" FOR THE RETURN IN then OF CLASS "A"

How to go back and refresh the previous page in Flutter?

I have a home page which when clicked takes me to another page through navigates, do some operations in then press the back button which takes me back to the home page. but the problem is the home page doesn't get refreshed.
Is there a way to reload the page when i press the back button and refreshes the home page?
You can trigger the API call when you navigate back to the first page like this pseudo-code
class PageOne extends StatefulWidget {
#override
_PageOneState createState() => new _PageOneState();
}
class _PageOneState extends State<PageOne> {
_getRequests()async{
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new RaisedButton(onPressed: ()=>
Navigator.of(context).push(new MaterialPageRoute(builder: (_)=>new PageTwo()),)
.then((val)=>val?_getRequests():null),
),
));
}
}
class PageTwo extends StatelessWidget {
#override
Widget build(BuildContext context) {
//somewhere
Navigator.pop(context,true);
}
}
Or you can just use a stream if the API is frequently updated, the new data will be automatically updated inside your ListView
For example with firebase we can do this
stream: FirebaseDatabase.instance.reference().child(
"profiles").onValue
And anytime you change something in the database (from edit profile page for example), it will reflect on your profile page. In this case, this is only possible because I am using onValue which will keep listening for any changes and do the update on your behalf.
(In your 1st page): Use this code to navigate to the 2nd page.
Navigator.pushNamed(context, '/page2').then((_) {
// This block runs when you have returned back to the 1st Page from 2nd.
setState(() {
// Call setState to refresh the page.
});
});
(In your 2nd page): Use this code to return back to the 1st page.
Navigator.pop(context);
use result when you navigate back from nextScreen as follow :
Navigator.of(context).pop('result');
or if you are using Getx
Get.back(result: 'hello');
and to reload previous page use this function :
void _navigateAndRefresh(BuildContext context) async {
final result = await Get.to(()=>NextScreen());//or use default navigation
if(result != null){
model.getEMR(''); // call your own function here to refresh screen
}
}
call this function instead of direct navigation to nextScreen
The solution which I found is simply navigating to the previous page:
In getx:
return WillPopScope(
onWillPop: () {
Get.off(() => const PreviousPage());
return Future.value(true);
},
child: YourChildWidget(),
or if you want to use simple navigation then:
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) =>PreviousPage() ,));
Simply i use this:
onPressed: () {
Navigator.pop(context,
MaterialPageRoute(builder: (context) => SecondPage()));
},
this to close current page:
Navigator.pop
to navigate previous page:
MaterialPageRoute(builder: (context) => SecondPage())
In FirtsPage, me adding this for refresh on startUpPage:
#override
void initState() {
//refresh the page here
super.initState();
}
For a more fine-grained, page-agnostic solution I came up with this Android Single LiveEvent mimicked behaviour.
I create such field inside Provider class, like:
SingleLiveEvent<int> currentYearConsumable = SingleLiveEvent<int>();
It has a public setter to set value. Public consume lets you read value only once if present (request UI refresh). Call consume where you need (like in build method).
You don't need Provider for it, you can use another solution to pass it.
Implementation:
/// Useful for page to page communication
/// Mimics Android SingleLiveEvent behaviour
/// https://stackoverflow.com/questions/51781176/is-singleliveevent-actually-part-of-the-android-architecture-components-library
class SingleLiveEvent<T> {
late T _value;
bool _consumed = true;
set(T val) {
_value = val;
_consumed = false;
}
T? consume() {
if (_consumed) {
return null;
} else {
_consumed = true;
return _value;
}
}
}
await the navigation and then call the api function.
await Navigator.of(context).pop();
await api call
You can do this with a simple callBack that is invoked when you pop the route. In the below code sample, it is called when you pop the route.
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => new _HomePageState();
}
class _HomePageState extends State<HomePage> {
_someFunction()async{
Navigator.of(context).push(MaterialPageRoute(builder: (_)=> PageTwo(
onClose():(){
// Call setState here to rebuild this widget
// or some function to refresh data on this page.
}
)));
}
#override
Widget build(BuildContext context) {
return SomeWidget();
}
...
} // end of widget
class PageTwo extends StatelessWidget {
final VoidCallback? onClose;
PageTwo({Key? key, this.onClose}) : super(key: key);
#override
Widget build(BuildContext context) {
return SomeWidget(
onEvent():{
Navigate.of(context).pop();
onClose(); // call this wherever you are popping the route
);
}
}