How to avoid jank (laggy animation) during page transition in Flutter - 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()

Related

FLUTTER : Displaying a loading indicator while listview.builder is building

When clicking on an OPEN LIST button, the app creates a series of lists needed for future display. I found out that when the number of items in the list increases, these calculations can take a little time (2, 3 seconds). So for better UX, I would like to add something similar to a loading indicator telling the user the "lists are being prepared".
In my app, I use the package Loading Indicator : it works fine.
So I wanted to use it for this situation.
Here's what I did :
I transformed my "void" list creating functions into "Future Void".
I added the async keyword to the function plugged to my "OPEN LIST BUTTON".
But... for some reason, it never displays the loading indicator....
Here's the code (UI part) :
onMenuOuvrir: () async {
DialogBuilder(context).showLoadingIndicator(
text: 'Ouverture de la liste', color: Colors.black45);
uD.setSelectedCarnetList(
index, uD.userInfo!.carnetList![index].ref!);
await uD.getListReady();
DialogBuilder(context).hideOpenDialog();
Navigator.pushReplacementNamed(
context, EditCarnetScreen.id);
},
Here's the code (Provider / back end part) :
Future<void> getListReady() async {
await createBufferCarnetWordBank();
await createBufferCarnetList();
await createBufferGrammarList();
await createBufferLevelList();
setEditMode(false);
clearSearchList();
}
The functions "createBuffer...List" are all of type Future .
What am I doing wrong ?
I actually found out that the problem was elsewhere... not in the creation of the lists, but in the building of the "listview.builder" in the EditCarnetScreen.
So here's the question now... how can we display some kind of indicator while this task is being processed.... it seems to be "in between screens"...
Create a widget in a separate dart file :
import 'package:flutter/material.dart';
Loading(BuildContext context) {
return showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
);
});
}
Then use it anywhere in your code:
Loading(context)
Dismiss in whenever you want:
Navigator.of(context).pop()
You can use FutureBuilder widget for that, e.g.
FutureBuilder(future: Provider.of<//your provider>(context)getListReady(), builder: (context, snapshot) => snapshot.connectionState == ConnectionState.waiting ? Center(child: CircularProgressIndicator()) : //your widget)
If you process the calculation in the main thread, even if the indicator shows up the UI freezes and the spinning animation would be laggy.
You may process the heavy lifting tasks by Isolates. Search for Isolate or compute method of dart.
I have used the fluttertoast: ^8.0.8 package as shown below. You can set the toast length to a longer duration if needed. FutureBuilder is not useful in the case, where your widget themselves take a long time render. FutureBuilder is useful while fetching the data required for your widgets.
#override
Widget build(BuildContext context) {
Fluttertoast.showToast(
msg: 'Loading...'
);
//Code for your complex widget here
}

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

Flutter loading Image asset too slow

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.

Flutter app freezes when a TextField or TextFormField is selected

I have a Flutter app that is functioning properly in all respects except when I select a TextField (or TextFormField). When I select the TextField, the cursor blinks in the TextField, but I can't type anything AND all other buttons like the floatingActionButton and the back button in the AppBar quit working. Essentially, the app appears to be frozen, but I don't get any error messages.
After numerous attempts to fix the problem in two different pages that contain FocusNodes and TextEditingControllers, I went back to square one by incorporating a new page with code straight from Flutter's website, but the TextField in this barebones code still locks up the app.
import 'package:flutter/material.dart';
class EventDetailForm extends StatefulWidget {
static const String routeName = "/events/event-detail-form";
#override
_EventDetailFormState createState() => _EventDetailFormState();
}
class _EventDetailFormState extends State<EventDetailForm> {
final myController = TextEditingController();
#override
void dispose() {
myController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Event Detail')),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: myController,
)),
floatingActionButton: FloatingActionButton(
onPressed: () {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text(myController.text),
);
});
},
child: Icon(Icons.text_fields),
),
);
}
}
Unfortunately, I am not getting any error messages. The cursor just blinks in the TextField and everything else loses function and I have to quit and restart. I am not sure what else I should be considering. Does anyone have any ideas on what might be causing this?
Simulator -> Device -> Erase All Content And Settings works for me.
Had same problem when I upgraded Xcode to ios 13.1. I switched to a different simulator, and the problem went away.
This maybe late, but it happened to me too just today. I also changed the channel to beta but unfortunately did not work too. Apparently what worked for me is when I restarted the simulator after I put back the channel to stable.
I had the same bug, solved by switching to the beta channel of Flutter.
In your terminal use
flutter channel beta
flutter upgrade
About channels you can read here https://github.com/flutter/flutter/wiki/Flutter-build-release-channels
I did not change channel, a simple flutter upgrade was enough to fix this problem. I also closed Android Studio and all simulators and when I restarted, the problem was gone.
I think I am late to the party but the issue still exists in 2021.
I tried all the solutions but couldn't fix it. Whatever I was typing in TextField or TextFormField or autocomplete_textfield, the characters were not visible.
I fixed it by opening the Widget as a showGeneralDialog() instead of using Navigator.of(...). Here is the sample code.
await showGeneralDialog(
barrierColor: AppStyle.primaryColor.withOpacity(0.3),
transitionBuilder: (context, a1, a2, widget) {
return Transform.scale(
scale: a1.value,
child: Opacity(opacity: a1.value, child: WidgetScreenToOpen()),
);
},
transitionDuration: Duration(milliseconds: 500),
barrierDismissible: true,
barrierLabel: 'Label',
context: context,
pageBuilder: (context, animation1, animation2) {
return Container();
}).then((result) {
return result;
});

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