So I have this button that navigates to a new view:
RaisedButton(
child: const Text('Advertise device', style: TextStyle(fontSize: 20)),
onPressed: () async {
Nearby().askExternalStoragePermission();
var navigationResult = await Navigator.push(context, MaterialPageRoute(builder: (context) => EndpointList(endpointList)));
endpointList = navigationResult;
Nearby().stopAdvertising();
print("stopped advertising");
}
),
The class implementing the view is declared with with WidgetsBindingObserver so that I can observe the view:
#override
void initState(){
WidgetsBinding.instance.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
When navigating to the view I want to call _advertiseDevice();. For some reason (that suits me), the AppLifecycleState.resumed gets toggled, so instead of calling _advertiseDevice(); in the initState() it gets called here:
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if(state == AppLifecycleState.detached || state == AppLifecycleState.inactive || state == AppLifecycleState.paused){
print("stopped advertising");
Nearby().stopAdvertising();
}else if (state == AppLifecycleState.resumed){
print("advertising because state resumed");
_advertiseDevice();
}
}
This is nice. View opens, and the _advertiseDevice(); does his thing.
Now, I did the same with another view. Also called via a button:
RaisedButton(
child: const Text('Discover devices', style: TextStyle(fontSize: 20)),
onPressed: () async {
await Nearby().askLocationPermission();
var navigationResult = await Navigator.push(context, MaterialPageRoute(builder: (context) => AdvertiserList(advertisingList)));
advertisingList = navigationResult;
Nearby().stopDiscovery();
print("stopped discovering");
},
),
and also declared with with WidgetsBindingObserver to observe the view:
#override
void initState(){
WidgetsBinding.instance.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
with a smilar didChangeAppLifecycleState:
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if(state == AppLifecycleState.detached || state == AppLifecycleState.inactive || state == AppLifecycleState.paused){
print("stopped discovery");
Nearby().stopDiscovery();
}else if (state == AppLifecycleState.resumed){
print("discovering because state resumed");
_discoverDevices();
}
}
Here however, AppLifecycleState.resumed is not called when navigating to the view, like in the previous view. Yes, I can put the _discoverDevices(); in the initState() and it would work just fine (the warning of "already discovering" that pops is irrelevant to me), but I'd like to know why.
Why two classes that implement the same WidgetsBindingObserver logic behave differently?
PS: I'm not adding the code for both classes because I'm still working on it and it's a mess and because (correct me if I'm mistaken), the actions performed by the view is irrelevant for this. This is related to the focus and unfocus of a view, and the actions taken accordingly.
EDIT: Adding the flow for both screens.
The app opens and displays a tab view. Within the first tab there are a couple buttons, one is the one that displays the first screen. Displaying it triggers AppLifecycleState.resumed and the _advertiseDevice(); method is called. (Why? No idea, the app lifecycle didn't change).
The app opens and displays a tab view. Select the second tab. Within the second tab there are a couple buttons, one is the one that displays the second screen. Displaying it does not trigger anything, unlike the first screen. No idea why the difference in behaviour.
Related
I am creating an alarm clock application, and I want to show a full screen page to allow the user to dismiss the alarm when it triggers. Thats all working well but the issue arises when I want to close that page.
What I have tried
Currently, when the alarm triggers, I am pushing that page onto the navigation stack to make it visible:
App.navigatorKey.currentState?.pushNamedAndRemoveUntil(
alarmNotificationRoute,
(route) {
return (route.settings.name != '/alarm-notification') ||
route.isFirst;
},
);
And then pop it when user presses "Dismiss":
if (App.navigatorKey.currentState?.canPop() ?? false) {
App.navigatorKey.currentState?.pop();
}
My App routing code:
class App extends StatefulWidget {
const App({super.key});
static final GlobalKey<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
#override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
#override
Widget build(BuildContext context) {
return MaterialApp(
...
navigatorKey: App.navigatorKey,
initialRoute: '/',
onGenerateRoute: (settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (context) => const RootScreen());
case '/alarm-notification':
return MaterialPageRoute(
builder: (context) {
return AlarmNotificationScreen();
},
);
default:
assert(false, 'Page ${settings.name} not found');
return null;
}
},
);
}
}
Current behavior
Now when I pop, it returns to the default route of the flutter app '/', even when the alarm triggered while the app was closed.
Expected behavior
The behavior I want is as follows:
If the app was in the foreground when alarm triggered, pressing dismiss should go back to the last screen (this is already working as expected)
If the app was in the background or closed when alarm triggered, pressing dismiss should send the app to background
If android decides to show a Heads Up Notification instead of a full page intent. pressing dismiss should do nothing
Thoughts
I am thinking that the cleanest way to do so would be to launch a standalone page/activity, which we can just close when we press dismiss. Is there anyway to do such a thing? I am fine with it being an android-only solution.
There appears to be a minimize_app package that does the "close to background" behavior you want. From there it's simply a matter of tracking where the page was navigated from and using conditional logic.
A possible implementation:
import 'package:minimize_app/minimize_app.dart';
...
// Set this variable when the app is opened via the alarm trigger
if (appWasInBackground) {
MinimizeApp().minimizeApp();
} else if (App.navigatorKey.currentState?.canPop() ?? false) {
App.navigatorKey.currentState?.pop();
}
How Can we get the lifecycle of StatefulWidget in flutter, if should pause if user is navigating to differnt screen and resume if user comes back to the screen.
didChangeAppLifecycleState giving lifecycle of app, not of the particular screen widget
Try FocusDetector, which is the closest to viewWillAppear/Disappear methods as seen on iOS.
FocusDetector(
onFocusLost: () {
logger.i(
'Focus Lost.'
'\nTriggered when either [onVisibilityLost] or [onForegroundLost] '
'is called.'
'\nEquivalent to onPause() on Android or viewDidDisappear() on iOS.',
);
},
onFocusGained: () {
logger.i(
'Focus Gained.'
'\nTriggered when either [onVisibilityGained] or [onForegroundGained] '
'is called.'
'\nEquivalent to onResume() on Android or viewDidAppear() on iOS.',
);
},
onVisibilityLost: () {
logger.i(
'Visibility Lost.'
'\nIt means the widget is no longer visible within your app.',
);
},
onVisibilityGained: () {
logger.i(
'Visibility Gained.'
'\nIt means the widget is now visible within your app.',
);
},
onForegroundLost: () {
logger.i(
'Foreground Lost.'
'\nIt means, for example, that the user sent your app to the background by opening '
'another app or turned off the device\'s screen while your '
'widget was visible.',
);
},
onForegroundGained: () {
logger.i(
'Foreground Gained.'
'\nIt means, for example, that the user switched back to your app or turned the '
'device\'s screen back on while your widget was visible.',
);
},
child: Container(),
);
I am using Flutter_bloc package to make a phone auth in flutte, everything work good, but my question is about adding events to the bloc, for example in my application, when i click on button like this code below, the event added to my loginBloc, and everything works good, but when i press back button in android device, and then return back by using normal navigater.pushNamed, and click the button again nothing happen? that mean the event not added to bloc or something like this? can anybody explain this problem? thanks in advance: this is my sample code to add event when click button:
child: RaisedButton(
onPressed: () {
if (_formKey.currentState.validate()) {
loginBloc.add(LoginPressesEvent(
phoNo: _phoneTextController.value.text));
}
},
For adding an 'Event' to 'Bloc' use this code:
BlocProvider.of<'YourBlocClass'>('blocContext').add('YourEvent()'));
'blocContext' is context parameter of `listener in BlocListener' :
BlocProvider(
create: (context) => BlocClass()..add(Fetch()),
child: BlocListener<BlocClass, BaseState>(
listener: (listenerContext, state) {
// listenerContext: store this parameter to Field
// and use that everywhere in your StateClass
},
or context parameter of 'builder in Bloc Builder`
BlocProvider(
create: (context) => BlocClass()..add(Fetch()),
child: BlocBuilder<IndexBloc, BaseState>(
builder: (builderContext, state) {
// builderContext: store this parameter to Field
// and use that everywhere in your StateClass
},
I'm experiencing the following issue: My Flutter app uses a GoogleMap. The map loads just fine initially. However, if I put the app into the background and resume a while later, the map stays blank. The Google logo still shows, like it happens when the API key isn't specified. My polygon overlay doesn't show up, either.
The behavior is not reliably repruducable. Sometimes, the map loads fine after the app had been in the background for hours, sometimes the map is blank after minutes. So far, I have only seen this behavior on Android.
There are no specific log outputs that indicate an error.
Any ideas how to fix/work around this?
I filed an issue with screenshot here: https://github.com/flutter/flutter/issues/40284
EDIT 1:
I was able to reproduce this with a GoogleMap as root widget and also without any polygon/feature overlay. Also, I found that wildly zooming in at some point 'reanimates' the map (suddenly the map becomes visible again). Is this maybe a known issue with the underlying Android Google Maps SDK?
EDIT 2:
I found that the map is still reacting (e.g. tap/gesture listeners still trigger). Also, the map isn't really empty, it just becomes translucent, so the screen displays whatever widget is behind the map.
I discovered that if you tap a marker or change the style the map re-renders
class TheWidgetThatHasTheMap with WidgetsBindingObserver {
//...your code
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
controller.setMapStyle("[]");
}
}
}
Not a solution to the core problem, but I was able to work around this bug by creating a fork of the plugins project and modifying GoogleMapController.java as follows:
#Override
public void onActivityResumed(Activity activity) {
if (disposed || activity.hashCode() != registrarActivityHashCode) {
return;
}
mapView.onResume();
// Workaround for https://github.com/flutter/flutter/issues/40284
// This apparently forces a re-render of the map.
if (googleMap != null) {
googleMap.setMapType(googleMap.getMapType());
}
}
Now, on every resume event, the map will be re-rendered.
I tried something & it seems to be working!
Step 01,
Implement WidgetsBindingObserver for related class's State class as follows,
i.e:
class MainScreenState extends State<MainScreen> with WidgetsBindingObserver {....
Step 02,
Override didChangeAppLifecycleState method
i.e:
#override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.inactive:
print('appLifeCycleState inactive');
break;
case AppLifecycleState.resumed:
print('appLifeCycleState resumed');
break;
case AppLifecycleState.paused:
print('appLifeCycleState paused');
break;
case AppLifecycleState.detached:
print('appLifeCycleState detached');
break;
}
}
Step 03
add this for init state
WidgetsBinding.instance!.addObserver(this);
Step 04
Step 4 should be as follows
//onMapCreated method
void onMapCreated(GoogleMapController controller) {
controller.setMapStyle(Utils.mapStyles);
_controller.complete(controller);
}
// lifecycle
#override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.inactive:
print('appLifeCycleState inactive');
break;
case AppLifecycleState.resumed:
**//Add These lines**
final GoogleMapController controller = await _controller.future;
onMapCreated(controller);
print('appLifeCycleState resumed');
break;
case AppLifecycleState.paused:
print('appLifeCycleState paused');
break;
case AppLifecycleState.detached:
print('appLifeCycleState detached');
break;
}
}
When dealing with a stateful widget, put the code below in your code as shown below
class MainScreenState extends State<MainScreen> with WidgetsBindingObserver
{....
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addObserver(this);
... // Other codes here
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
mapController!.setMapStyle("[]");
}
}
}
Then you can add the code below in the state widget
Another simpler way to implement solution with setMapStyle within statefull widget of map widget. No need to change anything else:
Import flutter services:
import 'package:flutter/services.dart';
Code:
SystemChannels.lifecycle.setMessageHandler((msg) {
if (msg == AppLifecycleState.resumed.toString()) {
mapController.setMapStyle("[]");
}
});
"mapController" here is the instance of Google map controller you named somewhere in your code. In my case it is like this:
GoogleMapController _mapController;
GoogleMapController get mapController => _mapController;
if you facing this problem in 2022 also add this line above your class
class YourClass extends StatefulWidget with WidgetsBindingObserver
Completer<GoogleMapController> controller = Completer();
#override
void dispose() {
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) async {
super.didChangeAppLifecycleState(state);
print('\n\ndidChangeAppLifecycleState');
if (state == AppLifecycleState.resumed) {
final GoogleMapController controller1 = await controller.future;
controller1.setMapStyle('[]');
}
}
void initState() {
super.initState();
WidgetsBinding.instance!.addObserver(this);
}
Another temporary fix that doesn't required forking the plugins, building, etc.
Add a didChangeAppLifecycleState implemented via WidgetsBindingObserver to your Widget and make the GoogleMap widget rebuild with a state change.
In my case the map threw a black screen when called setState, the below solution solved my problem.
return SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child:SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: //Your GoogleMap here,
),
);
I'm building an app that basically is a YouTube clone. I use the official video_player plugin for playback and chewie for controls. I'd like to implement a quality switcher, so the user can decide what quality they want the video to be streamed at
I've built a bottom sheet with switches and I run changeQuality() when the user selects the desired quality. What it should do is simply giving a new source file to the old player and keep playing from where the video left.
This is the video player and chewie player that run on initState():
videoPlayer = VideoPlayerController.network(data == null
? dataAll[indexNo]["video"]["480"]
: data[indexNo]["video"]["480"]);
chewieController = ChewieController(
videoPlayerController: videoPlayer,
aspectRatio: 16 / 9,
autoPlay: true,
allowedScreenSleep: false,
placeholder: data == null
? Image(
image: NetworkImage(dataAll[indexNo]["thumbnail"]),
)
: Image(
image: NetworkImage(data[indexNo]["thumbnail"]),
)
);
And the changeQuality() function:
changeQuality(String newQuality) {
setState(() {
position = videoPlayer.value.position;
chewieController.pause();
videoPlayer = new VideoPlayerController.network(data == null
? dataAll[indexNo]["video"]["$newQuality"]
: data[indexNo]["video"]["$newQuality"]);
chewieController = ChewieController(
videoPlayerController: videoPlayer,
aspectRatio: 16 / 9,
autoPlay: true,
allowedScreenSleep: false,
startAt: position,
);
});
Navigator.of(context).pop();
}
I've also tried disposing the old video player and then setting the new value, but I get an error that variables cannot be used after being disposed.
The switcher works a bit, because it changes the quality around 4 to 5 times and then it runs into an error and won't play anything.
I expand upon this solution for video_player and extend it to also cover chewie.
Key parts of this solution
You need two widgets. MyVideoPlayer that encapsulates video_player and chewie and an outer widget where you react to user input or state changes and swap out MyVideoPlayer with a new one.
This solution roundabouts the whole question in one way. I doesn't solve how to change video of video_player or chewie. Instead it follows the documented principal on how to use chewie for the whole life cycle of a host widget (MyVideoPlayer) and swap that one out to change video url.
You can stuff in more things in the outer widget as you see fit if you don't want to dedicate it just to containing MyVideoPlayer. Ie. if you want a description text adjacent to it based on app state.
Outer Widget
I write with this. but it can be omitted in Dart code.
class QuizVideoPlayer extends StatefulWidget {
#override
_QuizVideoPlayerState createState() => _QuizVideoPlayerState();
}
class _QuizVideoPlayerState extends State<QuizVideoPlayer> {
Word _url;
UniqueKey _urlKey;
// Call this method from button or in reaction to model change etc.
// I call it from Provider.of in didChangeDependencies, but I don't think it is
// a necessary detail of the answer as it depends on how you do state management.
// The key in this solution is that state management occur in the outer widget and
// due to some trigger call _changeUrl() which changes _url and _urlKey which then
// swaps out MyVideoPlayer.
#override
void _changeUrl(String newUrl) async {
this.setState(() {
// Rebuild MyVideoPlayer with a new instance => eventually dispose old controllers
this._url = newUrl;
this._urlKey = UniqueKey();
});
}
#override
Widget build(BuildContext context) {
return
/* ... */
this._url != null
? MyVideoPlayer(
this._url,
this._urlKey,
)
: AspectRatio(
aspectRatio: 3 / 2,
child: Container(color: Colors.black),
)
/* ... */
);
}
}
MyVideoPlayer
I write with this. but it can be omitted in Dart code.
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart';
class MyVideoPlayer extends StatefulWidget {
final String videoUrl;
final UniqueKey newKey;
MyVideoPlayer(this.videoUrl, this.newKey): super(key: newKey); // passing Unique key to dispose old class instance and create new with new data
#override
_MyVideoPlayerState createState() => _MyVideoPlayerState();
}
class _MyVideoPlayerState extends State<MyVideoPlayer> {
VideoPlayerController _controller;
ChewieController _chewie;
#override
void initState() {
this._initControllers(this.widget.videoUrl);
super.initState();
}
void _initControllers(String url) {
this._controller = VideoPlayerController.network(url);
this._chewie = ChewieController(
videoPlayerController: this._controller,
autoPlay: true,
);
}
#override
void dispose() {
this._controller?.dispose();
this._chewie?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Chewie(controller: this._chewie);
}
}