David Adegoke Well known for his skills and dynamic leadership, David has led development teams building and deploying great products. He is passionate about helping people learn mobile development with Flutter and the leadership strategies they need to succeed regardless of their background. As he says, "You just have to be consistent and intentional to make it."

How to implement a shimmer effect in Flutter

7 min read 2140

Implementing a Shimmer Effect in Flutter

Introduction

From the moment the application starts to the very time the user exits the app, their experience determines whether or not they’ll return to the app, or interact with it further — so a good user experience really cannot be overemphasized. If there are glitches, unwanted delays, uneven flow, or any number of more frustrating issues, you can as well know you’ve lost a user that month.

One significant contribution to a great user experience is choosing and implementing loading indicators. Loading indicators and their effects build up a healthy anticipation (as long as it’s not too long) for your application’s content.

For instance, when a user logs in to the application, if there is no on-screen change after the user clicks the Login button, the user might assume that there is a glitch somewhere and may keep re-tapping the button. If relevant checks are not in place, the user may make too many requests and put the app under unneeded stress, so it might eventually crash.

That’s just one out of several use cases in which indicators can be a convenient tool. In this post, we’ll discuss how to implement shimmer effects, a special kind of loading indicator. Let’s dive in!

What is a shimmer effect?

Shimmer effects are loading indicators used when fetching data from a data source that can either be local or remote. It paints a view that may be similar to the actual data to be rendered on the screen when the data is available.

Example of a shimmer effect

Instead of the usual CircularProgressIndicator or LinearProgressIndicator, shimmer effects present a more aesthetically pleasing view to the user and in some cases helps build up some anticipation of the data before it’s rendered on the screen.

In the sample app we’ll build, we’ll fetch character data from a Rick and Morty API and display it in our app. While fetching the data, the shimmer effect will display. Let’s get to it.

Implementing a shimmer effect

Let’s start by creating a new Flutter project.

flutter create shimmer_effect_app

Import the following dependencies and dev dependencies we need in the app in our pubspec.yaml file:

We made a custom demo for .
No really. Click here to check it out.

  • http: To make a GET request to the Rick and Morty API to get the list of characters and their data
  • shimmer: To make the shimmer effect
  • stacked: The architectural solution we’ll use in this package
  • stacked_services: Ready-to-use services made available by the stacked package
  • build_runner: Gives access to run commands for auto-generating files from annotations
  • stacked_generator: For generating files from Stacked Annotations
dependencies:
 cupertino_icons: ^1.0.2
 flutter:
  sdk: flutter
 http: ^0.13.4
 shimmer: ^2.0.0
 stacked: ^2.2.7+1
dev_dependencies:
 flutter_lints: ^1.0.0
 flutter_test:
  sdk: flutter
 build_runner: ^2.1.5
 stacked_generator: ^0.5.6

Setting up utilities

In the lib directory, create a folder named utils. This folder will contain one file, our api_constants.dart file, which is the endpoint to get characters from the API. This file makes it easier to call the getCharacters endpoint across the entire codebase without having to declare it in every file. Ours is a tiny app, but it’s much better to have clean reusable code at all times.

class ApiConstants {
 static const scheme = 'https';
 static const host = 'rickandmortyapi.com';
 static get getCharacters =>
   Uri(host: host, scheme: scheme, path: '/api/character/');
}

Setting up the models

Next up is creating the model classes. We will create two model classes. The first one is the CharacterResponseModel, which gets the response from the endpoint and sorts it into info and the actual data. The second one is the CharacterModel, which will hold the details of each character.

We only need two pieces of information for each character in the app: the name and the species of each of the characters.

Below is the CharacterResponseModel:

class CharacterResponseModel {
 //The InfoModel class which holds additional information e.g total number of characters, next, previous pages etc
 Info? info;
 //The CharacterModel class which holds the actual data/results
 List<CharacterModel>? results;
 CharacterResponseModel({this.info, this.results});

 //The fromJson method, which takes the JSON response, checks if the results are not null and then loops through the values, creating a List of CharacterModels.
 CharacterResponseModel.fromJson(Map<String, dynamic> json) {
  info = json['info'] != null ? Info.fromJson(json['info']) : null;
  if (json['results'] != null) {
   results = [];
   json['results'].forEach((v) {
    results!.add(CharacterModel.fromJson(v));
   });
  }
 }
 //The toJson method which creates a map from the given CharacterModel details
 Map<String, dynamic> toJson() {
  final Map<String, dynamic> data = {};
  if (info != null) {
   data['info'] = info!.toJson();
  }
  if (results != null) {
   data['results'] = results!.map((v) => v.toJson()).toList();
  }
  return data;
 }
}

And here is the CharacterModel:

class CharacterModel {
 String? name;
 String? species;
 CharacterModel({
  this.name,
  this.species,
 });
 //The fromJson method which takes the JSON response and creates a CharacterModel from it
 CharacterModel.fromJson(Map<String, dynamic> json) {
  name = json['name'];
  species = json['species'];
 }
 Map<String, dynamic> toJson() {
  final Map<String, dynamic> data = {};
  data['name'] = name;
  data['species'] = species;
  return data;
 }
}

Lastly, we have the InfoModel:

//Handles general information on the response from the endpoint
class Info {
 int? count;
 int? pages;
 String? next;
 String? prev;
 Info({this.count, this.pages, this.next, this.prev});
 Info.fromJson(Map<String, dynamic> json) {
  count = json['count'];
  pages = json['pages'];
  next = json['next'];
  prev = json['prev'];
 }
 Map<String, dynamic> toJson() {
  final Map<String, dynamic> data = {};
  data['count'] = count;
  data['pages'] = pages;
  data['next'] = next;
  data['prev'] = prev;
  return data;
 }
}

Fetching character data

Next is to set up the service responsible for fetching the list of characters and their data. Let’s call it DashboardService. It will contain just one method, the getCharactersDetails() method.

Import the http package, the dart convert file (which grants us access to the json.decode and json.encode functions from dart, the character_model file and the api_constants file. Next is to create the getCharactersDetails method,

//Import the necessary packages
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shimmer_effect_app/models/character_model.dart';
import 'package:shimmer_effect_app/utils/api_constant.dart';

class DashboardService {
 //Creating the getCharacterDetails method
 Future<List<CharacterModel>?> getCharactersDetails() async {}
}

In the getCharactersDetails method, we call the getCharacters API endpoint using the HTTP package and fetch the data. This data is then passed to the CharacterResponseModel.fromJson() method, and then we return the result.

 Future<List<CharacterModel>?> getCharactersDetails() async {

  // Make the call to the getCharacters endpoint of the Rick and Morty's API
  final response = await http.get(ApiConstants.getCharacters);

  // pass the response to the fromJson method of the CharacterResponseModel and access the results data in it
  final charactersList =
    CharacterResponseModel.fromJson(json.decode(response.body)).results;

  // return the list of characters gotten from the CharacterResponseModel
  return charactersList;
 }

Setting up the UI

In the lib directory, create a folder named UI. Create a new folder named home in this folder and add two files: the home_view.dart and home_viewmodel.dart files.

We’ll perform a basic setup in the next steps and fully flesh them out a bit later.

In the home_viewmodel.dart file, create a new class named HomeViewModel. This class extends the BaseViewModel from the stacked package.

class HomeViewModel extends BaseViewModel{}

In the home_view.dart file, create a stateless widget and name it HomeView; this file will hold all UI-related code for the homepage. This widget returns the ViewModelBuilder.reactive() constructor from the stacked package that links/bind the view to its ViewModel.

class HomeView extends StatelessWidget {
 const HomeView({Key? key}) : super(key: key);
 @override
 Widget build(BuildContext context) {
  return ViewModelBuilder<HomeViewModel>.reactive(
    viewModelBuilder: () => HomeViewModel(),
    builder: (context, viewModel, child) {
     return Scaffold();
    });
 }
}

We won’t put anything here yet, as we need to set up a locator and register the dependencies we’ll be using in the ViewModel. Let’s move directly to that.

Setting up a locator

In the lib directory, create a new folder named app. In this folder, create a new file named app.dart. This is where we’ll register the views and services we’ll be using.

First, create a new class named AppSetup and annotate it with the @StackedApp annotation. It takes in two parameters, routes and dependencies. We pass the HomeView and DashboardService to the routes and dependencies, respectively.

import 'package:shimmer_effect_app/services/dashboard_services.dart';
import 'package:shimmer_effect_app/ui/home/home_view.dart';
import 'package:stacked/stacked_annotations.dart';

@StackedApp(
 routes: [
  AdaptiveRoute(page: HomeView, initial: true),
 ],
 dependencies: [
  LazySingleton(classType: DashboardService),
 ],
)
class AppSetup {}
>

Next, run the Flutter command to generate the files.

flutter pub run build_runner build --delete-conflicting-outputs  

This command generates the app.locator.dart and app.router.dart files, which will handle the dependencies and routing.

Finishing the HomeViewModel

Back to the HomeViewModel file, create a getCharacters method. This method will reach out to the dashboard and get the list of CharacterModels from it. We then assign this list to the charactersList, which was created initially as an empty list. We then make use of the locator to access the DashboardService.

class HomeViewModel extends BaseViewModel {

 // This gives us access to the DashboardService using the locator
 final _dashboardService = locator<DashboardService>();

 // This is the list, initially empty, but would contain the List of CharacterModels after the getCharacter function runs.
 List<CharacterModel>? charactersList = [];

 Future<void> getCharacters() async {
 // We assign the result from the call to the dashboardService to the charactersList which can then be displayed in the HomeView
 // The runBusyFuture here would set the entire ViewModel to a busy state until the call finishes, this is pretty handy as it helps us display the shimmer while the call to get the data is still ongoing
  charactersList =
    await runBusyFuture(_dashboardService.getCharactersDetails());
 }

}

With that, we have the ViewModel all set up and ready to go.

Finishing the HomeView

Next is fully setting up the view. We’ll create a base view for this app that displays the shimmer when the ViewModel is busy — i.e., fetching the data — and then displays a list of cards when it’s done and the data is ready.

The shimmer package gives us access to two constructors:

  • A direct Shimmer() constructor:
     Shimmer(
      // The gradient controls the colours of the Shimmer effect, which would be painted over the child widget
      gradient: gradient,
      child: child,
     )
  • Shimmer.fromColors():
    Shimmer.fromColors(
     // The baseColor and highlightColor creates a LinearGradient which would be painted over the child widget
      baseColor: baseColor,
      highlightColor: highlightColor,
      child: child,
     )

In our sample app, we will be using the Shimmer.fromColors() constructor. While the V``iewModel is busy fetching the data, we’ll display a card widget, over which the shimmer effect will be implemented. It’s a placeholder and has no children in it. When the ViewModel finishes loading, we’ll display a card of a similar size with the actual data.

Expanded(
// First we check if the ViewModel is busy (isBusy :) definitely) and display the Shimmer
  child: viewModel.isBusy
    ? Shimmer.fromColors(
      baseColor: Colors.grey[300]!,
      highlightColor: Colors.grey[100]!,
      child: ListView.builder(
       itemCount: 6,
       itemBuilder: (context, index) {
        return Card(
         elevation: 1.0,
         shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(16),
         ),
         child: const SizedBox(height: 80),
        );
       },
      ),
     )
    : ListView.builder(
      itemCount: viewModel.charactersList!.length,
      itemBuilder: (context, index) {
       return Card(
         elevation: 1.0,
         // This is just a little play with colors changing the colors everytime the app is refreshed or restarted :)
         color: Colors.primaries[Random()
               .nextInt(Colors.primaries.length)]
             .withOpacity(0.5),
         shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(16),
           ),
         child: Container()
       );
      },
     ),
 )

You can check out the complete code for the HomeView here.

Setting up the main.dart file

In the main.dart file, we will add the setupLocator function from the app.locator.dart generated file, a navigator key from the stacked_services package, and the onGenerate route from the app.router.dart file, ultimately linking the app from start to finish.

import 'package:flutter/material.dart';
import 'package:shimmer_effect_app/app/app.locator.dart';
import 'package:shimmer_effect_app/app/app.router.dart';
import 'package:stacked_services/stacked_services.dart';
void main() {
 WidgetsFlutterBinding.ensureInitialized();
 setupLocator();
 runApp(const MyApp());
}
class MyApp extends StatelessWidget {
 const MyApp({Key? key}) : super(key: key);
 @override
 Widget build(BuildContext context) {
  return MaterialApp(
   title: 'Material App',
   onGenerateRoute: StackedRouter().onGenerateRoute,
   navigatorKey: StackedService.navigatorKey,
  );
 }
}  

Our final app, after linking the main.dart file

Conclusion

Yes! We are done. We’ve successfully set up a shimmer effect over the application. Looks cool, yeah? Definitely! You’ve increased the overall user experience of your app. You can implement this in your applications, improving your app’s aesthetic look and feel.

Check out the complete source code for the sample app. If you have any questions or inquiries, feel free to reach out to me on Twitter: @Blazebrain or LinkedIn: @Blazebrain.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
David Adegoke Well known for his skills and dynamic leadership, David has led development teams building and deploying great products. He is passionate about helping people learn mobile development with Flutter and the leadership strategies they need to succeed regardless of their background. As he says, "You just have to be consistent and intentional to make it."

Leave a Reply