Preface
In application development, we often encounter the loading of network images. Usually we cache the images so that we don’t need to download them again the next time we load the same image. In applications containing a large number of images, the speed of image display will be greatly improved, the user experience will be improved, and the user’s traffic will be saved. The Image Widget provided by Flutter itself has implemented the function of loading network images and has a memory cache mechanism. Let’s take a look at the implementation of Image’s network image loading.
Relive widget Image
Several constructors are implemented in commonly used widget Image, which is enough to create Image objects in various scenarios in our daily development.
Parameter constructor:
Image(Key key, @required , ...)
Developers can create Images based on custom ImageProvider.
Named constructor:
(String src, ...)
src is the image URL address obtained based on the network.
(File file, ...)
file refers to a local image file object, and the .READ_EXTERNAL_STORAGE permission is required in Android.
(String name, ...)
name refers to the image resource name added in the project, declared in the file in advance.
(Uint8List bytes, ...)
bytes refers to the image data in memory, which is converted into image objects.
Among them is the focus of our sharing in this article - loading network pictures.
Source code analysis
Let’s take a look at the specific implementation of loading network pictures through the source code.
(String src, { Key key, double scale = 1.0, . . }) : image = NetworkImage(src, scale: scale, headers: headers), assert(alignment != null), assert(repeat != null), assert(matchTextDirection != null), super(key: key); /// The image to display. final ImageProvider image;
First, when creating an Image object using a named constructor, the instance variable image will be initialized at the same time. Image is an ImageProvider object. The ImageProvider is the provider of the image we need. It itself is an abstract class. Subclasses include NetworkImage, FileImage, ExactAssetImage, AssetImage, MemoryImage, etc. The network uses NetworkImage to load images.
As a StatefulWidget, Image is controlled by _ImageState. _ImageState inherits from the State class. Its life cycle methods include initState(), didChangeDependencies(), build(), deactivate(), dispose(), didUpdateWidget(), etc. Let's focus on the execution of functions in ImageState.
Since the initState() function is called first when inserting the rendering tree, and then called didChangeDependencies() function, the initState function is not rewritten in _ImageState, so the didChangeDependencies() function will be executed. Let's take a look at the contents in didChangeDependencies().
@override void didChangeDependencies() { _invertColors = (context, nullOk: true)?.invertColors ?? ; _resolveImage(); if ((context)) _listenToStream(); else _stopListeningToStream(); (); } _resolveImage()Will be called,The function content is as follows void _resolveImage() { final ImageStream newStream = (createLocalImageConfiguration( context, size: != null && != null ? Size(, ) : null )); assert(newStream != null); _updateSourceStream(newStream); }
A ImageStream object is first created in the function, which is a handle to an image resource, which holds the listen callback and the image resource manager after the image resource is loaded. The ImageStreamCompleter object is a management class for image resources. That is to say, _ImageState establishes a connection through ImageStream and ImageStreamCompleter management classes.
Let’s look back at the ImageStream object is created through methods, which is the resolve method corresponding to NetworkImage. When we look at the source code of the NetworkImage class, we found that there is no resolve method, so we look for its parent class and found it in the ImageProvider class.
ImageStream resolve(ImageConfiguration configuration) { assert(configuration != null); final ImageStream stream = ImageStream(); T obtainedKey; Future<void> handleError(dynamic exception, StackTrace stack) async { . . } obtainKey(configuration).then<void>((T key) { obtainedKey = key; final ImageStreamCompleter completer = (key, () => load(key), onError: handleError); if (completer != null) { (completer); } }).catchError(handleError); return stream; }
Image Manager ImageStreamCompleter in ImageStream is created by the (key, () => load(key), onError: handleError); method. imageCache is a singleton used for image caching implemented in the Flutter framework. Check out the putIfAbsent method in it.
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) { assert(key != null); assert(loader != null); ImageStreamCompleter result = _pendingImages[key]?.completer; // Nothing needs to be done because the image hasn't loaded yet. if (result != null) return result; // Remove the provider from the list so that we can move it to the // recently used position below. final _CachedImage image = _cache.remove(key); if (image != null) { _cache[key] = image; return ; } try { result = loader(); } catch (error, stackTrace) { if (onError != null) { onError(error, stackTrace); return null; } else { rethrow; } } void listener(ImageInfo info, bool syncCall) { // Images that fail to load don't contribute to cache size. final int imageSize = info?.image == null ? 0 : * * 4; final _CachedImage image = _CachedImage(result, imageSize); // If the image is bigger than the maximum cache size, and the cache size // is not zero, then increase the cache size to the size of the image plus // some change. if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) { _maximumSizeBytes = imageSize + 1000; } _currentSizeBytes += imageSize; final _PendingImage pendingImage = _pendingImages.remove(key); if (pendingImage != null) { (); } _cache[key] = image; _checkCacheSize(); } if (maximumSize > 0 && maximumSizeBytes > 0) { _pendingImages[key] = _PendingImage(result, listener); (listener); } return result; }
Through the above code, you can see that the key will be used to find out whether the cache exists. If it exists, it will return. If it does not exist, it will create an image resource manager by executing the loader() method, and then register the listening method of the cached image resource to the newly created image manager so that the cache processing will be done after the image is loaded.
According to the above code call (key, () => load(key), onError: handleError); it is seen that the load() method is implemented by the ImageProvider object. Here is the NetworkImage object. See its specific implementation code
@override ImageStreamCompleter load(NetworkImage key) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key), scale: , informationCollector: (StringBuffer information) { ('Image provider: $this'); ('Image key: $key'); } ); }
In the code, it is to create a MultiFrameImageStreamCompleter object and return it. This is a multi-frame image manager, indicating that Flutter supports GIF images. The codec variable when creating an object is initialized by the return value of the _loadAsync method, and view the content of the method
static final HttpClient _httpClient = HttpClient(); Future<> _loadAsync(NetworkImage key) async { assert(key == this); final Uri resolved = (); final HttpClientRequest request = await _httpClient.getUrl(resolved); headers?.forEach((String name, String value) { (name, value); }); final HttpClientResponse response = await (); if ( != ) throw Exception('HTTP request failed, statusCode: ${response?.statusCode}, $resolved'); final Uint8List bytes = await consolidateHttpClientResponseBytes(response); if ( == 0) throw Exception('NetworkImage is an empty file: $resolved'); return (bytes); }
This is the key, which is to download the specified url through the HttpClient object. After the download is completed, instantiate the image codec object Codec based on the image binary data, and then return.
So how do I display the image on the interface after downloading it? Let’s take a look at the construction method of MultiFrameImageStreamCompleter
MultiFrameImageStreamCompleter({ @required Future<> codec, @required double scale, InformationCollector informationCollector }) : assert(codec != null), _informationCollector = informationCollector, _scale = scale, _framesEmitted = 0, _timer = null { <void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) { reportError( context: 'resolving an image codec', exception: error, stack: stack, informationCollector: informationCollector, silent: true, ); }); }
See, the code block in the constructor method, the codec asynchronous method will be executed after the codec is executed, and the function content is as follows
void _handleCodecReady( codec) { _codec = codec; assert(_codec != null); _decodeNextFrameAndSchedule(); }
The codec object will be saved in the method and then the image frame will be decoded.
Future<void> _decodeNextFrameAndSchedule() async { try { _nextFrame = await _codec.getNextFrame(); } catch (exception, stack) { reportError( context: 'resolving an image frame', exception: exception, stack: stack, informationCollector: _informationCollector, silent: true, ); return; } if (_codec.frameCount == 1) { // This is not an animated image, just return it and don't schedule more // frames. _emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale)); return; } (_handleAppFrame); }
If the image is png or jpg with only one frame, execute the _emitFrame function, get the image frame object from the frame data to create an ImageInfo object according to the scaling ratio, and then set the displayed image information
void _emitFrame(ImageInfo imageInfo) { setImage(imageInfo); _framesEmitted += 1; } /// Calls all the registered listeners to notify them of a new image. @protected void setImage(ImageInfo image) { _currentImage = image; if (_listeners.isEmpty) return; final List<ImageListener> localListeners = _listeners.map<ImageListener>( (_ImageListenerPair listenerPair) => ).toList(); for (ImageListener listener in localListeners) { try { listener(image, false); } catch (exception, stack) { reportError( context: 'by an image listener', exception: exception, stack: stack, ); } } }
At this time, a new image will be notified based on the added listener to be rendered. So when was this listener added? Let's look back at the didChangeDependencies() method in the _ImageState class. After executing _resolveImage(); the _listenToStream(); method will be executed.
void _listenToStream() { if (_isListeningToStream) return; _imageStream.addListener(_handleImageChanged); _isListeningToStream = true; }
This method adds a listener_handleImageChanged to the ImageStream object. The listening method is as follows
void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) { setState(() { _imageInfo = imageInfo; }); }
In the end, the setState method is called to notify the interface to refresh and render the downloaded image to the interface.
Practical Problems
From the above source code analysis, we should be clear about the process of loading to displaying the entire network image. However, using this native method, we found that the network image is only cached in memory. If the application process is killed and then reopened, we still have to download the image again. For users, each time they open the application, it will consume the traffic of downloading the image. However, we can learn some ideas from it to design the network image loading framework ourselves. The author will simply make a transformation based on it and increase the disk cache of the image.
Solution
Through source code analysis, we can see that when the image is not found in the cache, it will be downloaded directly through the network. The download method is in the NetworkImage class, so we can refer to NetworkImage to customize an ImageProvider.
Code implementation
Copy a copy of NetworkImage's code to the newly created network_image.dart file, and add the disk cache code to the _loadAsync method.
static final CacheFileImage _cacheFileImage = CacheFileImage(); Future<> _loadAsync(NetworkImage key) async { assert(key == this); /// Add new code block start/// Find the image from the cache directory if it exists final Uint8List cacheBytes = await _cacheFileImage.getFileBytes(); if(cacheBytes != null) { return (cacheBytes); } /// Add code block end final Uri resolved = (); final HttpClientRequest request = await _httpClient.getUrl(resolved); headers?.forEach((String name, String value) { (name, value); }); final HttpClientResponse response = await (); if ( != ) throw Exception('HTTP request failed, statusCode: ${response?.statusCode}, $resolved'); /// Add new code block start/// Save the downloaded image data to the specified cache file await _cacheFileImage.saveBytesToFile(, bytes); /// Add code block end return (bytes); }
The comments in the code have shown that the new code blocks are added based on the original code. CacheFileImage is a file cache class defined by itself. The complete code is as follows.
import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:crypto/'; import 'package:path_provider/path_provider.dart'; class CacheFileImage { /// Get the MD5 value of the url string static String getUrlMd5(String url) { var content = new Utf8Encoder().convert(url); var digest = (content); return (); } /// Get the image cache path Future<String> getCachePath() async { Directory dir = await getApplicationDocumentsDirectory(); Directory cachePath = Directory("${}/imagecache/"); if(!()) { (); } return ; } /// Determine whether there is a corresponding image cache file Future<Uint8List> getFileBytes(String url) async { String cacheDirPath = await getCachePath(); String urlMd5 = getUrlMd5(url); File file = File("$cacheDirPath/$urlMd5"); print("Read the file:${}"); if(()) { return await (); } return null; } /// Cache the downloaded image data to the specified file Future saveBytesToFile(String url, Uint8List bytes) async { String cacheDirPath = await getCachePath(); String urlMd5 = getUrlMd5(url); File file = File("$cacheDirPath/$urlMd5"); if(!()) { (); await (bytes); } } }
This adds the function of file caching. The idea is very simple. Before obtaining network pictures, check whether there are cached files in the local file cache directory. If so, you don’t need to download it again. Otherwise, download the picture. After the download is completed, cache the downloaded picture to the file for use next time.
The following dependency libraries need to be added to the project
dependencies: path_provider: ^0.4.1 crypto: ^2.0.6
Customize ImageProvider usage
When creating an image widget, use a non-named constructor with parameters, and specify the image parameter as a custom ImageProvider object. The code example is as follows
import 'imageloader/network_image.dart' as network; Widget getNetworkImage() { return Container( color: , width: 200, height: 200, child: Image(image: ("/images/")), ); }
Written at the end
The above analyzes the network image loading process of the Image widget that comes with Flutter. After understanding the source code design ideas, we have added a simple local file caching function, which allows our network image loading to have both memory cache and file caching capabilities, greatly improving the user experience. If other students have better solutions, they can leave messages to the author to communicate.
The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.