Logo
Published on

Good practices when loading SVG's with flutter_svg

Authors
  • avatar
    Name
    Emanoel Oliveira
    Twitter

Introduction

In the mobile context, using SVG images is very common and sometimes they are loaded directly from the application's own assets. But in other situations, we need to get them from remote servers and in this case it is necessary to carry out some processing on the interface.

In this post, I will show you some useful tips for dealing with loading SVG's in Flutter.

To more faithfully exemplify some of the real challenges, let's create a small example application.

Setting up the structure

First create a new project:

flutter create network_images

After creating the example project, enter the directory with the command cd network_images.

Then add flutter_svg as a dependency of the project using the command:

flutter pub add flutter_svg

Define the class that will be used to determine the image data.

In this example, the ImageEntity class will have two properties: name and url. Create the file lib/image_entity.dart:

// lib/image_entity.dart

class ImageEntity {
  ImageEntity({
    required this.url,
    required this.name,
  });

  final String url;
  final String name;
}

With the data class defined, create a new file in lib/data.dart, which will be the file responsible for the data used in the example application.

// lib/data.dart

import 'package:network_images/image_entity.dart';

final one = [
  ImageEntity(
    url: 'https://www.svgrepo.com/show/459084/logo-ts.svg',
    name: 'TypeScript',
  ),
  ImageEntity(
    url: 'https://www.svgrepo.com/show/327396/logo-stackoverflow.svg',
    name: 'StackOverflow',
  ),
  ImageEntity(
    url: 'https://www.svgrepo.com/show/327388/logo-react.svg',
    name: 'React',
  ),
  ImageEntity(
    url: 'https://www.svgrepo.com/show/358716/logo-android.svg',
    name: 'Android',
  ),
  ImageEntity(
    url: 'https://www.svgrepo.com/show/327403/logo-tux.svg',
    name: 'Linux',
  ),
];

final two = [
  ImageEntity(
    url: 'https://www.svgrepo.com/show/459082/logo-js.svg',
    name: 'JavaScript',
  ),
  ImageEntity(
    url: 'https://www.svgrepo.com/show/358715/logo-visual-studio.svg',
    name: 'Visual Studio',
  ),
  ImageEntity(
    url: 'https://www.svgrepo.com/show/327381/logo-npm.svg',
    name: 'NPM',
  ),
  ImageEntity(
    url: 'https://www.svgrepo.com/show/327352/logo-docker.svg',
    name: 'Docker',
  ),
  ImageEntity(
    url: 'https://www.svgrepo.com/show/327391/logo-sass.svg',
    name: 'Sass',
  ),
];

final images = {
  'One': one,
  'Two': two,
}

Next, create the file lib/card.dart, the component responsible for displaying the data:

// lib/card.dart

import 'package:flutter/material.dart';

class ImageCard extends StatelessWidget {
  const ImageCard({
    super.key,
    required this.url,
    required this.name,
  });

  final String url;
  final String name;

  
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 24),
      child: Container(
        decoration: BoxDecoration(
          color: Colors.deepPurple.withOpacity(0.3),
          borderRadius: const BorderRadius.all(Radius.circular(8)),
        ),
        child: Padding(
          padding: const EdgeInsets.all(12),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Container(
                height: 80,
                width: 80,
                color: Colors.black,
              ),
              Text(name),
            ],
          ),
        ),
      ),
    );
  }
}

Finally, modify the lib/main.dart file to create the visual structure using the components created previously:

// lib/main.dart

import 'package:network_images/card.dart';
import 'package:network_images/data.dart';
import 'package:network_images/image_entity.dart';
import 'package:flutter/material.dart' hide Image;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Network Images'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final tags = ['One', 'Two'];
  late String currentTag;

  
  void initState() {
    currentTag = tags.first;
    super.initState();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 24),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            _filterTags(),
            ..._cardList(images[currentTag]!),
          ],
        ),
      ),
    );
  }

  Widget _filterTags() {
    return Padding(
      padding: const EdgeInsets.only(bottom: 20),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: tags
            .map(
              (tag) => Padding(
                padding: const EdgeInsets.only(right: 5),
                child: InkWell(
                  onTap: () => setState(() {
                    currentTag = tag;
                  }),
                  child: Container(
                    padding: const EdgeInsets.all(10),
                    decoration: BoxDecoration(
                      color: tag == currentTag
                          ? Colors.deepPurpleAccent.withOpacity(0.2)
                          : Colors.grey.withOpacity(0.3),
                      borderRadius: const BorderRadius.all(
                        Radius.circular(18),
                      ),
                    ),
                    child: Text(tag),
                  ),
                ),
              ),
            )
            .toList(),
      ),
    );
  }

  List<Widget> _cardList(List<ImageEntity> images) {
    return [
      ...images
          .map(
            (image) => ImageCard(url: image.url, name: image.name),
          )
          .toList()
    ];
  }
}

After creating the base structure, the application will display the following view:

Initial Interface

Using FutureBuilder

With the visual base properly implemented, it's time to use FutureBuilder to load the images and deal with the different scenarios that may arise.

Scenarios

Possible Scnearios

In short, there are three scenarios:

  • Loading: The image is being loaded
  • Sucess: The image exists and has been loaded
  • Fallback: The image does not exist or an error occurred when fetching the image

Based on the possible scenarios, we will organize and componentize the possible interfaces in lib/card.dat;

// lib/card.dart

Widget loading() {
  return Container(
    padding: const EdgeInsets.all(22.0),
    child: const CircularProgressIndicator(
      key: Key('loading'),
    ),
  );
}

Widget networkImage(String imageUrl) {
  return SvgPicture.network(
    imageUrl,
    width: 80,
    height: 80,
    placeholderBuilder: (BuildContext context) => loading(),
  );
}

Widget successfulImage(String url) {
  return networkImage(url);
}

Widget fallbackImage() {
  return networkImage('https://www.svgrepo.com/show/489639/unavailable.svg');
}

Once this is done, we can move on to building the FutureBuilder.

Implementing FutureBuilder

The FutureBuilder has two mandatory parameters: future and builder.

future

The future is an asynchronous function to which the builder will be connected. In our case, future will receive a function responsible for checking whether the image exists, via an http request:

// lib/card.dart

Future<bool> imageExists(String url) async {
  try {
    final uri = Uri.parse(url);
    final response = await http.get(uri);
    final result = response.statusCode == 200;

    return result;
  } catch (error) {
    return false;
  }
}

builder

The builder function has access to the snapshot parameter, based on which we have access to:

PropertyDescriptionType
connectionStateCurrent connection status of the asynchronous functionConnectionState: none, waiting, active, done
hasDataChecks if the asynchronous function has already returned the databoolean
hasErrorChecks for errors in the asynchronous functionboolean
dataData returned by the asynchronous functionboolean

With this information we can create possible image loading scenarios:

// lib/card.dart

// Remove
Container(
  height: 80,
  width: 80,
  color: Colors.black,
),

// Add
FutureBuilder(
  future: imageExists(url),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.done) {
      if (snapshot.hasData) {
        if (snapshot.data == true) {
          return successfulImage(url);
        } else {
          return fallbackImage();
        }
      } else if (snapshot.hasError) {
          return fallbackImage();
      }
    }

    return loading();
  },
),

Visual result of this code:

Interface with FutureBuilder

However, as you can see, when you change tabs, the images are loaded again.

This is because we are checking if the image exists when the tab is changed, so the loading shown is not of the image being loaded, but of the function checking if the image exists on the server.

We can mitigate this small problem with a simple cache in the request.

Using cache

To deal with this problem, you'll need to implement a cache on the request made to the imageExists function. To do this, move the imageExists function to lib/validation:

// lib/validation.dart

import 'package:collection/collection.dart';
import 'package:http/http.dart' as http;

class Cache {
  final String url;

  final bool exists;
  const Cache({
    required this.url,
    required this.exists,
  });
}

final List<Cache> list = [];

Future<bool> imageExists(String url) async {
  final cache = list.firstWhereOrNull((element) => element.url == url);

  if (cache == null) {
    try {
      final uri = Uri.parse(url);
      final response = await http.get(uri);
      final result = response.statusCode == 200;

      list.add(Cache(url: url, exists: result));

      return result;
    } catch (error) {
      return false;
    }
  } else {
    return cache.exists;
  }
}

Finally, with this simple cache implementation, loading is done only when the tab is opened for the first time:

Interface with cache

Conclusion

As the focus of this post is only on an optimized implementation of the flutter_svg lib, the current implementation can still be optimized. Starting with the cache, specialized libraries can be used to improve performance, as well as the essential unit tests to guarantee code quality.