Flutter loading Image asset too slow - flutter

So, I have a screen which shows a text and an image above the text and loads, in background, some data from the shared preferences.
But the image is taking forever to load, and when I remove the call to get the shared preferences data the image loads very quiclky. Do someone knows if I'm doing something wrong with this call?
here's the code:
class WelcomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
precacheImage(AssetImage(Images.tomato), context);
_loadData(context);
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
Strings.appName,
style: TextStyle(
color: AppColors.black,
fontSize: 50.0,
fontWeight: FontWeight.w600,
),
),
Image.asset(Images.tomato),
],
),
),
);
}
void _loadData(BuildContext context) async {
final PreferencesRepository repository = PreferenceRepositoryImp();
repository.loadAll().then(
(_) {
sleep(Duration(seconds: 2));
Navigator.pushNamedAndRemoveUntil(context, Routes.HOME, (_) => false);
},
);
}
}
and that's the image

You're using sleep(Duration(seconds: 2)); which halts everything. You should not be using sleep, especially inside a build method. Dart is single threaded so that means when it halts a thread, it prevents the UI from being built.
Also, why is _loadData marked as async if you're not using the await keyword? You only need it if you're going to use the await keyword, otherwise, remove it. If you want to make use of it, you should use await Future.delayed(Duration(seconds: 2)) which makes it wait. Here:
void _loadData(BuildContext context) async {
final PreferencesRepository repository = PreferenceRepositoryImp();
await repository.loadAll();
await Future.delayed(Duration(seconds: 2));
Navigator.pushNamedAndRemoveUntil(context, Routes.HOME, (_) => false);
}
How does await differ from sleep?
await makes Dart say "Hey, I'm going to keep my eye out for this in the future" (haha get it? Future? hahahhahaha I'm funny). Anyways, it won't continue the _loadData function because it wants that value but it will keep running (in your example) the build method and then when the Future comes back with a value, it will be like "Hey! There's that value I was looking for! Now I can keep running that function!"
sleep on the other hand makes Dart say "I'm going to take a nap for the duration of time I get" (in this case two seconds). It won't do anything until it "wakes up" since Dart is single-threaded. After it takes its two-second nap, it will continue the function and after it finishes, the build method will carry on, therefore, loading your image which explains why it takes so long.

Related

How to avoid jank (laggy animation) during page transition in Flutter

I have two pages, Page A and Page B.
To do transition from Page A to Page B I use Navigation.push():
Navigator.push(
context,
CupertinoPageRoute(...)
);
However, this transition has so much jank and frame drops. (yes, I run in Profile mode)
One reason I think of is Page B has so much heavy-duty UI rendering (such as Google Maps and Charts) and I also noticed as page slide animation is happening, Page B rendering has already begun.
I'm trying to understand how I can improve this experience and maybe somehow pre-load Page B.
I already read this suggestion from a Github issue (tldr use Future.microtask(()) but it didn't work for me. Would appreciate any help or suggestion.
If the screen transition animation is especially janky the first time you run the app, but then gets smoother if you run the transition back and forth a few times, this is a known problem that both the Flutter team and their counterparts within Android and iOS are working on. They have a suggestion for a workaround here: https://docs.flutter.dev/perf/shader , built on "warming up shaders", but since it only warms up the shaders on the specific device you're debugging on, I honestly feel it's like the programming equivalent of "sweeping it under the rug"... You won't see the problem on your device anymore, but it's still there on other devices!
I however found a workaround for this problem myself, which works surprisingly well! I actually push the janky page, wait a bit, and then pop it again, without the user knowing it! 🙂 Like this:
import 'login_screen.dart';
import 'register_screen.dart';
import 'home_screen.dart';
import 'loading_screen.dart';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
class WelcomeScreen extends StatefulWidget {
const WelcomeScreen(Key? key) : super(key: key);
#override
_WelcomeScreenState createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends State<WelcomeScreen> {
#override
void initState() {
super.initState();
warmUp();
}
Future warmUp() async {
// This silly function is needed to remove jank from the first run screen transition...
print('Running warmUp()');
await Firebase.initializeApp();
// If not using Firebase, you'll have to add some other delay here!
// Otherwise, you will get errors below for trying to push new screens
// while the first one is still building.
if (mounted) {
Navigator.push(context, MaterialPageRoute(builder: (context) => LoginScreen(popWhenDone: false)));
Navigator.push(context, MaterialPageRoute(builder: (context) => RegisterScreen(popWhenDone: false, userType: UserType.artist)));
Navigator.push(context, MaterialPageRoute(builder: (context) => HomeScreen()));
Navigator.push(context, MaterialPageRoute(builder: (context) => LoadingScreen())); // Shows a spinner
await Future.delayed(Duration(milliseconds: 1000));
if (mounted) {
Navigator.popUntil(context, (route) => route.isFirst);
}
}
}
#override
Widget build(BuildContext context) {
print('Building $runtimeType');
return Scaffold(
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
MaterialButton(
child: const Text('Sign Up'),
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return const RegisterScreen();
}));
},
),
MaterialButton(
child: const Text('Log In'),
onPressed: () async {
await Firebase.initializeApp(); // In case I remove the warmUp() later...
if (mounted) {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, a1, a2) {
return LoginScreen();
},
transitionsBuilder: (context, a1, a2, child) {
return child; // I want only a Hero animation between the screens, nothing else
},
transitionDuration: const Duration(milliseconds: 1000),
),
);
}
},
),
],
),
Expanded(
flex: 4,
child: Hero(tag: 'logoWText', child: Image(image: AssetImage(kImageLogoWText))),
),
],
),
),
);
}
}
This ads a second of waiting for the app to load, with a spinner showing, but I find that most times, the spinner barely has time to show anyway, and in any case, it is a much better user experience to wait a sec for the app to load than to experience jank during use!
Other tips
If your screen transitions are still janky, even if you have run them back and forth, then you probably need to trim the performance of the screens involved. Perhaps it is your build method that's too big, and has too many things going on in it? Maybe certain widgets get rebuilt many times during each build of the screen?
Check out these pieces of advice and see if it helps: https://docs.flutter.dev/perf/best-practices In any case, they should improve your app's performance in general. 🙂
Edit: And check out this link, as provided by ch271828n in a comment below: https://github.com/fzyzcjy/flutter_smooth
If you are having jank without the shader compilation problem, then there is indeed a (new) approach to make it ~60FPS smooth without changing your code (indeed, only need to add 6 characters - CupertinoPageRoute -> SmoothCupertinoPageRoute).
GitHub homepage: https://github.com/fzyzcjy/flutter_smooth
Indeed, I personally see such jank (i.e. jank not caused by shader compilation) a lot in my app, maybe because the new page is quite complicated.
Disclaimer: I wrote that package ;)
Try adding a small delay before loading the initial tasks in page B. Maybe with a Future.delayed()

Dispose provider in Widget Test

I'm performing some widget tests on my Flutter app with flutter_test.
It basically works fine except when my testing widget is a Consumer.
Here, I basically have a DeviceProvider objects that wraps an object into a ChangeNotifier so that updates are correctly propagated to consumers.
To be able to use it with a WidgetTester, I put my Consumer widget inside a ChangeNotifierProvider that intantiates a DeviceProvider.
_pumpTestableWidgetWithProvider(WidgetTester tester, Widget widget) async {
DeviceProvider device = DeviceProvider(Device());
await tester.pumpWidget(ChangeNotifierProvider(
create: (context) => device,
child: MaterialApp(home: widget)));
return device;
}
Then, I use the WidgetTester I have just pumped and check whether some text fields are in them.
testWidgets("Infos - default device infos", (WidgetTester tester) async {
DeviceProvider device = await _pumpTestableWidgetWithProvider(
tester, DeviceInfos());
expect(find.text("Battery state: "), findsOneWidget);
});
It returns with an error telling me that 'A Timer is still pending even after the widget tree was disposed'. Looking at the log, this timer corresponds to a _FakeTimer created with the DeviceProvider. I thus tried to manually dispose the provider by using
device.dispose();
But now, I have an error telling me that 'A DeviceProvider was used after being disposed.'
Does anyone have a solution for me ?
I was able to solve it using a different constructor for the changeNotifierProvider on the tests:
Widget buildWidget() {
return ChangeNotifierProvider.value(
value: locator<TaNaLeiProvider>(),
child: MaterialApp(
home: Scaffold(
body: SizedBox(
width: 640,
child: DrawerTaNaLei(),
),
),
),
);
}
Note that I'm using ChangeNotifierProvider.value instead of create.
I'm also using getIt to create singletons on my app.
locator.registerLazySingleton<TaNaLeiProvider>(() => TaNaLeiProvider());
Hope that helps!
Finally, I found the problem, I had an actual timer running inside my DeviceProvider and this timer was not canceled.
Thanks for your support !

Flutter: setState((){} fails to redraw DropdownButton value

I am new to flutter, and am trying to code mobile game. So far I have been able to manage through previous questions on forums, youtube tutorials and example apps. However I have hit a wall that I cannot solve.
Many of the features in my UI are supposed to change based on user behavior but do not update, I am using this DropdownButton as my example but it is a problem I am having elsewhere in the code as well. When the user makes a selection from the DropdownButton it should update the value of the button, but instead only the hint is displayed. I have been able to determine through print statements that the selection is registered.
Based on what I have learned so far I suspect that the issue entails my widget's statefulness or lack thereof. I found a few similar questions which suggested using a StatefulBuilder, however when I tried to implement it the code had errors or else duplicated my entire ListTile group. I also saw suggestions about creating a stateful widget instead but the instructions were too vague for me to follow and implement without errors. I am including snippets of the code below, I am hesitant to paste the whole thing because the app is nearly 2000 lines at this point. Please let me know if any further information or code is needed.
this is the problematic code, see below for the code in context.
DropdownButton(
value: skillChoice,
items: listDrop,
hint: Text("Choose Skill"),
onChanged: (value) {
setState(() {
skillChoice = value;
});
},
),
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hunters Guild',
theme: ThemeData(
primaryColor: Colors.grey,
),
home: Main(),
);
}
}
final List<Combatant> _combatants = List<Combatant>();
//combatant is a class that holds stats like HP which may change during a battle
//they also have a Fighter and a Monster and one of those will be null and the other useful
//depending if faction = "good" or "evil
//I am aware that this may not be the most elegant implementation but that is a problem for another day.
class MainState extends State<Main> {
final List<Fighter> _squad = List<Fighter>();
//Fighter is a class which stores info like HP, name, skills, etc for game character,
//this List is populated in the page previous to _fight
//where the user picks which characters they are using in a given battle
void _fight(Quest theQuest){
for(Fighter guy in _squad){
_combatants.add(Combatant("good", guy, null));
}
_combatants.add(Combatant("evil", null, theQuest.boss));
//quests are a class which store a boss Monster, later on I will add other things like rewards and prerequisites
final tiles = _combatants.map((Combatant comba){ //this structure is from the build your first app codelab
List<DropdownMenuItem<String>> listDrop = [];
String skillChoice = null;
if (comba.faction == "good"){
for (Skill skill in comba.guy.skills){
listDrop.add((DropdownMenuItem(child: Text(skill.name), value: skill.name)));
}
}
else if (comba.faction == "evil"){
for (Skill skill in comba.boss.skills){
listDrop.add((DropdownMenuItem(child: Text(skill.name), value: skill.name)));
}
}
//^this code populates each ListTile (one for each combatant) with a drop down button of all their combat skills
return ListTile(
leading: comba.port,
title: Text(comba.info),
onTap: () {
if(comba.faction == "good"){
_fighterFightInfo(comba.guy, theQuest.boss); //separate page with more detailed info, not finished
}
else{
_monsterFightInfo(theQuest.boss); //same but for monsters
}
},
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
DropdownButton( //HERE IS WHERE THE ERROR LIES
value: skillChoice,
items: listDrop,
hint: Text("Choose Skill"),
onChanged: (value) {
setState(() {
skillChoice = value;
});
},
),
IconButton(icon: Icon(Icons.check), onPressed: (){
//eventually this button will be used to execute the user's skill choice
})
],
),
subtitle: Text(comba.hp.toString()),
);
},
);
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
//I have tried moving most of the above code into here,
//and also implementing StatelessBuilders here and other places in the below code to no effect
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
return Scaffold(
appBar: AppBar(
title: Text("Slay "+theQuest.boss.name+" "+theQuest.boss.level.toString()+" "+theQuest.boss.type.name, style: TextStyle(fontSize: 14),),
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: () {
_squadSelect(theQuest);
}),
),
body: ListView(children: divided),
);
},
),
);
}
//there are a lot more methods here which I haven't included
#override
Widget build(BuildContext context) {
//I can show this if you need it but there's a lot of UI building on the main page
//Also might not be the most efficiently implemented
//what you need to know is that there are buttons which call _fight and go to that page
}
class Main extends StatefulWidget {
#override
MainState createState() => MainState();
}
Thank you for any help or advice you can offer.
Its a bit difficult to understand what is going on here, so if possible add the problem areas in a new widget and post it here.
In any case, for now it seems your skillShare variable is local, so every time you are setting state it does not update. Try something like this:
class MainState extends State<Main> {
String skillShare = ""; //i.e make skillShare a global variable
final List<Fighter> _squad = List<Fighter>();
The following question had information which helped me find the answer. Different question but the solution applies. Thanks user:4576996 Sven. Basically I need to reformat from doing my constructing in the void _fight to a new stateful page. This may be useful for anyone else who is using the beginning tutorial as a framework to build their app.
flutter delete item from listview

which widget can be used to explain functionality in app

I want to explain something in my app and add a widget which looks like a notification or chat. I want this widget to be visible for some time and then get dismissed. I tried using tooltip but it is visible only when I click it.
Which widget can I use?
The Dart package intro_views_flutter is what you need, but one of its main limitations is that it is displayed on full screen, if that is not an issue to you, then you should take a look at it. Or you can use a showDialog method inside a Future function this way :
Future showNotification() async {
showDialog<String>(
context: context,
child: new AlertDialog(
title: Text('Note!') ,
contentPadding: const EdgeInsets.all(16.0),
content: //any widget you want to display here
),
);
await new Future.delayed(const Duration(seconds: 5), () {
Navigator.of(context).pop(); // this will dismiss the dialog automatically after five seconds
}
}
then when you need it call:
showNotificaion();

Flutter setState() doesn't always call my build method

I'm trying out Flutter, but I'm having trouble getting the UI to update consistently. I'd like to show a status message while a long-running async method is called, but the setState() call I make just before calling the long-running method doesn't seem to cause my build() method to get invoked.
I've created a simple example that calculates the Fibonacci number for a randomly selected number between 25 and 30. In my sample code/app, hitting the "calc" button calls _calc(). _calc() picks a random number, sets a status message "Calculating Fib of $num..." tied to a text widget (_status) and updates it with setState(); then calls the async _fib() routine to calculate the number; then updates _status with the result using setState(). Additionally, the build() method prints the value of _status to the console, which can be used to see when build() is invoked.
In practice, when the button is pressed, the first status message does not appear either in the debug console, or on the UI. Doing a bit of experimentation, I added a pseudo sleep function that I call just prior to calling _fib(). This sometimes causes the first setState() call to work properly - invoking build(). The longer I make the sleep, the more often it works. (I'm using values from a few milliseconds up to a full second).
So my question are: What am I doing wrong? and What's the right way to do this? Using the pseudo sleep is obviously not the correct solution.
Other, probably not too relevant info: My dev environment is Android Studio 3.1.2 on a Win10 machine. Using Android SDK 27.0.3, with Flutter beta 0.3.2. My target device is the emulator for a pixel2 running Android 8.1. Also, sorry if my lack of 'new' keywords is off-putting, but from what I read in Dart 2 release notes, it's not usually necessary now.
import 'package:flutter/material.dart';
import "dart:async";
import "dart:math";
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Debug Toy',
home: MyWidget(),
);
}
}
class MyWidget extends StatefulWidget {
#override
MyWidgetState createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
String _status = "Initialized";
final rand = Random();
Future sleep1() async {
return new Future.delayed(const Duration(milliseconds: 100),() => "1");
}
Future<Null> _resetState() async {
setState(() { _status = "State Reset"; });
}
Future<Null> _calc() async {
// calculate something that takes a while
int num = 25 + rand.nextInt(5);
setState(() { _status = "Calculating Fib of $num..."; });
//await sleep1(); // without this, the status above does not appear
int fn = await _fib(num);
// update the display
setState(() { _status = "Fib($num) = $fn"; });
}
Future<int> _fib(int n) async {
if (n<=0) return 0;
if ((n==1) || (n==2)) return 1;
return await _fib(n-1) + await _fib(n-2);
}
#override
Widget build(BuildContext context) {
print("Build called with status: $_status");
return Scaffold(
appBar: AppBar(title: Text('Flutter Debug Toy')),
body: Column(
children: <Widget>[
Container(
child: Row(children: <Widget>[
RaisedButton( child: Text("Reset"), onPressed: _resetState, ),
RaisedButton( child: Text("Calc"), onPressed: _calc, )
]),
),
Text(_status),
],
),
);
}
}
Let's start by going to one extreme and rewriting fib as fibSync
int fibSync(int n) {
if (n <= 0) return 0;
if (n == 1 || n == 2) return 1;
return fibSync(n - 1) + fibSync(n - 2);
}
and calling that
Future<Null> _calc() async {
// calculate something that takes a while
int num = 25 + rand.nextInt(5);
setState(() {
_status = "Calculating Fib of $num...";
});
//await Future.delayed(Duration(milliseconds: 100));
int fn = fibSync(num);
// update the display
setState(() {
_status = "Fib($num) = $fn";
});
}
The first setState just marks the Widget as needing to be rebuilt and (without the 'sleep') continues straight into the calculation, never giving the framework the chance to rebuild the Widget, so the 'Calculating' message isn't displayed. The second setState is called after the calculation and once again (redundantly) marks the Widget as needing to be rebuilt.
So, the execution order is:
Set status to Calculating, mark Widget as dirty
Perform the synchronous calculation
Set status to Result, mark Widget as dirty (redundantly)
Framework finally gets chance to rebuild; build method is called
When we uncomment the 'sleep', the execution order changes to
Set status to Calculating, mark Widget as dirty
'Sleep', allowing the framework to call build
Perform the synchronous calculation
Set status to Result, mark Widget as dirty (again)
Framework calls build
(As an aside, note how the synchronous fib calculation is an order of magnitude faster because it doesn't have to do all the microtask scheduling.)
Let's re-consider the async calculation. What's the motivation of making it async? So that the UI remains responsive during the calculation? As you've seen, that doesn't achieve the desired effect. You still only have one thread of execution, and you aren't allowing any gaps in execution for callbacks and rendering to occur. Sleeping for 100ms is not compute bound, so drawing etc can occur.
We use async functions to wait for external events, like replies from web servers, where we don't have anything to do until the reply arrives, and we can use that time to keep rendering the display, reacting to gestures, etc.
For compute bound stuff, you need a second thread of execution which is achieved with an Isolate. An isolate has its own heap, so you have to pass it its data, it works away in its own space, then passes back some results. You can also stop it, if it's taking too long, or the user cancels, etc.
(There are much less computationally expensive ways to calculate fibs, but I guess we're using the recursive version as a good example of an O(n^2) function, right?)