I want to show full-screen loading view in flutter while API calling. But when I add loading widget in scaffold body it appears after appbar and bottom navigator.
I am spending lots of time for display loading view in full-screen. Also, I want to prevent back action while API calling.
Well, since you're using Scaffold, then make use of its showDialog() method.
It has a property called barrierDismissible that if you set as false, the user won't be able to close or interact with the screen outside of it.
void _openLoadingDialog(BuildContext context) {
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: CircularProgressIndicator(),
);
},
);
}
Once you're done with the API loading, call Navigator.pop(context); to dismiss the dialog.
To prevent the user from clicking the back button on the Dialog, dismissing it, envelop your Scaffold inside a WillPopScope widget and implement the onWillPop function.
#override
Widget build(BuildContext context) {
return WillPopScope(
child: Scaffold(
body: Container(),
),
onWillPop: _onBackButton
);
}
Future<bool> _onBackButton() {
// Implement your logic
return Future.value(false);
}
If you return false on it, the user won't be able to press the back button. So use any logic you desire, e.g 'If I'm loading return false, otherwise return true'.
Full-screen loader: Best solution to display a full screen loader is to use package
https://pub.dev/packages/screen_loader.
Prevent back action while loading: This package this package provides a variable isLoading, use it to prevent navigating back. eg:
// --------------- some_screen.dart ---------------
import 'package:flutter/material.dart';
import 'package:screen_loader/screen_loader.dart';
class SomeScreen extends StatefulWidget {
#override
_SomeScreenState createState() => _SomeScreenState();
}
class _SomeScreenState extends State<SomeScreen> with ScreenLoader<SomeScreen> {
getData() {
this.performFuture(NetworkService.callApi);
}
#override
Widget screen(BuildContext context) {
return WillPopScope(
child: Scaffold(
// your beautiful design..
),
onWillPop: () async {
return !isLoading;
},
);
}
}
// --------------- app_api.dart ---------------
class NetworkService {
static Future callApi() async {
}
}
NOTE: You'll need to see the definition of performFuture to see how it works for different scenarios.
You can use this dialog for full screen loading progress_dialog
Did you not think of removing the Scaffold for the loading screen?
Related
TLDR; I added a custom page transition, sliding to the left. It works perfectly for every page in the App except the Welcome screen which contains only a video player. After the video ends, I get transferred to the desired page but the normal 'transition animation' happens instead of my custom one.
More info: I use Navigator.pushNamed(context, '/pagename') for my page travels. I created a custom transition, CustomPageRoute, which did not support named navigation. After a few searches I made it work and now supports both the cleanness of named navigation plus the transition animations plus the data transferring from page to page. All expect the welcome screen which just plays a video then after 3 seconds it pushes the Login page. Here is the code of the Welcome Page:
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
class WelcomePage extends StatefulWidget {
const WelcomePage({Key? key}) : super(key: key);
#override
State<WelcomePage> createState() => _WelcomePageState();
}
class _WelcomePageState extends State<WelcomePage> {
late VideoPlayerController _controller;
#override
void initState() {
super.initState();
_controller = VideoPlayerController.asset('assets/welcome_vid.mp4')
..initialize().then((_) {
setState(() {});
})
..setVolume(0.0);
_playVideo();
}
void _playVideo() async {
_controller.play();
await Future.delayed(const Duration(seconds: 3))
.then((_) => Navigator.pushNamed(context, '/login'));
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: _controller.value.isInitialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(
_controller,
),
)
: Container(),
));
}
}
Also, here is a sample of the named routes I use in main.dart:
static Route onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
case '/login':
return CustomPageRoute(child: LoginPage(), settings: settings);
Also, Flutter Doctor detected no problems, everything is up to date and I don't think any libraries overlap another. I even tried to change the destination if maybe somehow that affected anything but no.
Why doesn't it work?
I have a screen with a bottom navigation bar and a floating action button.
I'm trying to display a bottom sheet that comes up from behind the bottom navigation bar. Also, I would like to see the barrier. Only the bottom navigation bar and the bottom sheet should be clearly visible, while the rest of the screen must be darkened. (kind of this).
For navigation, I'm using GetX, so first I tried to specify useRootNavigator: true to Get.bottomSheet, but didn't work.
I've seen many answers which suggest the use of showModalBottomSheet with useRootNavigator: true. Again, didn't work.
I've already achieved this with a custom solution (posted here), but I want to understand what can inhibit the effect of useRootNavigator or if there is another built-in solution to achieve my goal.
LE:
I'm doing simple calls, nothing fancy.
References to the documentation below.
GetX package: https://pub.dev/packages/get
showModalBottomSheet
Source code example:
https://gist.github.com/alexgrusu/8fd173e56d4046cdbda487e4b98bd950
The solution for this behavior can be achieved through nested navigation.
First, define the keys of the navigators.
class NavigatorKeys {
static final GlobalKey<NavigatorState> mainNavigatorKey = GlobalKey();
static final GlobalKey<NavigatorState> secondaryNavigatorKey =
GlobalKey();
}
The main key can be assigned to the navigatorKey of the MaterialApp.
Next, let's consider that our app's home is the dashboard screen so that our MaterialApp would look like so.
MaterialApp(
title: 'Demo App',
home: const DashboardScreen(),
navigatorKey: NavigatorKeys.mainNavigatorKey,
);
Then, the build method for the DashboardScreen can be implemented as shown below.
#override
Widget build(BuildContext context) {
return Scaffold(
body: Navigator(
key: NavigatorKeys.secondaryNavigatorKey,
onGenerateRoute: (routeSettings) {
return MaterialPageRoute(
builder: (context) =>
_buildMainUi(),
);
},
),
);
},
),
bottomNavigationBar: _buildNavigationBar(),
);
}
_buildMainUi() => Scaffold(body: Center(child: Text('Dashboard')));
In the end, simply pass the context of the secondary navigator to the showModalBottomSheet method and set useRootNavigator to false.
showModalBottomSheet<Widget?>(
useRootNavigator: false,
context: NavigatorKeys.secondaryNavigatorKey.currentContext!,
builder: (BuildContext context) {
return Container(height: 200, child: Text('hello'));
},
);
I'm having a little error I can't seem to figure out, and I believe it's due to my Alert Dialog implementation. I'm using the flutter Provider package, and opening my Dialog like this:
_openSearchHistory(
BuildContext context, TextEditingController searchController) {
final searchModel = Provider.of<SearchModel>(context, listen: false);
showDialog(
context: context,
builder: (_) => ChangeNotifierProvider<SearchModel>.value(
value: searchModel,
child: DialogSearchHistory(
searchHistory: searchModel.searchHistory,
searchController: searchController,
),
));
}
The problem is when on android and the user presses the back button, the Dialog does not close, but the page behind the Dialog pops back a page. I have a close button on the Dialog that successfully closes the dialog, but I want users to be able to use the back button for a better experience. The Dialog does close if the user clicks outside of it as well. I've tried wrapping the Dialog with WillPopScope, but it does not get called on a back button press.
Could anyone shed some light on what I'm doing wrong here?
I have tried wrapping the Widget in the calling method with WillPopScope
showDialog(
context: context,
builder: (_) => ChangeNotifierProvider<SearchModel>.value(
value: searchModel,
child: WillPopScope(
onWillPop: () {
Navigator.of(context).pop();
return Future.value(false);
},
child: DialogSearchHistory(
searchHistory: searchModel.searchHistory,
searchController: searchController,
),
),
));
As well as wrapping the Alert Dialog itself in the build function
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
Navigator.of(context).pop();
return Future.value(false);
},
child: AlertDialog(
backgroundColor: kCardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),...
Neither have worked. The page behind the Dialog pops back, but the Dialog stays put. onWillPop is never hit by the debugger
Late to the party but setting useRootNavigator to false inside showDialog worked for me.
showDialog(
context: context,
useRootNavigator: false,
builder: (context) {
return AlertDialog();
},
);
If the application has multiple Navigator objects, it may be necessary to call Navigator.of(context, rootNavigator: true).pop(result) to close the dialog rather than just Navigator.pop(context, result).
I hope this will close the alert dialog If you have multiple navigator objects. Give it a try
onWillPop: (){
Navigator.of(context, rootNavigator: true).pop();
return Future.value(false);
},
OR
Try Navigator.pop(context);, this will call internally Navigator.of(context).pop() method.
Official Implementation of Navigator.pop() method.
static void pop<T extends Object>(BuildContext context, [ T result ]) {
Navigator.of(context).pop<T>(result);
}
Doc - https://api.flutter.dev/flutter/widgets/Navigator/pop.html
Hi there i face similar issue and the below method worked for me as Vinoth vino tell wrap your page in WillPopScope widget and try to add pop function there like
body: WillPopScope(
onWillPop: (){
Navigator.of(context).pop();
return Future.value(false);
},
child:...your code
My problem is that I have a main page then sign in page then a homepage where if I press logout in the homepage it should navigate me back to the main page which contains the willpopscope , what I'm trying to do is to prevent the user from pressing the back button in the main page so that the app does not return to the homepage without the user being signed in , the problem is that the willpopscope does not work when I Navigator.push the homepage , so whenever I press the back button it returns me to the home page.
I tried changing the position of WillPopScope, If I wrap the whole widget by it , it will never work for me
Code to reproduce
//Main page:
class MainActivity extends StatefulWidget {
final bool isWelcome;
MainActivity(this.isWelcome);
#override
State<StatefulWidget> createState() {
return OperateMainActivity(isWelcome);
}
}
class OperateMainActivity extends State<MainActivity> {
bool isWelcome = false;
OperateMainActivity(this.isWelcome);
#override
Widget build(BuildContext context) {
//print(isWelcome);
return MaterialApp(
home: MyHome(
isWelcome: isWelcome,
));
}
}
class myHome extends StatelessWidget
return Scaffold(
body: Center(
child: WillPopScope(
onWillPop: () async => false,
child: Container(....)
// Home page
Navigator.push(context,MaterialPageRoute(builder: (context) => MainActivity(false)));
//When pushing to main activity WillPopScope does not work
WillPopScope doesn't get called when you push. That's the logical behavior, because when you're pushing another view it goes above the current view in the stack. Therefore, the current view never pops out.
If you know that your MainActivity is below you're current page in the stack, you could change:
Navigator.push(context,MaterialPageRoute(builder: (context) => MainActivity(false)));
to:
Navigator.pop(context); // This will make a call to onWillPop.
UPDATE:
For what you want to achieve, you could just use pushAndRemoveUntil:
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) {
return MainActivity();
},
),
(Route<dynamic> route) => false,
);
It will push the desired page and remove everything else from the stack.
I have a drawer on my main page which became bloated with content and logic. For example, I need to fetch an image, get some data from Internet and so on. After extracting Drawer in new MyCustomDrawerWidget which extends StatefulWidget, its state has build function which looks like this:
#override
Widget build(BuildContext context) {
return Drawer(
child: ...
);
}
My HomePageState has build function which looks like this:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home page'),
),
drawer: MyCustomDrawerWidget(),
body: ...
);
}
After refactoring the code like this, I noticed how drawer does not get initialized when page loads. It would initialize just after I open the drawer, which is bad because user needs to wait for data to be loaded.
Is there a way to set eager loading on drawer to get initialized along with the page?
I had the same problem. The Scaffold only attaches the drawer when necessary.
The solution: Move the data loading logic into the parent widget. You can still encapsulate it in a separate model class (e.g. DrawerModel), but this class must be instantiated when the parent widget is initialized. Keep it in the State of the parent. Pass DrawerModel to your drawer widget.
You have to be a bit careful when using streams and futures, because those usually can not be resubscribed. I would recommend you to use a StreamBuilder in the drawer widget that is connected to a BehaviorSubject in your DrawerModel.
That's very much the BLoC pattern. Do you need a code example?
Problem with having Drawer in MainPage:
Content, more logic, fetch an image, get some data from the Internet and so on.
Problem with having Drawer in MyCustomDrawerWidget:
Loading time, bad user experience
I would suggest getting the hybrid approach to solve the problem.
Trigger the network calls in MainPage
Send future to the MyCustomDrawerWidget (If the user opens drawer before network call completes, we can show loader based on future value)
Move the logic and contents related to drawer into MyCustomDrawerWidget.
class MyCustomDrawerWidget extends StatelessWidget {
var _future;
MyCustomDrawerWidget(this._future);
#override
Widget build(BuildContext context) {
return new Drawer(
child: FutureBuilder(
builder: (context, snapshot) {
if (snapshot.hasData) {
var data = snapshot.data;
return new Text(data);
} else {
return new Text("loading");
}
},
future: _future,
),
);
}
}
And in MainPage
#override
Widget build(BuildContext context) {
val future = Future.delayed(Duration(seconds: 10), () => "new value") //network call
return Scaffold(
appBar: AppBar(
title: Text('Home page'),
),
drawer: MyCustomDrawerWidget(future),
body: ...
);
}
Hope this helps.