I recently ported my flutter app from admob_flutter to google_mobile_ads. I've successfully implemented banner ads according to the tutorial here. The ads appear as expected however they cause the rest of the app to wait for them to load, as if something is being awaited. Here is a visual of the problem:
no banner (expected behaviour)
with banner (buggy)
The delay is even worse when I want to load pages with more than a couple widgets. At first I thought this was because the app is being run on an emulator, however the phenomenon persists on physical phones with updated OSes as well. I found this issue and thought it might be related however using the suggested package didn't work, and again the problem occurs on updated versions of android as well ( > 10). Here is my Ad class:
class AnchoredAdaptiveBanner extends StatefulWidget {
#override
_AnchoredAdaptiveBannerState createState() => _AnchoredAdaptiveBannerState();
}
class _AnchoredAdaptiveBannerState extends State<AnchoredAdaptiveBanner> {
BannerAd _anchoredAdaptiveAd;
bool _isLoaded = false;
#override
void didChangeDependencies() {
super.didChangeDependencies();
_loadAd();
}
Future<void> _loadAd() async {
// This was originally dynamic with an await, I changed it to static values for a sanity check, again same problem.
final AnchoredAdaptiveBannerAdSize size = AnchoredAdaptiveBannerAdSize(Orientation.portrait, width: 300, height: 50);
_anchoredAdaptiveAd = BannerAd(
adUnitId: AdManager.bannerAdUnitId,
size: size,
request: AdRequest(),
listener: BannerAdListener(
onAdLoaded: (Ad ad) {
setState(() {
_anchoredAdaptiveAd = ad as BannerAd;
_isLoaded = true;
});
},
onAdFailedToLoad: (Ad ad, LoadAdError error) {
ad.dispose();
},
),
);
return _anchoredAdaptiveAd.load();
}
#override
Widget build(BuildContext context) {
if (_anchoredAdaptiveAd != null && _isLoaded)
return Container(
width: _anchoredAdaptiveAd.size.width.toDouble(),
height: _anchoredAdaptiveAd.size.height.toDouble(),
child: AdWidget(ad: _anchoredAdaptiveAd),
);
else
return Container();
}
#override
void dispose() {
super.dispose();
_anchoredAdaptiveAd?.dispose();
}
}
My usage in my home widget for example:
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Container(
child: SafeArea(
child: Scaffold(
body: Builder(...),
floatingActionButton: Container(...),
bottomNavigationBar: AnchoredAdaptiveBanner()
),
),
);
}
}
As you can see nothing is being top-level awaited. I am currently on version 2.0.1 of google_mobile_ads.
Any idea what is causing this? Any help is appreciated!
The solution: Return SizedBox.shrink()
Why does this work?
Until the ad is loaded you're returning an empty container from your build method. If you don't pass height or width to the container, it expands as much as possible. In this case bottomNavigationBars can expand infinitely. This causes the rest of the widgets to be pushed out of screen and the page seems frozen until the ad loads. Using SizedBox.shrink() will have the container be 0x0 until it loads.
Related
I have a stateful widget called AuthenticatingScreen where I'm trying to perform the following flow...
Output message letting the user know we are logging them in
Get user oAuth token (calls to service file)
Update the message to let the user know we are loading their details
Fetch the users details and redirect them away
The problem is that at step three, I'm rebuilding the state, which is in turn causing the build method to be fired again and calling the service again, which triggers an exception.
import 'package:flutter/material.dart';
import 'package:testing/services/auth_service.dart';
class AuthenticatingScreen extends StatefulWidget {
final String token;
AuthenticatingScreen(this.token);
#override
State<AuthenticatingScreen> createState() => _AuthenticatingScreenState();
}
class _AuthenticatingScreenState extends State<AuthenticatingScreen> {
// step 1) our default message
String _message = 'Please wait while we log you in...';
Future<void> _fetchUserDetails() {
return Future.delayed(const Duration(seconds: 3), () {
// ToDo: fetch user details from the server
});
}
#override
Widget build(BuildContext context) {
// step 2) get our oAuth token
AuthService.handleCallback(widget.token).then((accessCode) async {
// step 3) update our message
setState(() => _message = 'We\'re just getting your details');
// step 4) retrieve our user details and redirect away
_fetchUserDetails().then((_) {
Navigator.of(context).pushNamedAndRemoveUntil(
'/home',
(Route<dynamic> route) => false,
);
});
});
/// output our authenticating screen.
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.only(bottom: 20.0),
child: CircularProgressIndicator(),
),
Text(_message),
],
),
),
);
}
}
My question being: How can I work around this / extract this logic to only fire when the widget is created, while still having access to the build context for navigation?
I've tried making the widget itself stateless and extracting the message and spinner into a separate widget, but changing the input argument alone still doesn't force a rebuild.
you can do it this way, i usually use getx & controller to achieve this.
separate the UI class & service class preferably in a controller
make the UI class statefull
call the API in onInit() method,as it called only once it will trigger the
service class
in API method when you get the result 200, initiate the UI transition
Ok, so I have figured out the solution. It seems making service calls within the build() method is a bad idea.
Moving my service calls into a void function which can then be called within the initState() method seems to be the way to go.
import 'package:flutter/material.dart';
import 'package:testing/screens/home.dart';
import 'package:testing/services/auth_service.dart';
class AuthenticatingScreen extends StatefulWidget {
final String token;
AuthenticatingScreen(this.token);
#override
State<AuthenticatingScreen> createState() => _AuthenticatingScreenState();
}
class _AuthenticatingScreenState extends State<AuthenticatingScreen> {
/// the default message to display to the user.
String _message = 'Please wait while we log you in...';
void _authenticateUser(String token) {
AuthService.handleCallback(widget.token).then((accessCode) async {
// we've got the users token, now we need to fetch the user details
setState(() => _message = 'We\'re just getting your details');
// after fetching the user details, push them to the home screen
_fetchUserDetails().then((_) {
Navigator.of(context).pushNamedAndRemoveUntil(
HomeScreen.name,
(Route<dynamic> route) => false,
);
});
});
}
Future<void> _fetchUserDetails() {
return Future.delayed(const Duration(seconds: 3), () {
// ToDo: fetch user details from the server
});
}
#override
void initState() {
super.initState();
_authenticateUser(widget.token);
}
#override
Widget build(BuildContext context) {
/// output our authenticating screen.
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.only(bottom: 20.0),
child: CircularProgressIndicator(),
),
Text(_message),
],
),
),
);
}
}
And this way when the build() method is called again for the rebuild, very little details have to be redrawn.
I am trying to display 3 images one after another by using GestureDetector and its onTap method but one image should be displayed without tapping. Supposing I have three images 1.png, 2.png, 3.png I would like to display the first image 1.png at the first place. Clicking on it will result in displaying the second image 2.png.
The second image 2.png should now be replaced after x seconds by displaying the image 3.png.
I am using a stateful widget that stores which image is currently shown. For the first image change the onTap will execute setState( () {screen = "2.png"} and the widget is rebuilt. However, I am pretty stuck now when the third image should be displayed after x seconds delay because there should not be any user interaction and I am not sure where to change it. I tried a lot of stuff especially editting the state from outside the widget but I did not succeed til now.
This is the code I am using:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Navigation Basics',
home: MyScreen(),
));
}
class MyScreen extends StatefulWidget {
#override
_MyScreenState createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
String currentScreen = 'assets/images/1.png';
#override
Widget build(BuildContext context) {
return GestureDetector(
child: FittedBox(
child: Container(
child: Image.asset(currentScreen),
),
fit: BoxFit.fill,
),
onTap: () {
if (currentScreen == 'assets/images/1.png') {
setState(() {
currentScreen = 'assets/images/2.png';
});
}
},
);
}
}
Displaying the second image worked without any issues but I have no idea what/where to code in order to display the third image.
Thanks for any help!
I'm assuming you don't want the second and third picture to respond to the gesture detection. Try this, please.
onTap: () {
if (currentScreen == 'assets/images/1.png') {
setState(() {
currentScreen = 'assets/images/2.png';
});
Future.delayed(const Duration(milliseconds: 3000), () {
setState(() {
currentScreen = 'assets/images/3.png';
});
});
} else {
return;
}
},
I have a first screen which ask the user to enter to input, then when the users clicks on a button, the app goes on a second screen which uses a FutureBuilder to call an API.
If the API returns an error, I would like to go back to the previous screen with Navigator.pop. When I try to do that in the builder of the FutureBuilder, I get an error because I modify the tree while I am building it...
setState() or markNeedsBuild() called during build. This Overlay
widget cannot be marked as needing to build because the framework is
already in the process of building widgets
What is the proper way to go to the previous screen if an error occur?
class Stackoverflow extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FutureBuilder<Flight>(
future: fetchData(context),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ScreenBody(snapshot.data);
} else if (snapshot.hasError) {
Navigator.pop(context, "an error");
}
// By default, show a loading spinner.
return CircularProgressIndicator();
},
)
),
);
}
}
PS: I tried to use addPostFrameCallback and use the Navigator.pop inside, but for some unknown reason, it is called multiple times
You can not directly navigate when build method is running, so it better to show some error screen and give use chance to go back to last screen.
However if you want to do so then you can use following statement to do so.
Future.microtask(() => Navigator.pop(context));
I'd prefer to convert class into StateFullWidget and get rid of FutureBuilder
class Stackoverflow extends StatefulWidget {
#override
_StackoverflowState createState() => _StackoverflowState();
}
class _StackoverflowState extends State<Stackoverflow> {
Flight flight;
#override
void initState() {
super.initState();
fetchData().then((data) {
setState(() {
flight = data;
});
}).catchError((e) {
Navigator.pop(context, "an error");
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: flight != null ? ScreenBody(flight) : CircularProgressIndicator(),
),
);
}
}
and of cause pass context somewhere outside class is not good approach
I would like to find out in a stateful widget when the whole widget appears on screen or disappears, similar to iOS onViewWillAppear / disappear. Is that possible somehow? I didn't find anything related in the Flutter docs.
Thanks!
What your looking for is in the flutter_widgets package
Add the following to your pubspec.yaml
flutter_widgets: ^0.1.7+1
Inside this package is a widget called VisibilityDetector it requires a key, a child, and a function onVisibilityChanged
return VisibilityDetector(
key: Key("1"),
onVisibilityChanged: (visibility) {
//This will give you a range of values between 0 and 1,
//0 being not visible and 1 being fully visible.
print(visibility.visibleFraction)
}
child: Container(
width:double.infinity,
height: 300,
),
),
If you are thinking to perform something after widget build, You should use below code:
void initState() {
super.initState();
if (SchedulerBinding.instance.schedulerPhase ==
SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((_) => onWidgetBuild());
}
/// appear
void onWidgetBuild() {
/// This block will be called onWidgetBuild
/// do your code here
}
/// disappear
#override
void dispose() {
super.dispose();
/// release whatever you have consume
}
Hope this will helps you.
I've had good luck using the focus_detector package. A FocusDetector widget will need to be placed within the default build() function and your existing UI code made it's child:
#override
Widget build(BuildContext context) =>
FocusDetector(
onFocusLost: () {
},
onFocusGained: () {
},
onVisibilityLost: () {
},
onVisibilityGained: () {
},
onForegroundLost: () {
},
onForegroundGained: () {
},
// Your previous build code replaces the Container() below
child: Container(),
);
I am a newbie to flutter. I am developing an app that contains a splash screen widget then the home screen will appear. I just want to check if there is any internet connection or not, if yes then it will go to the home screen otherwise it will close. I have checked the internet connection programmatically. It's ok, but without any internet connection, it goes to the home screen. Please help in this matter. Thanks in advance.
class Splashscreen extends StatefulWidget {
#override
State<StatefulWidget> createState() => _Splashscreenmain();
}
class _Splashscreenmain extends State<Splashscreen>{
Helperfunction helperfunction = new Helperfunction();
#override
void initState() {
super.initState();
startSplashScreen();
}
startSplashScreen() async {
var duration = const Duration(seconds: 10);
if (helperfunction.internetConnection() != "No Internet"){
return Timer(duration, () {
Navigator.of(context).pushReplacement(
new MaterialPageRoute(builder: (BuildContext context) {
return MyApp();
}));
});
}
else {
print(helperfunction.internetConnection());
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
}
}
#override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
body: Container(
child: Image.asset('assets/images/splashscreen.png',fit: BoxFit.cover,
height: double.infinity,
width: double.infinity,
alignment: Alignment.center,),
),
);
}
}
I have printed internetConnection() function result but the 'else' part is not executed.
You want to close app if no internet connection.
You can do this with SystemNavigator.pop(). does not work for ios as in IOS an app should not exit itself.I would suggest you show the user a dialog to enable internet connectivty.
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
from docs .
Instructs the system navigator to remove this activity from the stack and return to the previous activity.
On iOS, calls to this method are ignored because Apple's human interface guidelines state that applications should not exit themselves.
This method should be preferred over calling dart:io's exit method, as the latter may cause the underlying platform to act as if the application had crashed.