How can I stop radio stream when navigating in Flutter? - flutter

I'm working on this flutter radio streaming app that currently works.I'm using AnimatedIcons.play_pause to start and stop the radio stream. Currently if I navigate to another page, the stream will keep playing. I'd like to stop the streaming (without using the pause button) when I navigate to another page. The reason is when you return back to the streaming page ... the page has reset back to the play buttonbool isPlaying = false;
but the stream is still on and so pressing the play button will result in 2 streams playing.
Here's the full code below:
import 'dart:async';
import 'package:flutter_radio/flutter_radio.dart';
void main() => runApp(HarvestRadio());
class HarvestRadio extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<HarvestRadio>
with SingleTickerProviderStateMixin {
String url = "https://fm898.online/mystream.mp3";
AnimationController _animationController;
bool isPlaying = false;
#override
void initState() {
super.initState();
audioStart();
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
}
#override
void dispose() {
super.dispose();
_animationController.dispose();
}
Future<void> audioStart() async {
await FlutterRadio.audioStart();
print('Audio Start OK');
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Harvest Radio Online',
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Harvest Radio Online '),
backgroundColor: Colors.blue,
centerTitle: true,
),
body: Container(
color: Colors.blueGrey.shade900,
child: Column(
children: <Widget>[
Expanded(
flex: 7,
child: Icon(
Icons.radio, size: 250,
color: Colors.lightBlue,
),
),
Material(
color: Colors.blueGrey.shade900,
child: Center(
child: Ink(
decoration: const ShapeDecoration(
color: Colors.lightBlue,
shape: CircleBorder(),
),
child: IconButton(
color: Colors.blueGrey.shade900,
iconSize: 67,
icon: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _animationController,
),
onPressed: () => _handleOnPressed(),
),
),
),
),
SizedBox(height: 50,)
],
),
),
));
}
void _handleOnPressed() {
setState(() {
FlutterRadio.play(url: url);
isPlaying = !isPlaying;
isPlaying
? _animationController.forward()
: _animationController.reverse();
});
}
}
Any help is much appreciated ... Thank you!

I had a similar situation, and ended up extending the routeObserver to know the users state all time:
the routeObserver.dart:
import 'package:flutter/material.dart';
class RouteObserverUtil extends RouteObserver<PageRoute<dynamic>> {
RouteObserverUtil({this.onChange});
final Function onChange;
void _sendScreenView(PageRoute<dynamic> route) {
String screenName = route.settings.name;
onChange(screenName);
}
#override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
super.didPush(route, previousRoute);
if (route is PageRoute) {
_sendScreenView(route);
}
}
#override
void didReplace({Route<dynamic> newRoute, Route<dynamic> oldRoute}) {
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
if (newRoute is PageRoute) {
_sendScreenView(newRoute);
}
}
#override
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
super.didPop(route, previousRoute);
if (previousRoute is PageRoute && route is PageRoute) {
_sendScreenView(previousRoute);
}
}
}
and here is a usage example:
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:myApp/utils/routeObserver.dart';
class _RouterScreenState extends State<RouterScreen> {
RouteObserverUtil _routeObserver;
#override
void initState() {
super.initState();
_routeObserver = RouteObserverUtil(onChange: _onChangeScreen);
}
void _onChangeScreen(String screen) {
SchedulerBinding.instance.addPostFrameCallback((_) {
print("changed to screen: " + screen);
});
}
...
}

I found a simple solution by wrapping my MaterialApp with WillPopScope ...Here's the code:
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _onBackPressed,
child: MaterialApp(
title: 'Harvest Radio Online',
home: Scaffold(
appBar: AppBar(
title: const Text('Harvest Radio Online '),
backgroundColor: Colors.blue,
centerTitle: true,
),
Then the function for _onBackPressed:
Future<bool>_onBackPressed(){
FlutterRadio.stop();
Navigator.pop(
context, MaterialPageRoute(builder: (_) => Media()));
}
I also had to add the Navigator route back to the previous page.
I know it's a bit of a hack but it works. Thank you all!

Related

Lottie Animation works only once on tap

I am using Lottie Animation and want it to animate everytime I click on it , To this I am using GestureDetector However it only works the first time then for some reason it wont work again
Here is the code
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
void main() async {
runApp(const App());
}
class App extends StatefulWidget {
const App({super.key});
#override
State<App> createState() {
return _AppState();
}
}
class _AppState extends State<App> with SingleTickerProviderStateMixin {
late final AnimationController my_location_controller;
#override
void initState() {
// TODO: implement initState
super.initState();
my_location_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 5));
}
#override
Widget build(BuildContext context) {
return MaterialApp(
color: Colors.lightBlue,
home: Scaffold(
backgroundColor: Colors.lightBlue,
body: Center(
child: SizedBox(
width: 300,
height: 300,
child: GestureDetector(
onTap: () {
my_location_controller.forward();
},
child: Lottie.asset(
'assets/my_location.json',
controller: my_location_controller,
animate: true,
repeat: true,
),
),
),
),
),
);
}
}
#Ante Bule thnx, will accept your answer and this seems to work too ..
child: GestureDetector(
onTap: () {
my_location_controller.reset();
my_location_controller.forward();
},
child: Lottie.asset(
'assets/my_location.json',
controller: my_location_controller,
),
Add a listener to reset your animation when it gets completed, like this:
#override
void initState() {
super.initState();
my_location_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 5));
my_location_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
my_location_controller.reset();
}
});
}

Flutter Navigate back to home Screen after a time when there is no interaction on the second Screen

Flutter beginner needs help.
Is it possible that the app by himself without a press of a button, navigates from a second screen to the home screen after a period of time, if there is no any interaction on the second Screen.
For example in the following code is it possible to make this?
I don't know how to implement this.
Thanks in advance for some help
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
);
}
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Navigate to a new screen on Button click'),
backgroundColor: Colors.teal),
body: Center(
child: FlatButton(
color: Colors.teal,
textColor: Colors.white,
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context)=>Screen2()));
},
child: Text('GO TO SCREEN 2'),
),
),
);
}
}
class Screen2 extends StatefulWidget {
#override
_Screen2State createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Navigate to a new screen on Button click'),
backgroundColor: Colors.blueAccent),
body: Center(
child: FlatButton(
color: Colors.blueAccent,
textColor: Colors.white,
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context)=>Home()));
},
child: Text('GO TO HOME'),
),
),
);
}
}
First, I would recommend pop and not push because Flutter navigator works like a stack.
Next, you can set a timer in the initState function:
#override
void initState() {
Timer(Duration(seconds: 3), () {
Navigator.of(context).pop(MaterialPageRoute(builder: (context) => Home()));
});
super.initState();
}
for a better solution, I would set a timer in the initState function and wrap the entire page with GestureDetector, so that each gesture resets the timer.
Use GestureDetector's behavior property and pass HitTestBehavior.opaque to it, which recognizes entire screen and detects the tap when you tap anywhere on the screen. like this:
import 'dart:async';
import 'package:flutter/material.dart';
class Screen2 extends StatefulWidget {
#override
_Screen2State createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
Timer? searchOnStoppedTyping;
_onChangeHandler() {
const duration = Duration(milliseconds: 800); // set the duration that you want call pop() after that.
if (searchOnStoppedTyping != null) {
searchOnStoppedTyping?.cancel(); // clear timer
}
searchOnStoppedTyping = new Timer(duration, () => navigateHome());
}
navigateHome() {
Navigator.of(context).pop(MaterialPageRoute(builder: (context) => Home()));
}
#override
void initState() {
_onChangeHandler();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Navigate to a new screen on Button click'), backgroundColor: Colors.blueAccent),
body: GestureDetector(
//Use GestureDetector's behavior property and pass HitTestBehavior.opaque to it, which recognizes entire screen and detects the tap when you tap anywhere on the screen.
behavior: HitTestBehavior.opaque,
onTap: _onChangeHandler,
child: yourBody...,
),
);
}
}
class Screen2 extends StatefulWidget {
#override
_Screen2State createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
// try to add init state and add timer inside it then it will navigate after duration
void initState() {
super.initState();
Timer(Duration(seconds: 4), (){
Navigator.of(context).pop(MaterialPageRoute(builder: (context)=>Home()));
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Navigate to a new screen on Button click'),
backgroundColor: Colors.blueAccent),
body: Center(
child: FlatButton(
color: Colors.blueAccent,
textColor: Colors.white,
onPressed: () {
// i moved this line inside the Timer , look at initState
},
child: Text('GO TO HOME'),
),
),
);
}
}

Background music in Flutter does not work

I'm trying to add background music to my app. if i start my app the music starts rightly, but if i press a button which should have no impact to the music, the music starts from new. i code in Flutter.Here is my code i cutted the unimportant things away.
import 'package:audioplayers/audio_cache.dart';
import 'package:audioplayers/audioplayers.dart';
class _MyHomepageState extends State<MyHomepage> {
AudioPlayer player = AudioPlayer();
AudioCache cache = new AudioCache();
bool isPlaying = false;
Future<bool> _willPopCallback() async {
if (isPlaying == false) {
setState(() {
isPlaying = true;
});
player.stop();
}
return true;
}
openingActions() async {
player = await cache.loop('audio/test.mp3');
}
#override
Widget build(BuildContext context) {
openingActions();
return WillPopScope(
onWillPop: () => _willPopCallback(),
child: Scaffold(
body: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/background.jpg'),
fit: BoxFit.cover,
),
),
...
...
...
raisedbutton(
....
)
You can copy paste run full code below
You can move openingActions(); from build to initState
And rebuild will not call openingActions(); again
#override
void initState() {
openingActions();
super.initState();
}
#override
Widget build(BuildContext context) {
//openingActions(); //delete this line and move to initState
full code
import 'package:flutter/material.dart';
import 'package:audioplayers/audio_cache.dart';
import 'package:audioplayers/audioplayers.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
AudioPlayer player = AudioPlayer();
AudioCache cache = new AudioCache();
bool isPlaying = false;
Future<bool> _willPopCallback() async {
if (isPlaying == false) {
setState(() {
isPlaying = true;
});
player.stop();
}
return true;
}
openingActions() async {
player = await cache.loop('audio/test.mp3');
}
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
void initState() {
openingActions();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: Text('Open route'),
onPressed: () {
setState(() {});
},
),
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

How to get the page is not disposed

I have application which has mappage using location
class _MapPageState extends State<MapPage> {
LocationData currentLocation;
Location _locationService = new Location();
#override
void initState(){
super.initState();
_locationService.onLocationChanged().listen((LocationData result) async {
setState(() {
print(result.latitude);
print(result.longitude);
currentLocation = result;
});
});
}
In this case, setState() works well when mappage is shown.
However after mappage is disposed, there comes error like this.
E/flutter ( 6596): This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.
E/flutter ( 6596): The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
So, I have two ideas.
Remove onLocationChanged() listener when page is disposed.
Check if State is disposed or not before setState()
How can I solve this??
You can copy paste two files below and directly replace official example's code
https://github.com/Lyokone/flutterlocation/tree/master/location/example/lib
After Navigate to ListenLocationWidget page,
you can call _stopListen() in dispose()
code snippet
class _MyHomePageState
...
RaisedButton(
child: Text('Open route'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => ListenLocationWidget()),
);
},
),
PermissionStatusWidget(),
Divider(height: 32),
ServiceEnabledWidget(),
Divider(height: 32),
GetLocationWidget(),
Divider(height: 32),
//ListenLocationWidget()
class _ListenLocationState extends State<ListenLocationWidget> {
...
StreamSubscription<LocationData> _locationSubscription;
String _error;
#override
void initState() {
print("initState");
super.initState();
_listenLocation();
}
#override
void dispose() {
print("stopListen");
_stopListen();
super.dispose();
}
Future<void> _listenLocation() async {
_locationSubscription =
location.onLocationChanged.handleError((dynamic err) {
setState(() {
_error = err.code;
});
_locationSubscription.cancel();
}).listen((LocationData currentLocation) {
setState(() {
print("setState");
_error = null;
_location = currentLocation;
});
});
}
Future<void> _stopListen() async {
_locationSubscription.cancel();
}
working demo
full code ListenLocationWidget
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:location/location.dart';
class ListenLocationWidget extends StatefulWidget {
const ListenLocationWidget({Key key}) : super(key: key);
#override
_ListenLocationState createState() => _ListenLocationState();
}
class _ListenLocationState extends State<ListenLocationWidget> {
final Location location = Location();
LocationData _location;
StreamSubscription<LocationData> _locationSubscription;
String _error;
#override
void initState() {
print("initState");
super.initState();
_listenLocation();
}
#override
void dispose() {
print("stopListen");
_stopListen();
super.dispose();
}
Future<void> _listenLocation() async {
_locationSubscription =
location.onLocationChanged.handleError((dynamic err) {
setState(() {
_error = err.code;
});
_locationSubscription.cancel();
}).listen((LocationData currentLocation) {
setState(() {
print("setState");
_error = null;
_location = currentLocation;
});
});
}
Future<void> _stopListen() async {
_locationSubscription.cancel();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Container(
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Listen location: ' + (_error ?? '${_location ?? "unknown"}'),
style: Theme.of(context).textTheme.body2,
),
Row(
children: <Widget>[
Container(
margin: const EdgeInsets.only(right: 42),
child: RaisedButton(
child: const Text('Listen'),
onPressed: _listenLocation,
),
),
RaisedButton(
child: const Text('Stop'),
onPressed: _stopListen,
)
],
),
],
),
),
);
}
}
full code main.dart
import 'package:flutter/material.dart';
import 'package:location/location.dart';
import 'package:url_launcher/url_launcher.dart';
import 'get_location.dart';
import 'listen_location.dart';
import 'permission_status.dart';
import 'service_enabled.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Location',
theme: ThemeData(
primarySwatch: Colors.amber,
),
home: const MyHomePage(title: 'Flutter Location Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final Location location = Location();
Future<void> _showInfoDialog() {
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Demo Application'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
const Text('Created by Guillaume Bernos'),
InkWell(
child: Text(
'https://github.com/Lyokone/flutterlocation',
style: TextStyle(
decoration: TextDecoration.underline,
),
),
onTap: () =>
launch('https://github.com/Lyokone/flutterlocation'),
),
],
),
),
actions: <Widget>[
FlatButton(
child: const Text('Ok'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.info_outline),
onPressed: _showInfoDialog,
)
],
),
body: Container(
padding: const EdgeInsets.all(32),
child: Column(
children: <Widget>[
RaisedButton(
child: Text('Open route'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => ListenLocationWidget()),
);
},
),
PermissionStatusWidget(),
Divider(height: 32),
ServiceEnabledWidget(),
Divider(height: 32),
GetLocationWidget(),
Divider(height: 32),
//ListenLocationWidget()
],
),
),
);
}
}

How to close a specific Flutter AlertDialog?

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.