Flutter GoogleMap is blank after resuming from background - android

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,
),
);

Related

Flutter: `setState()` when screen off, or how to detect if screen is on?

TLDR: I have a problem that should be solved if there is answer to one of the below questions:
Is there a way I can detect if the screen is on e.g.
if (SCREEN IS ON) {
setState(() {}); // i.e. update displayed widgets
}
Is there a way to call setState() so that is finishes when the screen is off?
More details:
I have an app that plays audio. When the screen is on, the widgets update via setState() (e.g. to display the progress of the audio). Nonetheless it would be nice to let a user turn off their screen to save battery - however if I call setState() when the screen is off, it seems that the app waits for the screen to be turned back on before finishing the setState() code.
Hence I am hoping to determine if I can detect and only call setState() when the screen is on, alternatively can I call setState() when the screen is off so that is completes.
I use this for chatApp to track the app State. I wrap the MaterialApp. With some changes you could do what you want.
import 'package:flutter/material.dart';
import 'package:flutter_stripe/flutter_stripe.dart';
import 'constants/credentials.dart';
import 'domain/repositories/my_user_repository.dart';
class AppLifeCycleManager extends StatefulWidget {
final Widget child;
final MyUserRepository myUserRepo;
const AppLifeCycleManager(
{Key? key, required this.child, required this.myUserRepo})
: super(key: key);
#override
_AppLifeCycleManagerState createState() => _AppLifeCycleManagerState();
}
class _AppLifeCycleManagerState extends State<AppLifeCycleManager>
with WidgetsBindingObserver {
#override
void initState() {
super.initState();
widget.myUserRepo.setOnlineCustomer();
WidgetsBinding.instance.addObserver(this);
// set the publishable key for Stripe - this is mandatory
Stripe.publishableKey = pkTest;
}
#override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.paused:
widget.myUserRepo.setInactiveCustomer();
break;
case AppLifecycleState.resumed:
widget.myUserRepo.setOnlineCustomer();
break;
case AppLifecycleState.inactive:
widget.myUserRepo.setInactiveCustomer();
break;
case AppLifecycleState.detached:
widget.myUserRepo.setInactiveCustomer();
break;
}
}
#override
Widget build(BuildContext context) {
return widget.child;
}
#override
void dispose() {
widget.myUserRepo.setInactiveCustomer();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
mounted might be what you are looking for. Alternatively, you could check the AppLifecycleState by using WidgetsBindingObserver.
I haven't tested this, but screen state in the pub might be what you're looking for.
Big thanks to #mario-francois I got the following to work for me:
import 'package:flutter/material.dart';
class MyPage extends StatefulWidget {
MyPage({Key? key}) : super(key: key);
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> with WidgetsBindingObserver {
bool screenOn = true;
#override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.paused:// screen off/navigate away
// this.screenOn = false;
break;
case AppLifecycleState.resumed: // screen on/navigate to
this.screenOn = true;
break;
case AppLifecycleState.inactive: // screen off/navigate away
this.screenOn = false;
break;
case AppLifecycleState.detached:
break;
}
}
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
Widget build(BuildContext context) {
...
}
}

How can I update a stream from within a StreamBuilder?

Thank you very much in advance for your help! Please let me know if the question is not clear i would be happy to add more details if needed.
I have a Finite State Machine that handles some audio recognition. This FSM is wrapped by a "manager" whose job is to handle the state transitions (processState, nextState). The FSM manager exposes a stream which is updated every time nextState is called
FSM/Manager layout
class FSM_Manager{
StreamController<RecognitionState> _stateStream =
StreamController<RecognitionState>();
Sink<RecognitionState> get _inState => _stateStream.sink;
Stream<RecognitionState> get outState => _stateStream.stream;
RecognitionState _currentState, _previousState;
void setState(RecognitionState state) {
_previousState = _currentState;
_currentState = state;
_addCurrentStateToStream();
}
void _addCurrentStateToStream() {
_inState.add(_currentState);
}
Future nextState() async {
_currentState.nextState(this);
}
Future processState(itemToRecognize) async {
await _currentState.processState(itemToRecognize);
}
}
abstract class BaseState {
RecognitionStateID get stateID; //enum with each state's ID
Future processState(itemToRecognize);
Future nextState(FSM_Manager manager);
}
class FSM_State1 implements BaseState{
bool isSuccess = false;
void processState(itemToRecognize) async {
isSuccess = await performRecognition(itemToRecognize);
}
void nextState (FSM_Manager fsmManager) {
if(isSuccess){
// go to next State
fsmManager.setState(NEXT_STATE);
} else {
//go to some other state
fsmManager.setState(SOME_OTHER_STATE);
}
}
}
class FSM_State2 implements BaseState{
bool isSuccess = false;
void processState(itemToRecognize) async {
isSuccess = await performRecognition(itemToRecognize);
}
void nextState () {
if(isSuccess){
// go to next State
} else {
// go to another State
}
}
}
I have a screen (Stateful Widget) which uses a StreamBuidler to listen to the "outState" stream in order to rebuild the screen with the information in the new State.
Stateful Widget
class _RecognitionScreenState extends State<RecognitionScreen> {
ItemToRecognize item;
var currStateiD;
FSM_Manager _fsmManager;
RecognitionScreenState(
ItemToRecognize item, FSM_Manager fsmManager) {
this.item = item;
this._fsmManager = fsmManager;
}
#override
Widget build(BuildContext context) {
String outString = '';
return StreamBuilder<RecognitionState>(
stream: _stateContext.outState,
builder: (BuildContext context,
AsyncSnapshot<RecognitionState> snapshot) {
if (snapshot.hasData) {
outString = snapshot.data.stateID.toString();
return Text(outString);
} else {
return Text('');
}
});
}
}
Now, I do not know where/how to call processState and nextState from, I cant do it from the build method so i Tried to use initState() and didUpdateWidget in the StatefulWidget so that the states are processed in the beginning and after every build respectively. This approach didnt work, the nextState method was never called. I feel like im missing something trivial but i just dont see where to call those functions from outside of the Stateful Widget in order to trigger a rebuild only after the state has changed
Thanks again for your help
EDIT
I apologize for the confusion,
I added the BaseState definition (just an abstract class with some method so that i dont forget to implement them)
the nextState method takes an FSM_Manager as a parameter and calls setState on success or failure and sets the next state
the States "implement" the BaseState class, they dont "extend" it
It's difficult for me to tell from your code because I don't know what your BaseState does but inside your builder - presumably from some event or callback - you would do:
inState.add(<--Some RecognitionState-->);
This would trigger the StreamBuilder to rebuild.
If everything else in your code is put together properly.

When does AppLifecycleState toggle - flutter

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.

How to prevent admob from working on background

I'm using this(admob_flutter) package to add Admob ads on my flutter app, recently I got a notification from google play that my app got removed and they send the following statement in my email:
Ads associated with your app must not interfere with other apps, ads,
or the operation of the device, including system or device buttons and
ports. This includes overlays, companion functionality, and widgetized
ad units. Ads must only be displayed within the app serving them.
After some research by that unclear confusing stressful email, I got that happened because the Interstitial Ad could still keep working if the app not active or be in the foreground or even after closing the app by the back button, not terminated.
So I have searched for how to listen to those events on flutter, and I did found the didChangeAppLifecycleState but I couldn't figure out how to implement it with my ad!!
Here's my code: ( didn't implement the widget tree in here as I guess not important )
AdmobInterstitial interstitialAd;
dynamic _state;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
interstitialAd = AdmobInterstitial(
adUnitId: 'xxxxxxxxxxxxxx',
listener: (AdmobAdEvent event, Map<String, dynamic> args) {
if (event == AdmobAdEvent.loaded) interstitialAd.show();
if (event == AdmobAdEvent.closed) interstitialAd.dispose();
if (event == AdmobAdEvent.failedToLoad) {
print("Error code: ${args['errorCode']}");
}
},
);
interstitialAd.load();
}
#override
void dispose() {
interstitialAd.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
setState(() {
_state = state;
});
switch(state){
case AppLifecycleState.paused:
print('paused state');
break;
case AppLifecycleState.resumed:
print('resumed state');
break;
case AppLifecycleState.inactive:
print('inactive state');
break;
default:
break;
}
}
Update the interstitialAd initialization code block with the following snippet:
interstitialAd = AdmobInterstitial(
adUnitId: 'xxxxxxxxxxxxxx',
listener: (AdmobAdEvent event, Map<String, dynamic> args) {
if (event == AdmobAdEvent.loaded) {
If (_state != AppLifecycleState.paused) {
interstitialAd.show();
}
}
if (event == AdmobAdEvent.closed) interstitialAd.dispose();
if (event == AdmobAdEvent.failedToLoad) {
print("Error code: ${args['errorCode']}");
}
},
);

How to properly change video player source at runtime?

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);
}
}

Categories

Resources