Skip to content
Flutter Software Agency

Lottie Splash Screen intro in Flutter

This tutorial goes over how we can use Lottie to show a Splash screen animation on startup.

Posted on:August 30, 2020
7 minutes

This tutorial will be building off the Custom StartUp functionality. This is logic that we show, usually while showing a view that looks exactly like the splash screen with a loading indicator on it. In this tutorial we will show an animated version of the splash screen that will loop until the initialisation logic is complete, or the animation is played from beginning to end.

What is Lottie and why Lottie

Lottie is an animation tool / runtime designed by airbnb that allows you to export after effect animations as json to playback in any app. If you know me or my website you know that I use rive, previously known as flare for my animations given how easy it is to make the actual animations to use. In this case my client has provided me with animations that they would like to use in their app. Their animators use after effects so it’s easier to export them as Lottie animations than have them rebuild their animations in rive just to please me. Even though they probably would ;)

Setup

We’ll start off by adding the Lottie package into our pubspec.

lottie: ^0.6.0

Then we will make use of the splash screen animations. Now I obviously can’t give you the files that I’m using since they belong to the client, but below you can see a preview of the animation. To get your own file head over to Loffiefiles where you can get thousands of free lottie animations. Download 1 that’s short and loopable and then you can get started.

Lottie Splash screen animation

When you have your file, create a new folder in root called assets with a folder inside called lottie. Place your file in there. For me the path to the file is assets/lottie/login.json. Then you can open the pubspec.yaml file again and add the asset path under the assets section.

assets:
  - assets/lottie/login.json

That’s all the setup required for the package and the animation. Onto the code.

Usage

The usage of a Lottie animation is easy. This is all you need.

Lottie.asset('assets/lottie/login.json')

That will load the animation you have added and play it. Unfortunately we can’t use that. The first reason is, the client wants the full intro animation to play whenever the app opens. And the second reason comes from us, we can’t navigate away from the StartUpView until the initialisation logic is complete. If we do the app will be broken, that’s where we get the user information, setup the database, register for push notifications etc. It never takes very long but in some cases, like where you need to fetch information, like language strings, it might take some time given network connectivity or size of the payload. So because of that we need to play this using an animation controller. Open up your startup view. it will probably look something like this.

class StartupView extends StatelessWidget {
  const StartupView({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ViewModelBuilder<StartupViewModel>.reactive(
      builder: (context, model, child) => Scaffold(
        backgroundColor: kcPrimaryColor,
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              // Image
              Image.asset(
                'assets/images/logos/startup.png',
                height: screenHeightFraction(context, dividedBy: 4),
              ),
              verticalSpaceMedium,
              // Loading indicator
              CircularProgressIndicator(
                valueColor: AlwaysStoppedAnimation(Colors.white),
              )
            ],
          ),
        ),
      ),
      onModelReady: (model) => SchedulerBinding.instance
          .addPostFrameCallback((_) => model.initialise()),
      viewModelBuilder: () => StartupViewModel(),
    );
  }
}

Nothing special. Just an image with a loading indicator underneath it that spins until the initialise function is complete. What we need from the lottie animation is to know when it has completed it’s first playback.

Lottie with Animation Controller

to achieve that we’ll need an animation controller. I prefer to use flutter_hooks for animation because I don’t have to convert to a stateful widget, keep track of the controller, dispose the controller and provide tickers etc. So you can add flutter_hooks to your pubspec and then change the StartupView to a HookWidget. You can also create a new final variable called animationController and use the userAnimationController function to get a new animation controller.

class StartupView extends HookWidget {
  const StartupView({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final animationController = useAnimationController();
    ...
  }
}

Then we can setup the Lottie widget to load the animation and then start playing it when ready. So instead of using the Lottie animation like shown above you’ll still load it with the asset constructor but this time provide a controller an an onLoaded function.

// Inside build function. Refer to full source above
...
Scaffold(
    backgroundColor: kcPrimaryColor,
    body: Center(
      child: ,
    ),
  )

This will start the app, when the splash screen is removed it will play the Lottie animation in the centre of the screen. The next thing we need to do is make sure we know when the animation is complete. This is quite easy given we have the animationController at our disposal. We can listen to the status of that controller and call a function our ViewModel based on that. Add this code into the onLoaded function seen above.

...
onLoaded: (composition) {
  animationController.addStatusListener((status) {
    if (status == AnimationStatus.completed) {
      model.indicateAnimationComplete();
    }
  });

  // Configure the AnimationController with the duration of the
  // Lottie file and start the animation.
  animationController
    ..duration = composition.duration
    ..forward();
},
...

Then you can open up the ViewModel and add the function indicateAnimationComplete.

class StartupViewModel extends BaseViewModel {
  ...
  bool _animationComplete = false;
  ...

  void indicateAnimationComplete() {
    _animationComplete = true;
  }
}

Delay the startup logic

If you look at the function that runs the startup logic in the full ViewModel code below.

class StartupViewModel extends BaseViewModel {
  final log = getLogger('StartupViewModel');
  final _sharedPreferencesService = locator<SharedPreferencesService>();
  final _navigationService = locator<NavigationService>();
  final _database = locator<DDDatabase>();
  final _driftChatService = locator<DriftChatService>();
  final _permissionsService = locator<PermissionsService>();
  final _userService = locator<UserService>();

  bool _animationComplete = false;

  Future initialise() async {
    await locator<RemoteConfigService>().initialise();
    await _driftChatService
        .setupDrift(FlavorConfig.instance.values.driftChatId);

    var hasUser = _sharedPreferencesService.hasUser;
    var isGuestUser = _sharedPreferencesService.guestMode;
    log.v('hasUser:$hasUser');

    if (hasUser || isGuestUser) {
      // Database initialised first because user service will insert the address
      // into the database
      await _database.initialise();
      await _userService.initialise();

      if (!isGuestUser) {
        await _driftChatService.registerUser(
          userId: _userService.currentUser.id.toString(),
          email: _userService.currentUser.email,
        );
      }

      if (_userService.currentAddress != null) {
        await _navigationService.replaceWith(Routes.homeView);
      } else {
        var hasPermission = await _permissionsService.hasLocationPermission;
        if (!hasPermission) {
          await _permissionsService.requestLocationPermission();
        }

        await _navigationService.replaceWith(
          Routes.addressSelectionView,
          arguments: AddressSelectionViewArguments(
            showDefaultUi: false,
            firstSignIn: true,
            localOnly: isGuestUser,
          ),
        );
      }
    } else {
      await _navigationService.replaceWith(Routes.welcomeView);
    }
  }

  void indicateAnimationComplete() {
    _animationComplete = true;
  }
}

You can see here that we have 3 possible places to navigate to and all of them make use of the replaceWith function on the NavigationService. This is the only call that we need to delay. There are a few ways to implement this but we’ll choose a straight forward approach to keep the implementation short. We’ll create a new function called _replaceWith that will take in the route to go to and arguments if any. It’s basically a copy of the original replace with function but instead will store the route and arguments it was called with and will only navigate if the _animationComplete value is true.

Future _replaceWith({String route, dynamic arguments}) async {
    var hasDestinationRoute = _destinationRoute != null;
    var hasDestinationArguments = _destinationArguments != null;

    // Set the route only if we don't have a route
    if (!hasDestinationRoute) {
      _destinationRoute = route;
    }

    // set the arguments only if we don't have arguments
    if (!hasDestinationArguments) {
      _destinationArguments = arguments;
    }

    // navigate only if the animation is complete
    if (_animationComplete && _destinationRoute != null) {
      await _navigationService.replaceWith(
        _destinationRoute,
        arguments: _destinationArguments,
      );
    }
  }

Now we can replace all the replaceWith calls on the NavigationService with this function above.

// replace home view navigation
await _replaceWith(route: Routes.homeView);
...

// replace address selection navigation
await _replaceWith(
  route: Routes.addressSelectionView,
  arguments: AddressSelectionViewArguments(
    showDefaultUi: false,
    firstSignIn: true,
    localOnly: isGuestUser,
  ),
);
...

// replace welcome view navigation
await _replaceWith(route: Routes.welcomeView);

The final thing to do is to update the indicateAnimationComplete function to return a Future and await the replaceWith call with no parameters.

Future indicateAnimationComplete() async {
  _animationComplete = true;
  await _replaceWith();
}

This is the logic that will be followed for the navigation delay until animation is complete and business logic is complete. Lets go over the 2 scenarios.

  1. Business logic completes first (most likely in every scenario):
  1. Animation completes first:

That’s it.

If you like my expertise

Also check out