- Published on
Good practices when loading SVG's with flutter_svg
- Authors
- Name
- Emanoel Oliveira
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:
FutureBuilder
Using 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
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
.
FutureBuilder
Implementing 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:
Property | Description | Type |
---|---|---|
connectionState | Current connection status of the asynchronous function | ConnectionState : none, waiting, active, done |
hasData | Checks if the asynchronous function has already returned the data | boolean |
hasError | Checks for errors in the asynchronous function | boolean |
data | Data returned by the asynchronous function | boolean |
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:
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.
cache
Using 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:
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.