FLUTTER: How to show dialog before beginning of the app? - dart

I want to show a confirmation alert dialog right before opening the app, can someone please tell me how can I achieve that in flutter?
The showDialog() method needs a context, hence I should put it somewhere with buildContext, I assume in app's build method, but how can I trigger the dialog before the actual layout will be built on screen?

In your initState you can add your callback which will show your dialog with WidgetsBinding.instance.addPostFrameCallback which will be displayed immediately after layout. You can update your layout state according to your dialog result.
class HomePageState extends State<HomePage> {
#override
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => showDialog(...));
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('HomePage'),
),
body: Container(),
);
}

Code below works, I guess this is the answer
#override
Widget build(BuildContext context) {
Future.delayed(Duration.zero, () => showAlert(context));
return new WhateverLayoutYouWantToBeDisplayed();
}
void showAlert(BuildContext context) {
showDialog(
child: new WhateverCustomDialogYouHave(),
context: context);
}

Best ways of doing this,
1. WidgetsBinding
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog();
});
2. SchedulerBinding
SchedulerBinding.instance.addPostFrameCallback((_) {
showDialog();
});
WidgetsBinding & SchedulerBinding will be called only once as we called it in initState(), but remember it will be called when the build method finished its rendering.
void initState() {
// TODO: implement initState
super.initState();
print("initState");
WidgetsBinding.instance.addPostFrameCallback((_) {
print("WidgetsBinding");
});
SchedulerBinding.instance.addPostFrameCallback((_) {
print("SchedulerBinding");
});
}
Detail Description: https://medium.com/flutterworld/flutter-schedulerbinding-vs-widgetsbinding-149c71cb607f

Related

Equivalent of viewWillAppear() in Flutter

I'm rebuilding an iOS app using Flutter and the flow is as followed:
Everytime the user lands on the homepage, the user data is reloaded from the backend to check if anything has changed.
The way I achieve this in Swift / iOS is by using the viewDidLoad() function.
My Flutter code is like this:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
User user = User();
#override
void dispose() {
super.dispose();
}
#override
void initState() {
super.initState();
_fetchData(context);
}
#override
Widget build(BuildContext context) {
return Container(
color: RColor.COLOR_main,
child: Column(
children: [
Container(
height: MediaQuery.of(context).size.height / 7,
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.all(20),
child: Container(
child: Text("This is the homepage"),
alignment: Alignment.bottomCenter,
),
),
],
));
}
Future _fetchData(BuildContext context) async {
_fetchUserAPI(context);
}
Future _fetchUserAPI(BuildContext context) async {
try {
SharedPreferences prefs = await SharedPreferences.getInstance();
var accessToken = prefs.getString("access_token");
var url = RConstants.API_BASE_URL + "/v1/users/self";
Response response = await Dio()
.get(url, options: Options(headers: {"Authorization": accessToken}));
setState(() {
user = User.fromJson(response.data);
});
} catch (e) {
print(e.toString());
Alert(
context: context,
title: "Something Went Wrong",
desc: "Something went wrong while fetching your user data",
type: AlertType.error)
.show();
}
}
}
void initState() however, doesn't get triggered each time the user lands on the homepage. What is the correct way to achieve this?
Well, it really depends on what exactly you mean by "Everytime the user lands on the homepage".
If user navigates from the HomePage to some other view via Navigator.push and then goes back via Navigator.pop then the HomePage's state remain the same and of course the initState method does not trigger.
If you want to get notified on the HomePage if the route above it in the navigator gets popped then you need to use this method, override it and then inside it you will be able to call the _fetchData() and update the homepage's state.
One more thing: when you have some async call like _fetchData() it is a wrong pattern to just invoke it inside the initState() or any other framework methods. Because it will be invoked and the build() method of your state will almost always be invoked immediately before the result of async call will come back. The correct way to handle such situations is to use the FutureBuilder widget.
If "Everytime the user lands on the homepage" means something else, like e.g. the app was in the background and gets brought foreground or when there is support for push notifications implemented and users click on the notification and the app is opened - such cases also can be handled but that is a broader topic.
RouteAware can help.
define final RouteObserver<ModalRoute<void>> routeObserver = RouteObserver<ModalRoute<void>>(); in main.dart.
set navigatorObservers: [ routeObserver ], in MaterialApp function.
mixin RouteAware in your page that need achieve viewWillAppear feature.
override didChangeDependencies and didPopNext methods and subscribe this page to routeObserver.
For example:
main.dart
final RouteObserver<ModalRoute<void>> routeObserver = RouteObserver<ModalRoute<void>>();
void main() {
runApp(MaterialApp(
home: HomePage(),
navigatorObservers: [ routeObserver ],
));
}
home_page.dart
class _HomePageState extends State<HomePage> with RouteAware {
#override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context)!);
}
#override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
#override
void didPopNext() {
super.didPopNext();
debugPrint("viewWillAppear");
}
}

How to detect whether the route is the current route in its build method

I use FutureBuilder in my routes, which displays data after getting data from database.
I found that when I open the second route, the build method of home route was called even home route is not the current route. However, I hope the build method does not get data if home route isn't the current one.
That's the code I try to implement:
class HomeRoute extends StatefulWidget { State<StatefulWidget> createState() => HomeRouteState(); }
class HomeRouteState extends State<HomeRoute> {
#override
Widget build(BuildContext context) => Scaffold(
//...
drawer: Drawer(
// There is a ListTile that can push SecondRoute
),
body: FutureBuilder(
future: _getData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {/* ... */}
),
);
_getData() async {
if(/* this route is the current one */) {
// get data
}
}
}
So basically you want to prevent the build() of your home page from getting called when it is not visible. Here is what you can try.
bool _isHomeVisible = true;
#override
Widget build(BuildContext context) {
return _isHomeVisible ? YourWidgetImplementation() : Container();
}
And before you navigate do this.
void _navigateToNewPage() {
_isHomeVisible = false; // going to new page, make it false
Navigator.push(...).then((_) {
_isHomeVisible = true; // coming back to home page, make it true
});
}

How to open Scaffold's Drawer on page load?

Logging into our Flutter app opens to dashboard that has a Scaffold with a Drawer full of menu items.
I'd like to perform some A/B testing with having the Drawer open on page load or at least animating the Drawer being opened immediately on load.
I'm aware of Scaffold.of(context).openDrawer() but I'm not sure where to place this code so that it will run immediately after the build() method. I'm also not aware of any fields on either Drawer or Scaffold which would load with the Drawer open.
Thanks for your time and help.
You need to wait after the first frame is loaded.
_onLayoutDone(_) {
//your logic here
}
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_onLayoutDone);
super.initState();
}
I wrote a post about this, you can take a look if you want : https://medium.com/#diegoveloper/flutter-widget-size-and-position-b0a9ffed9407
Override initState.
#override
void initState() {
super.initState();
// use this
Timer.run(() => Scaffold.of(context).openDrawer());
}
Store a state variable to hide and show drawer - isDrawerBeingShown.
Based on the state variable toggle the state of drawer. It is set to false by default so it will be displayed for the first time.
void _showDrawer(BuildContext context) async it must be marked as async so that it runs after build method.
Create showDrawerUtility method to show drawer on demand when ever required.
Edit:
Use GlobalKey
GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey();
class MainScreen extends StatefulWidget {
MainScreen({Key key }) : super(key: key);
#override
State<MainScreen> createState() => new MainScreenState();
}
class MainScreenState extends State<MainScreen> {
bool isDrawerBeingShown;
#override
void initState() {
super.initState();
isDrawerBeingShown = false;
_showDrawer(context);
}
void _showDrawer(BuildContext context) async {
if(!isDrawerBeingShown) {
_scaffoldKey.currentState.openDrawer();
setState(() => isDrawerBeingShown = true);
}
}
#override
Widget build(BuildContext context) { // build method goes here}
}
follow my code
import 'package:easy_debounce/easy_debounce.dart';
import 'package:flutter/material.dart';
GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey();
class openDrawerOnLoadPage extends StatefulWidget {
openDrawerOnLoadPage({Key? key}) : super(key: key);
#override
_openDrawerOnLoadPageState createState() => _openDrawerOnLoadPageState();
}
class _openDrawerOnLoadPageState extends State<openDrawerOnLoadPage> {
late bool isDrawerBeingShown;
#override
void initState() {
super.initState();
isDrawerBeingShown = false;
_showDrawer(context);
}
void _showDrawer(BuildContext context) async {
if (!isDrawerBeingShown) {
EasyDebounce.debounce('openDrawer', Duration(milliseconds: 100),
() async {
_scaffoldKey.currentState!.openDrawer();
setState(() => isDrawerBeingShown = true);
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
);
}
}

Flutter camera frame drops when switching between pages

I have tried to use the Flutter camera plugin (0.2.1) in combination with a PageView and a BottomNavigationBar, but everytime the page gets switched, a few frames get skipped and the UI freezes for a second.
I've simplified my codebase for this example:
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
void main() => runApp(new Pages());
class Pages extends StatefulWidget {
#override
_PagesState createState() => _PagesState();
}
class _PagesState extends State<Pages> {
PageController _pageController;
int _page = 0;
#override
void initState() {
_pageController = new PageController();
super.initState();
}
void navTapped(int page) {
_pageController.animateToPage(page,
duration: const Duration(milliseconds: 300), curve: Curves.ease);
}
void onPageChanged(int page) {
setState(() {
this._page = page;
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: new Text("CameraTest"),
),
body: PageView(
children: <Widget>[Feed(), Camera(), Profile()],
controller: _pageController,
onPageChanged: onPageChanged,
),
bottomNavigationBar: new BottomNavigationBar(
items: [
new BottomNavigationBarItem(
icon: new Icon(Icons.home), title: new Text("Feed")),
new BottomNavigationBarItem(
icon: new Icon(Icons.camera), title: new Text("Capture")),
new BottomNavigationBarItem(
icon: new Icon(Icons.person), title: new Text("Profile"))
],
onTap: navTapped,
currentIndex: _page,
),
),
);
}
}
class Camera extends StatefulWidget {
#override
_CameraState createState() => _CameraState();
}
class _CameraState extends State<Camera> {
List<CameraDescription> _cameras;
CameraController _controller;
initCameras() async{
_cameras = await availableCameras();
_controller = new CameraController(_cameras[0], ResolutionPreset.medium);
await _controller.initialize();
setState(() {});
}
#override
void initState() {
initCameras();
super.initState();
}
#override
void dispose() {
_controller?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
if (_controller == null || !_controller.value.isInitialized) {
return new Center(
child: new Text("Waiting for camera...", style: TextStyle(color: Colors.grey),),
);
}
return new AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: new CameraPreview(_controller));
}
}
//just placeholder widgets
class Feed extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(child: new Text("Feed"));
}
}
class Profile extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(child: new Text("Profile"));
}
}
There are basically three pages with the middle one showing a camera-preview
(how it's supposed to look), but on switching to the camera and back from it this happens. This is really annoying since it ruins the user experience and is not smooth at all. The lag appears when calling initCameras() or when disposing the camera-controller. I tried using initCameras() in combination with a FutureBuilder, which didn't help at all, and running the method in a seperate isolate, but platform calls seem to be only allowed on the main isolate. It seems a bit weird to me since opening the camera doesn't need too much cpu power, so an async method should be fine. I am aware there is an image-picker plugin, but I want to have the preview in the app directly. I have also considered to run initCameras() on app start, but i don't want to have the camera running all the time when the user is just using another page of the app.
Is there any way to improve upon initCameras() or perhaps use a different implementation to fix the stuttering? I wouldn't even care if it takes a second to load, but i don't want any frame skips.
I followed the example on the bottom of the camera page.
Tested on physical devices as well as emulators on different Android versions.
I have solved a similar issue by adding a delay before initializing the camera:
Future.delayed(Duration(seconds: 1), () {
initCameras();
});
This way the initCameras() is called after the page navigation animation is completed. You can show a CircularProgressIndicator() to make this delay more user friendly.
I am sure there is a more neat way to work around the issue, but this seems to be the simplest solution.
If you want to use image path in the different page than store image path as the global variable, and use it where you want.

Flutter: Run method on Widget build complete

I would like to be able to run functions once a Widget has finished building/loading but I am unsure how.
My current use case is to check if a user is authenticated and if not, redirect to a login view. I do not want to check before and push either the login view or the main view, it needs to happen after the main view has loaded.
Is there anything I can use to do this?
You could use
https://github.com/slightfoot/flutter_after_layout
which executes a function only one time after the layout is completed.
Or just look at its implementation and add it to your code :-)
Which is basically
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => yourFunction(context));
}
UPDATE: Flutter v1.8.4
Both mentioned codes are working now:
Working:
WidgetsBinding.instance
.addPostFrameCallback((_) => yourFunction(context));
Working
import 'package:flutter/scheduler.dart';
SchedulerBinding.instance.addPostFrameCallback((_) => yourFunction(context));
Best ways of doing this,
1. WidgetsBinding
WidgetsBinding.instance.addPostFrameCallback((_) {
print("WidgetsBinding");
});
2. SchedulerBinding
SchedulerBinding.instance.addPostFrameCallback((_) {
print("SchedulerBinding");
});
It can be called inside initState, both will be called only once after Build widgets done with rendering.
#override
void initState() {
// TODO: implement initState
super.initState();
print("initState");
WidgetsBinding.instance.addPostFrameCallback((_) {
print("WidgetsBinding");
});
SchedulerBinding.instance.addPostFrameCallback((_) {
print("SchedulerBinding");
});
}
both above codes will work the same as both use the similar binding framework.
For the difference find the below link.
https://medium.com/flutterworld/flutter-schedulerbinding-vs-widgetsbinding-149c71cb607f
There are 3 possible ways:
1) WidgetsBinding.instance.addPostFrameCallback((_) => yourFunc(context));
2) Future.delayed(Duration.zero, () => yourFunc(context));
3) Timer.run(() => yourFunc(context));
As for context, I needed it for use in Scaffold.of(context) after all my widgets were rendered.
But in my humble opinion, the best way to do it is this:
void main() async {
WidgetsFlutterBinding.ensureInitialized(); //all widgets are rendered here
await yourFunc();
runApp( MyApp() );
}
Flutter 1.2 - dart 2.2
According with the official guidelines and sources if you want to be certain that also the last frame of your layout was drawned you can write for example:
import 'package:flutter/scheduler.dart';
void initState() {
super.initState();
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((_) => yourFunction(context));
}
}
If you are looking for ReactNative's componentDidMount equivalent, Flutter has it. It's not that simple but it's working just the same way. In Flutter, Widgets do not handle their events directly. Instead they use their State object to do that.
class MyWidget extends StatefulWidget{
#override
State<StatefulWidget> createState() => MyState(this);
Widget build(BuildContext context){...} //build layout here
void onLoad(BuildContext context){...} //callback when layout build done
}
class MyState extends State<MyWidget>{
MyWidget widget;
MyState(this.widget);
#override
Widget build(BuildContext context) => widget.build(context);
#override
void initState() => widget.onLoad(context);
}
State.initState immediately will be called once upon screen has finishes rendering the layout. And will never again be called even on hot reload if you're in debug mode, until explicitly reaches time to do so.
In flutter version 1.14.6, Dart version 28.
Below is what worked for me, You simply just need to bundle everything you want to happen after the build method into a separate method or function.
#override
void initState() {
super.initState();
print('hello girl');
WidgetsBinding.instance
.addPostFrameCallback((_) => afterLayoutWidgetBuild());
}
The PostFrameCallback fires before the screen has fully painted. Therefore Devv's answer above was helpful with the added delay to allow the screen to paint.
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(Duration(seconds: 3), () => yourFunction());
});
}
Try SchedulerBinding,
SchedulerBinding.instance
.addPostFrameCallback((_) => setState(() {
isDataFetched = true;
}));
if you having issue with new SDK and old answer you can try my solution.I have tested it on v3.0.4
WidgetsBinding.instance.endOfFrame.then(
(_) {
if (mounted) {
// do some suff
// you can get width height of specific widget based on GlobalKey
};
},
);
If you don't want to use WidgetsBinding or SchedulerBinding:
Use Future or Timer (easy-peasy)
Future<void> _runsAfterBuild() async {
// This code runs after build ...
}
#override
Widget build(BuildContext context) {
Future(_runsAfterBuild); // <-- Use Future or Timer
return Container();
}
Await a dummy Future
Future<void> _runsAfterBuild() async {
await Future((){}); // <-- Dummy await
// This code runs after build ...
}
#override
Widget build(BuildContext context) {
_runsAfterBuild();
return Container();
}
my english is poor
forgive me
import 'package:flutter/material.dart';
class TestBox extends StatefulWidget {
final Color color;
final Duration delay;
const TestBox({
Key? key,
this.color = Colors.red,
this.delay = const Duration(seconds: 5),
}) : super(key: key);
#override
_TestBoxState createState() => _TestBoxState();
}
class _TestBoxState extends State<TestBox> {
String? label;
#override
void initState() {
initialMembers();
super.initState();
}
void initialMembers() async {
label = await fetchLabel();
if (mounted) setState(() {});
/// don't worry
/// if `(!mounted)`, means wen `build` calld
/// the label already has the newest value
}
Future<String> fetchLabel() async {
await Future.delayed(widget.delay);
print('fetchLabel call');
return 'from fetchLabel()';
}
#override
Widget build(BuildContext context) {
return AnimatedContainer(
margin: EdgeInsets.symmetric(vertical: 12),
duration: Duration(milliseconds: 500),
width: 220,
height: 120,
color: label == null ? Colors.white : widget.color,
child: Center(
child: Text(label ?? 'fetching...'),
),
);
}
}
Column(
children: [
TestBox(
delay: Duration(seconds: 1),
color: Colors.green,
),
TestBox(
delay: Duration(seconds: 3),
color: Colors.yellow,
),
TestBox(
delay: Duration(seconds: 5),
color: Colors.red,
),
],
),
I have a Stateful widget where I use html_editor_enhanced plugin widget. This is the only way to set initial message in it.
class _SendChatMessageState extends State<SendChatMessage> {
final _htmlController = HtmlEditorController();
#override
void initState() {
super.initState();
Future.delayed(const Duration(seconds: 3), () {
_htmlController.setText(widget.chatMessage.message ?? '');
});
}
I tried addPostFrameCallback but it didn't work because a JavaScript generates exception "HTML editor is still loading, please wait before evaluating this JS ..."
another solution that worked pretty well for me is wrapping the function you want to call by Future.delayed() as showen below:
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(Duration(seconds: 3), () => yourFunction());
});
}

Resources