Steps to reproduce:
Copy paste the below code in DartPad.dev/flutter
Hit run
Click the Do Api Call button
you should see two popups, one below and one above
After 5 seconds, the one below is desired to close not the one above, instead, the one above closes
How to close the one below and leave the one above open ?
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: CloseSpecificDialog(),
),
),
);
}
}
class CloseSpecificDialog extends StatefulWidget {
#override
_CloseSpecificDialogState createState() => _CloseSpecificDialogState();
}
class _CloseSpecificDialogState extends State<CloseSpecificDialog> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text('Do API call'),
onPressed: () async {
showDialogBelow();
showDialogAbove();
await Future.delayed(Duration(seconds: 5));
closeDialogBelowNotAbove();
},
)),
);
}
void showDialogBelow() {
showDialog(
context: context,
builder: (BuildContext contextPopup) {
return AlertDialog(
content: Container(
width: 350.0,
height: 150.0,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CircularProgressIndicator(),
Text('I am below (you should not see this after 5 sec)'),
],
),
),
),
);
});
}
void showDialogAbove() {
showDialog(
context: context,
builder: (BuildContext contextPopup) {
return AlertDialog(
content: Container(
height: 100.0,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CircularProgressIndicator(),
Text('I am above (this should not close)'),
],
),
),
),
);
});
}
/// This should close the dialog below not the one above
void closeDialogBelowNotAbove() {
Navigator.of(context).pop();
}
}
I had a similar requirement for my applications and had to spend quite some time to figure out the approach.
First I will tell you what advice I've got/read online which did not work for me:
Store BuildContext of each dialog from builder function when calling showDialog
Using Navigator.pop(context, rootNavigator: true)
removeRoute method on Navigator
None of these worked. #1 and #2 are a no-go because pop method can only remove the latest route/dialog on the navigation stack, so you can't really remove dialog that is placed below other dialog.
#3 was something I was hoping would work but ultimately it did not work for me. I tried creating enclosing Navigator for specific widget where I'm displaying the dialogs but pushing dialog as new route caused dialog being treated as page.
Solution: using Overlay widget
This is not a perfect solution but Overlay widget is actually used internally by other Flutter widgets, including Navigator. It allows you to control what gets placed in which order so it also means you can decide which element on overlay to remove!
My approach was to create a StatefulWidget which would contain a Stack. This stack would render whatever else passed to it and also Overlay widget. This widget would also hold references to OverlayEntry which are basically identifiers for dialogs themselves.
I'd use GlobalKey to reference the Overlay's state and then insert and remove dialogs (OverlayEntry) as I wished.
There is a disadvantage to this though:
No back button support on Android, so pressing back won't close the dialog.¹
Dialog positioning - you have to manage centering of your dialog yourself, as well as setting up the backdrop.²
Animations - you will have to implement these yourself as well. (You might want to fade in/ fade out backdrop, change position of dialog when opening and closing).
You can find interactive example on this dartpad or you can see the code here:
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey<OverlayState> _overlay = GlobalKey<OverlayState>();
OverlayEntry? _dialog1;
OverlayEntry? _dialog2;
#override
void initState() {
super.initState();
Timer(const Duration(seconds: 3), () {
_openDialog1();
debugPrint('Opened dialog 1. Dialog should read: "Dialog 1"');
Timer(const Duration(seconds: 2), () {
_openDialog2();
debugPrint('Opened dialog 2. Dialog should read: "Dialog 2"');
Timer(const Duration(seconds: 3), () {
_closeDialog1();
debugPrint('Closed dialog 1. Dialog should read: "Dialog 2"');
Timer(const Duration(seconds: 5), () {
_closeDialog2();
debugPrint('Closed dialog 2. You should not see any dialog at all.');
});
});
});
});
}
#override
void dispose() {
_closeDialog1();
_closeDialog2();
super.dispose();
}
Future<void> _openDialog1() async {
_dialog1 = OverlayEntry(
opaque: false,
builder: (dialogContext) => CustomDialog(
title: 'Dialog 1', timeout: false, onClose: _closeDialog1));
setState(() {
_overlay.currentState?.insert(_dialog1!);
});
}
Future<void> _openDialog2() async {
_dialog2 = OverlayEntry(
opaque: false,
builder: (dialogContext) => CustomDialog(
title: 'Dialog 2', timeout: false, onClose: _closeDialog2));
setState(() {
_overlay.currentState?.insert(_dialog2!);
});
}
Future<void> _closeDialog1() async {
setState(() {
_dialog1?.remove();
_dialog1 = null;
});
}
Future<void> _closeDialog2() async {
setState(() {
_dialog2?.remove();
_dialog2 = null;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Stack(
children: <Widget>[
Align(
child:
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
TextButton(onPressed: _openDialog1, child: const Text('Open 1')),
TextButton(onPressed: _openDialog2, child: const Text('Open 2')),
])),
Align(
alignment: Alignment.bottomCenter,
child: Text(
'Opened 1? ${_dialog1 != null}\nOpened 2? ${_dialog2 != null}'),
),
Overlay(key: _overlay),
],
),
);
}
}
class CustomDialog extends StatefulWidget {
const CustomDialog({
Key? key,
required this.timeout,
required this.title,
required this.onClose,
}) : super(key: key);
final String id;
final bool timeout;
final String title;
final void Function() onClose;
#override
createState() => _CustomDialogState();
}
class _CustomDialogState extends State<CustomDialog>
with SingleTickerProviderStateMixin {
late final Ticker _ticker;
Duration? _elapsed;
final Duration _closeIn = const Duration(seconds: 5);
late final Timer? _timer;
#override
void initState() {
super.initState();
_timer = widget.timeout ? Timer(_closeIn, widget.onClose) : null;
_ticker = createTicker((elapsed) {
setState(() {
_elapsed = elapsed;
});
});
_ticker.start();
}
#override
void dispose() {
_ticker.dispose();
_timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Positioned(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Stack(children: [
GestureDetector(
onTap: widget.onClose,
child: Container(
color: Colors.transparent,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height)),
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: AlertDialog(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
title: Text(widget.title),
content: SizedBox(
height: MediaQuery.of(context).size.height / 3,
child: Center(
child: Text([
'${_elapsed?.inMilliseconds ?? 0.0}',
if (widget.timeout) ' / ${_closeIn.inMilliseconds}',
].join('')))),
actions: [
TextButton(
onPressed: widget.onClose, child: const Text('Close'))
],
)),
]));
}
}
In my example you can see that when the app runs, it will start up Timer which will fire other timers. This only demonstrates that you are able to close/open specific dialogs programatically. Feel free to comment out initState method if you don't want this.
1: Since this solution does not use Navigator at all, you can't use WillPopScope to detect back button press. It's a shame, it'd be great if Flutter had a way to attach listener to back button press.
2: showDialog method does lot for you and you basically have to re-implement what it does within your own code.
Popping will remove route which is added the latest, and showDialog just pushes a new route with dialogue you can directly use the Dialog widgets in a Stack and manage the state using a boolean variable To Achieve same the effect,
class _MyHomePageState extends State<MyHomePage> {
bool showBelow = true;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
await Future.delayed(Duration(seconds: 5));
setState(() {
showBelow = false;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
if(showBelow) AlertDialog(
title: Text('Below..'),
content: Text('Beyond'),
),
AlertDialog(
title: Text('Above..'),
),
],
),
);
}
}
Remove
await Future.delayed(Duration(seconds: 5));
closeDialogBelowNotAbove();
Add Future.delayed
void showDialogAbove() {
showDialog(
context: context,
builder: (BuildContext contextPopup) {
Future.delayed(Duration(seconds: 5), () {
closeDialogBelowNotAbove();
});
return AlertDialog(
content: Container(
height: 100.0,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CircularProgressIndicator(),
Text('I am above (this should not close)'),
],
),
),
),
);
});
}
Note: Navigator.pop() method always pop above alert/widget available on the screen, as it works with BuildContext which widget currently has.
Related
How am I supposed to pass a value in this big mess called Flutter?
30 years old php global $var wasn't good?
All these years were to come up with setState, passed in a controller which get redeclared as a key inside a stateful widget that receive the value from a Navigator?
By the way, I tried using Navigator.push but it seems to open a completely new window, the value is there but I'd need it to show in the tab body not in a new window, below is my code:
main.dart
import 'dart:core';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter App',
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomeView(),
);
}
}
class HomeView extends StatefulWidget {
#override
_HomeViewState createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
final tabs = [QRViewExample(), SecondView(res: '')];
int _currentIndex = 0;
#override
void initState() {
setState(() {});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
toolbarHeight: 40.0,
elevation: 0,
centerTitle: true,
title: Text('Flutter App'),
),
body: tabs[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
backgroundColor: Colors.red,
currentIndex: _currentIndex,
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.white,
unselectedItemColor: Colors.white.withOpacity(0.5),
items: [
BottomNavigationBarItem(
icon: Icon(Icons.qr_code),
label: 'Scan',
),
BottomNavigationBarItem(
icon: Icon(Icons.list),
label: 'List',
),
],
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
),
);
}
}
// SECOND TAB WIDGET (custom)
class SecondView extends StatelessWidget {
const SecondView({Key? key, required this.res}) : super(key: key);
final String? res;
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text(res!),
),
);
}
}
// FIRST TAB WIDGET (qrcode)
class QRViewExample extends StatefulWidget {
const QRViewExample({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() => _QRViewExampleState();
}
class _QRViewExampleState extends State<QRViewExample> {
Barcode? result;
QRViewController? controller;
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
#override
void reassemble() {
super.reassemble();
if (Platform.isAndroid) {
controller!.pauseCamera();
}
controller!.resumeCamera();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
height: 500,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
Expanded(flex: 4, child: _buildQrView(context)),
Expanded(
flex: 1,
child: FittedBox(
fit: BoxFit.contain,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
if (result != null)
Text(
'Barcode Type: ${describeEnum(result!.format)} Data: ${result!.code}')
else
const Text('Scan a code'),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
margin: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: () async {
await controller?.toggleFlash();
setState(() {});
},
child: FutureBuilder(
future: controller?.getFlashStatus(),
builder: (context, snapshot) {
return Text('Flash: ${snapshot.data}');
},
)),
),
Container(
margin: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: () async {
await controller?.flipCamera();
setState(() {});
},
child: FutureBuilder(
future: controller?.getCameraInfo(),
builder: (context, snapshot) {
if (snapshot.data != null) {
return Text(
'Camera facing ${describeEnum(snapshot.data!)}');
} else {
return const Text('loading');
}
},
)),
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
margin: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: () async {
await controller?.pauseCamera();
},
child: const Text('pause',
style: TextStyle(fontSize: 20)),
),
),
Container(
margin: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: () async {
await controller?.resumeCamera();
},
child: const Text('resume',
style: TextStyle(fontSize: 20)),
),
)
],
),
],
),
),
)
],
),
),
),
);
}
Widget _buildQrView(BuildContext context) {
var scanArea = (MediaQuery.of(context).size.width < 400 ||
MediaQuery.of(context).size.height < 400)
? 150.0
: 300.0;
return QRView(
key: qrKey,
onQRViewCreated: _onQRViewCreated,
overlay: QrScannerOverlayShape(
borderColor: Colors.cyanAccent,
borderRadius: 10,
borderLength: 30,
borderWidth: 10,
cutOutSize: scanArea),
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
);
}
void _onQRViewCreated(QRViewController controller) {
setState(() {
this.controller = controller;
});
controller.scannedDataStream.listen((scanData) {
controller.pauseCamera();
setState(() {
result = scanData;
});
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondView(res: result!.code)))
.then((value) => controller.resumeCamera());
});
}
void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) {
log('${DateTime.now().toIso8601String()}_onPermissionSet $p');
if (!p) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('no Permission')),
);
}
}
#override
void dispose() {
controller?.dispose();
super.dispose();
}
}
How am I supposed to pass a value in this big mess called Flutter?
With state management tools like InheritedWidget, InheritedModel, Provider, BloC and many more.
30 years old php global $var wasn't good? All these years were to come up with setState, passed in a controller which get redeclared as a key inside a stateful widget that receive the value from a Navigator?
Well, you shouldn't do that and it's not meant to be done like that. We can use several methods to propagate data down the widget tree. Let me explain this with InheritedWidget. But sometimes you want to go for Provider which is a wrapper class for InheritedWidget.
First we create a class named QRListModel which extends InheritedModel:
class QRListModel extends InheritedWidget {
final List<Barcode> qrList = []; // <- This holds our data
QRListModel({required super.child});
#override
bool updateShouldNotify(QRListModel oldWidget) {
return !listEquals(oldWidget.qrList, qrList);
}
static QRListModel of(BuildContext context) {
final QRListModel? result = context.dependOnInheritedWidgetOfExactType<QRListModel>();
assert(result != null, 'No QRListModel found in context');
return result!;
}
}
updateShouldNotify is a method we have to override to tell Flutter, when we want the widgets to rebuild. We want this to happen when the list changes. The of method is just a handy way to access the QRListModel.
Now wrap a parent widget of both the scan tab view and the list tab view inside QRListModel. We go for HomeView:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter App',
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: QRListModel(child: HomeView()), // <- here!
);
}
}
We can take any parent widget but it should be a class where we don't call setState. Otherwise our QRListModel also gets rebuilt and our list is gone.
Now we can access QRListModel from anywhere inside the subtree. We need it here:
void _onQRViewCreated(QRViewController controller) {
setState(() {
this.controller = controller;
this.controller!.resumeCamera();
});
controller.scannedDataStream.listen((scanData) async {
controller.pauseCamera();
QRListModel.of(context).qrList.add(scanData); // <- Here we access the list
await showDialog(
context: context,
builder: (context) => SimpleDialog(
title: Text("Barcode was added!"),
children: [
Text(scanData.code!)
],
)
);
});
}
And here we read the list:
class SecondView extends StatelessWidget {
const SecondView({Key? key, required this.res}) : super(key: key);
final String? res;
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: QRListModel.of(context).qrList.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
title: Text(QRListModel.of(context).qrList[index].code ?? "NO"),
),
);
}
);
}
}
Now both pages have access to the qr list. Please do mind that a InheritedWidget can only have final fields. So if you need mutable fields, you need an additional wrapper class. We don't need it as we don't change the list but only its elements.
By the way: You shouldn't call setState inside initState. You did this here:
class _HomeViewState extends State<HomeView> {
final tabs = [QRViewExample(), SecondView(res: '')];
int _currentIndex = 0;
#override
void initState() {
setState(() {}); // <- Don't call setState inside initState!
super.initState();
}
I have two screens where the second screen is pushed above the first with Navigator.push() and the second screen is partial transparent. I want to display a SnackBar, but it isn't really visible. It looks like the ScaffoldMessenger chooses the wrong of the two Scaffolds to attach the Snackbar. This leads to the effect that the SnackBar collides with the TextInput and it is also not fully visible. But this bad behavior is only the case as long as the soft keyboard is open. If the keyboard is closed, everything works fine. It seems like the open keyboard tells the ScaffoldMessenger to choose the Scaffold from the second screen to display the SnackBar.
How can I achieve that the SnackBar is shown normally in the sense of is attached to the Scaffold of screen 2 while the keyboard is open? The expected behavior is that the Snackbar isn't displayed transparent.
Keyboard open -> SnackBar is attached to Scaffold of screen 1 -> Bad
Keyboard closed -> SnackBar is attached to Scaffold of screen 2 -> Good
GIF showing the complete workflow
My code (fully executable)
import 'dart:io';
import 'package:keyboard_utils/keyboard_listener.dart';
import 'package:keyboard_utils/keyboard_utils.dart';
import 'package:flutter/material.dart' hide KeyboardListener;
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => const MaterialApp(home: MyHomePage());
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Title')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[const Text('You have pushed the button this many times:'),
Text('$_counter', style: Theme.of(context).textTheme.headline4),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(PageRouteBuilder(
opaque: false, // push route with transparency
pageBuilder: (context, animation, secondaryAnimation) => const Screen2(),
));
},
child: const Text('navigate'),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => _counter++),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
class Screen2 extends StatefulWidget {
const Screen2({Key? key}) : super(key: key);
#override
State<Screen2> createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
final _keyboardUtils = KeyboardUtils();
late int _idKeyboardListener;
final focusNode = FocusNode();
bool isEmojiKeyboardVisible = false;
bool isKeyboardVisible = false;
double _keyboardHeight = 300;
#override
void initState() {
super.initState();
_idKeyboardListener = _keyboardUtils.add(
listener: KeyboardListener(
willHideKeyboard: () {
if (isKeyboardVisible) {
isKeyboardVisible = false;
isEmojiKeyboardVisible = false;
}
setState(() {}); // show correct Icon in IconButton
},
willShowKeyboard: (double keyboardHeight) async {
if (Platform.isAndroid) {
_keyboardHeight = keyboardHeight + WidgetsBinding.instance.window.viewPadding.top / WidgetsBinding.instance.window.devicePixelRatio;
} else {
_keyboardHeight = keyboardHeight;
}
isKeyboardVisible = true;
isEmojiKeyboardVisible = true;
setState(() {});
},
)
);
}
#override
void dispose() {
_keyboardUtils.unsubscribeListener(subscribingId: _idKeyboardListener);
if (_keyboardUtils.canCallDispose()) {
_keyboardUtils.dispose();
}
focusNode.dispose();
super.dispose();
}
Future<void> onEmojiButtonPressed() async {
if(isEmojiKeyboardVisible){
if(isKeyboardVisible){
FocusManager.instance.primaryFocus?.unfocus();
isKeyboardVisible = false;
} else {
focusNode.unfocus();
await Future<void>.delayed(const Duration(milliseconds: 1));
if(!mounted) return;
FocusScope.of(context).requestFocus(focusNode);
}
} else {
assert(!isKeyboardVisible);
setState(() {
isEmojiKeyboardVisible = true;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold( // wrapping with ScaffoldMessenger does NOT fix this bug
backgroundColor: Colors.white.withOpacity(0.5),
resizeToAvoidBottomInset: false,
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(child: SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
children: [
Expanded(
child: Container(
height: 200,
),
),
Row(
children: [
IconButton(
icon: Icon(isKeyboardVisible || !isEmojiKeyboardVisible ? Icons.emoji_emotions_outlined : Icons.keyboard_rounded),
onPressed: onEmojiButtonPressed,
),
Expanded(
child: TextField(
focusNode: focusNode,
),
),
IconButton(
icon: const Icon(Icons.send),
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('A snack!'))),
),
],
),
],
),
),
),
Offstage(
offstage: !isEmojiKeyboardVisible,
child: SizedBox(
height: _keyboardHeight,
child: Container(color: Colors.red),
),
),
],
),
),
);
}
}
Dependencies
keyboard_utils: ^1.3.4
What I've tried
I tried to wrap the Scaffold of Screen2 with a ScaffoldMessenger. This doesn't fix my problem. In that case, no SnackBar was shown at all if the keyboard was open.
Edit: I also created an GitHub issue for that but I don't expect an answer soon: https://github.com/flutter/flutter/issues/105406#issuecomment-1147194647
Edit 2: A workaround for this issue is to use SnackBarBehaviod.floating and a bottom margin, for example:
SnackBar(
content: Text('A snack!'),
margin: EdgeInsets.only(bottom: 350.0),
behavior: SnackBarBehavior.floating,
)
But this is not a satisfying solution.
I have TestApp, where I have SplitView with 2 horizontal Containers. By clicking button in the first container on the left(blue) I want to show new page (DetailPage widget) but not all over the page, but only in the first Container. Now it shows on the whole screen. What is a best approach to do it?
import 'package:flutter/material.dart';
import 'package:split_view/split_view.dart';
void main() {
runApp(MaterialApp(
title: 'Test',
home: TestApp(),
));
}
class TestApp extends StatelessWidget {
const TestApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: SplitView(
children: [
Container(
color: Colors.blue,
child: ElevatedButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => DetailPage()));
},
child: const Text('CLICK')),
),
Container(color: Colors.yellow),
],
viewMode: SplitViewMode.Horizontal,
indicator: SplitIndicator(viewMode: SplitViewMode.Horizontal),
activeIndicator: SplitIndicator(
viewMode: SplitViewMode.Horizontal,
isActive: true,
),
controller: SplitViewController(limits: [null, WeightLimit(max: 1)]),
),
);
}
}
class DetailPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('')), body: Container(color: Colors.red));
}
}
When pushing a new page you will be overriding the old one, meaning the new page will not have a spiltView, the best way to do this is by changing the widget displayed inside of the splitView like this :
import 'package:flutter/material.dart';
import 'package:split_view/split_view.dart';
void main() {
runApp(MaterialApp(
title: 'Test',
home: TestApp(),
));
}
class TestApp extends StatefulWidget { // I have already changed the widgte to stateful here
const TestApp({Key? key}) : super(key: key);
#override
_TestAppState createState() => _TestAppState();
}
class _TestAppState extends State<TestApp> {
#override
Widget build(BuildContext context) {
bool Bool;
return MaterialApp(
home: SplitView(
children: [
if (Bool == false){
Container(
color: Colors.blue,
child: ElevatedButton(
onPressed: () {
setState(() {
Bool = !Bool; // this the method for inverting the boolean, it just gives it the opposite value
});
},
child: const Text('CLICK')),
),
}
else{
DetailPage()
},
Container(color: Colors.yellow),
],
viewMode: SplitViewMode.Horizontal,
indicator: SplitIndicator(viewMode: SplitViewMode.Horizontal),
activeIndicator: SplitIndicator(
viewMode: SplitViewMode.Horizontal,
isActive: true,
),
controller: SplitViewController(limits: [null, WeightLimit(max: 1)]),
),
);
}
}
class DetailPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('')), body: Container(color: Colors.red));
}
}
Above I defined a bool called Bool, when rendering the page it checks if Bool is false, in that case it returns the blue widget, if it is true then it returns the red one, and when you click on the button it inverts the bool and updates the page.
Please note that for updating the page you have to use setState which rebuilds the widget, and to use it you have to use a stateful widget since stateless widget is static and cannot be changed.
Also I haven't tested the code because I don't have split_view package, but you should be able to copy and paste it just fine, if you get any errors please let me know.
When you use Navigator.push your routing to a new page and creating a new state. I think you should use showGeneralDialog instead.
showGeneralDialog(
context: context,
pageBuilder: (BuildContext context,
Animation<double> animation, Animation<double> pagebuilder) {
return Align(
alignment: Alignment.centerLeft,
child: Card(
child: Container(
alignment: Alignment.topLeft,
color: Colors.amber,
//show half the screen width
width: MediaQuery.of(context).size.width / 2,
child: IconButton(
icon: const Icon(Icons.cancel),
onPressed: () {
Navigator.pop(context);
}))),
);
});
try to create new Navigator within Container:
GlobalKey<NavigatorState> _navKey = GlobalKey();
home: SplitView(
children: [
Container(
child: Navigator(
key: _navKey,
onGenerateRoute: (_) => MaterialPageRoute<dynamic>(
builder: (_) {
return Container(
color: Colors.blue,
child: ElevatedButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => DetailPage()));
},
child: const Text('CLICK')),
);
},
),
),),
Say I have two pages :-
Tester1.dart
import 'package:flutter/material.dart';
import 'package:gelijk_spel/Testers2.dart';
class Testers1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: IconButton(
icon: Hero(
tag: 'elderimg',
child: Image.asset(
'assets/Images/elder.png', width: 100, height: 100,),
),
onPressed: () {
Navigator.push(context,
MaterialPageRoute(
builder: (context) => Testers2()
)
);
}
),
),
),
);
}
}
and Tester2.dart
import 'package:flutter/material.dart';
class Testers2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Screen2'),
),
body: Column(
children: <Widget>[
Text('Placeholder0'),
Hero(
tag: 'elderimg',
child: Image.asset('assets/Images/elder.png',width: 100, height: 100,)
),
Text('Placeholder1'),
],
),
),
);
}
}
I want to be able to transition from Tester1 to Tester2, but instead of having to click on the screen or any events, just sequentially, after some time. How can I achieve this?
One possible approach is to use Timer and a StatefulWidget so you can set up a timer with a trigger that will run your Navigation code as soon as the timer runs out and triggers.
This is the code example:
class Testers1 extends StatefulWidget {
#override
_Testers1State createState() => _Testers1State();
}
class _Testers1State extends State<Testers1> {
Widget build(BuildContext context) {
return Scaffold(
body: Container()
);
}
#override
void initState() {
super.initState();
Timer(Duration(seconds: 10), () {
Navigator.push(
context, MaterialPageRoute(builder: (context) => Testers2()));
});
}
}
class Testers2 extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Screen2'),
),
body: Column(
children: <Widget>[
Text('Placeholder0'),
Text('Placeholder1'),
],
),
);
}
}
NOTE: Timer is coming from the 'dart:async'
Yes, you have to use Future.delayed() on init() function in first page
Like this(Add this to First page):
Future<void> initState() {
super.initState();
//replace 1000 with your time required
Future.delayed(Duration(milliseconds: 1000), () {
Navigator.push(currentContext,
MaterialPageRoute(builder: (context) => Testers2()));
},
});
}
In Flutter, I want to make screens like with Fragment in android, in this my code i try to replace each screens into current screen like with Fragment.replecae in android, i used Hook and Provider and my code work fine when in click on buttons to switch between them but i can't implementing back stack, which means when i click on Back button on phone, my code should show latest screen which i stored into _backStack variable, each swtich between this screens i stored current screen index into the this variable.
how can i solve back from this stack in my sample code?
// Switch Between screens:
DashboardPage(), UserProfilePage(), SearchPage()
-------------> -------------> ------------->
// When back from stack:
DashboardPage(), UserProfilePage(), SearchPage()
Exit from application <-------------- <---------------- <-----------
i used Hook and i want to implementing this action with this library features
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:provider/provider.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MultiProvider(providers: [
Provider.value(value: StreamBackStackSupport()),
StreamProvider<homePages>(
create: (context) =>
Provider.of<StreamBackStackSupport>(context, listen: false)
.selectedPage,
)
], child: StartupApplication()));
}
class StartupApplication extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'BackStack Support App',
home: MainBodyApp(),
);
}
}
class MainBodyApp extends HookWidget {
final List<Widget> _fragments = [
DashboardPage(),
UserProfilePage(),
SearchPage()
];
List<int> _backStack = [0];
int _currentIndex = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('BackStack Screen'),
),
body: WillPopScope(
// ignore: missing_return
onWillPop: () {
customPop(context);
},
child: Container(
child: Column(
children: <Widget>[
Consumer<homePages>(
builder: (context, selectedPage, child) {
_currentIndex = selectedPage != null ? selectedPage.index : 0;
_backStack.add(_currentIndex);
return Expanded(child: _fragments[_currentIndex]);
},
),
Container(
width: double.infinity,
height: 50.0,
padding: const EdgeInsets.symmetric(horizontal: 15.0),
color: Colors.indigo[400],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () => Provider.of<StreamBackStackSupport>(
context,
listen: false)
.switchBetweenPages(homePages.screenDashboard),
child: Text('Dashboard'),
),
RaisedButton(
onPressed: () => Provider.of<StreamBackStackSupport>(
context,
listen: false)
.switchBetweenPages(homePages.screenProfile),
child: Text('Profile'),
),
RaisedButton(
onPressed: () => Provider.of<StreamBackStackSupport>(
context,
listen: false)
.switchBetweenPages(homePages.screenSearch),
child: Text('Search'),
),
],
),
),
],
),
),
),
);
}
void navigateBack(int index) {
useState(() => _currentIndex = index);
}
void customPop(BuildContext context) {
if (_backStack.length - 1 > 0) {
navigateBack(_backStack[_backStack.length - 1]);
} else {
_backStack.removeAt(_backStack.length - 1);
Provider.of<StreamBackStackSupport>(context, listen: false)
.switchBetweenPages(homePages.values[_backStack.length - 1]);
Navigator.pop(context);
}
}
}
class UserProfilePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenProfile ...'),
);
}
}
class DashboardPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenDashboard ...'),
);
}
}
class SearchPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenSearch ...'),
);
}
}
enum homePages { screenDashboard, screenProfile, screenSearch }
class StreamBackStackSupport {
final StreamController<homePages> _homePages = StreamController<homePages>();
Stream<homePages> get selectedPage => _homePages.stream;
void switchBetweenPages(homePages selectedPage) {
_homePages.add(homePages.values[selectedPage.index]);
}
void close() {
_homePages.close();
}
}
TL;DR
The full code is at the end.
Use Navigator instead
You should approach this problem differently. I could present you with a solution that would work with your approach, however, I think that you should instead solve this by implementing a custom Navigator as this is a built-in solution in Flutter.
When you are using a Navigator, you do not need any of your stream-based management, i.e. you can remove StreamBackStackSupport entirely.
Now, you insert a Navigator widget where you had your Consumer before:
children: <Widget>[
Expanded(
child: Navigator(
...
),
),
Container(...), // Your bottom bar..
]
The navigator manages its routes using strings, which means that we will need to have a way to convert your enum (which I renamed to Page) to Strings. We can use describeEnum for that and put that into an extension:
enum Page { screenDashboard, screenProfile, screenSearch }
extension on Page {
String get route => describeEnum(this);
}
Now, you can get the string representation of a page using e.g. Page.screenDashboard.route.
Furthermore, you want to map your actual pages to your fragment widgets, which you can do like this:
class MainBodyApp extends HookWidget {
final Map<Page, Widget> _fragments = {
Page.screenDashboard: DashboardPage(),
Page.screenProfile: UserProfilePage(),
Page.screenSearch: SearchPage(),
};
...
To access the Navigator, we need to have a GlobalKey. Usually we would have a StatefulWidget and manage the GlobalKey like that. Since you want to use flutter_hooks, I opted to use a GlobalObjectKey instead:
#override
Widget build(BuildContext context) {
final navigatorKey = GlobalObjectKey<NavigatorState>(context);
...
Now, you can use navigatorKey.currentState anywhere in your widget to access this custom navigator. The full Navigator setup looks like this:
Navigator(
key: navigatorKey,
initialRoute: Page.screenDashboard.route,
onGenerateRoute: (settings) {
final pageName = settings.name;
final page = _fragments.keys.firstWhere((element) => describeEnum(element) == pageName);
return MaterialPageRoute(settings: settings, builder: (context) => _fragments[page]);
},
)
As you can see, we pass the navigatorKey created before and define an initialRoute, making use of the route extension we created. In onGenerateRoute, we find the Page enum entry corresponding to the route name (a String) and then return a MaterialPageRoute with the appropriate _fragments entry.
To push a new route, you simply use the navigatorKey and pushNamed:
onPressed: () => navigatorKey.currentState.pushNamed(Page.screenDashboard.route),
Back button
We also need to customly call pop on our custom navigator. For this purpose, a WillPopScope is needed:
WillPopScope(
onWillPop: () async {
if (navigatorKey.currentState.canPop()) {
navigatorKey.currentState.pop();
return false;
}
return true;
},
child: ..,
)
Access the custom navigator inside of the nested pages
In any page that is passed to onGenerateRoute, i.e. in any of your "fragments", you can just call Navigator.of(context) instead of using the global key. This is possible because these routes are children of the custom navigator and thus, the BuildContext contains that custom navigator.
For example:
// In SearchPage
Navigator.of(context).pushNamed(Page.screenProfile.route);
Default navigator
You might be wondering how you can get access to the MaterialApp root navigator now, e.g. to push a new full screen route. You can use findRootAncestorStateOfType for that:
context.findRootAncestorStateOfType<NavigatorState>().push(..);
or simply
Navigator.of(context, rootNavigator: true).push(..);
Here is the full code:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
void main() {
runApp(StartupApplication());
}
enum Page { screenDashboard, screenProfile, screenSearch }
extension on Page {
String get route => describeEnum(this);
}
class StartupApplication extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'BackStack Support App',
home: MainBodyApp(),
);
}
}
class MainBodyApp extends HookWidget {
final Map<Page, Widget> _fragments = {
Page.screenDashboard: DashboardPage(),
Page.screenProfile: UserProfilePage(),
Page.screenSearch: SearchPage(),
};
#override
Widget build(BuildContext context) {
final navigatorKey = GlobalObjectKey<NavigatorState>(context);
return WillPopScope(
onWillPop: () async {
if (navigatorKey.currentState.canPop()) {
navigatorKey.currentState.pop();
return false;
}
return true;
},
child: Scaffold(
appBar: AppBar(
title: Text('BackStack Screen'),
),
body: Container(
child: Column(
children: <Widget>[
Expanded(
child: Navigator(
key: navigatorKey,
initialRoute: Page.screenDashboard.route,
onGenerateRoute: (settings) {
final pageName = settings.name;
final page = _fragments.keys.firstWhere(
(element) => describeEnum(element) == pageName);
return MaterialPageRoute(settings: settings,
builder: (context) => _fragments[page]);
},
),
),
Container(
width: double.infinity,
height: 50.0,
padding: const EdgeInsets.symmetric(horizontal: 15.0),
color: Colors.indigo[400],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () => navigatorKey.currentState
.pushNamed(Page.screenDashboard.route),
child: Text('Dashboard'),
),
RaisedButton(
onPressed: () => navigatorKey.currentState
.pushNamed(Page.screenProfile.route),
child: Text('Profile'),
),
RaisedButton(
onPressed: () => navigatorKey.currentState
.pushNamed(Page.screenSearch.route),
child: Text('Search'),
),
],
),
),
],
),
),
),
);
}
}
class UserProfilePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenProfile ...'),
);
}
}
class DashboardPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenDashboard ...'),
);
}
}
class SearchPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Text(' screenSearch ...'),
);
}
}