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

4

10

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.

Pineapple answered 9/11, 2021 at 1:33 Comment(2)
I'd just like to throw in that you can't always trust the emulator/simulator, make sure you try it on a real device. I've seen simple image sliders wreak havoc with the sim/emu on powerful machines but then runs flawlessly on real but weak devices. (Of course, this point is moot if you're already doing that.) – Bluefish
Thank you for mentioning this. it's very important to test on real device and I've been doing that as well and still getting the jank experience. – Pineapple
A
5

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

Aili answered 16/6, 2022 at 13:22 Comment(3)
Ok... My answer is primarily about the shader-jank, though, but... that link seems useful, too! πŸ™‚ In fact, I should add it at the end of my answer. – Circumnavigate
I don't fully understand how the warmup function called from initState is using the context that's not supposed to exist yet to push pages, but I can't argue with the results. Good trick. – Peradventure
Oh, initState() has access to the context! "The framework associates [State] objects with a [BuildContext] after creating them with [StatefulWidget.createState] and before calling [initState]", as is stated in framework.dart. However, I just checked and I do get an error if I remove the await Firebase.initializeApp(); stated just before. πŸ™‚ The error says something about not being ready to push new screens while still building the first one... but after initializing the Firebase app, it's ready to push on! (Guess you can add a Future.delayed() or something if not using Firebase.) – Circumnavigate
V
1

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

Verdie answered 2/11, 2022 at 7:47 Comment(3)
Thanks for sharing the package you created. Can you share a bit more what's happening under the hood? Is there a trade-off for using this approach? – Pineapple
@SamRamezanli Sure, here is how it is implemented -> cjycode.com/flutter_smooth/design – Verdie
And it is fully open source so also feel free to look at source code – Verdie
D
0

Try adding a small delay before loading the initial tasks in page B. Maybe with a Future.delayed()

Dionnedionysia answered 9/11, 2021 at 4:23 Comment(2)
Tried this already but didn't help. Thanks for the response Kaushik – Pineapple
can you post codes here please? like how you load page B and what's in initstate of page B – Dionnedionysia
F
0

What is the top level widget you're returning from Page B's build()?

Mine was a FutureBuilder, which was building an Scaffold. I just swapped them (Scaffold at top) and problem was gone.

Footlambert answered 6/6, 2023 at 0:59 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.