Progress made to CachedNetworkImage
At this moment, we are in a worldwide healthcare crisis due to coronavirus. As we developers already know, the whole world knows by now: “Testing, testing, testing” is critical, and we know we should do that, but not many of us have adequate test coverage. When a package becomes as popular as CachedNetworkImage, it is essential to test all the cases, so new functionality doesn’t break anything for existing users. CachedNetworkImage now exceeds the 1.1k Github Stars and still belongs to the top packages on pub.
Testing, testing, testing
Before we focus on the new functionality of CachedNetworkImage, we want to improve the quality of the packages that support our beautiful images, flutter_cache_manager. For a package used by so many people, it had terrifyingly few tests: 0. We are proud to announce that we brought the test coverage from 0% to 81% in the latest release. Next to testing, we also added lots of rules in the analysis_options.yaml file, which didn’t even exist, which helped us solve some potential bugs. It can’t be emphasized enough how important it is to add at least a minimal set of analysis options and to write tests for your code early on. An excellent place to start with the analysis options is to add the pedantic set as advised by the dart team.
Updates on your download progress
One of the things we did rewrite with the help of the community is the FileFetcher. Previously you could supply your own FileFetcher to change the default http client, but you could only provide full-size content and headers. This setup makes it inefficient and hard to manage. For example, when you want to change the max-age of your object, you have to change the headers, which is not something you should be doing. The new FileService asks you to extract all the headers but gives you also the option to manipulate them as you want. For example, when you expect a particular file extension that doesn’t match the http content-type, you can do that. We also no longer expect the full-size content, but a stream of the content. When you still use the now deprecated FileFetcher, the content will be supplied as a stream with only one emitting value.
The content stream has two implications on how the content is handled. First, the library directly writes content to a file on disk, while the content is streaming from the webserver to the application. Doing so reduces the amount of memory used by the library as it no longer has to keep the whole file in memory before it is stored. Doing so in the old situation would introduce significant risk. The CacheManager used to keep the file name constant while updating the content. If you had an outdated file in your cache and the server gave you an update, it would overwrite the outdated file. In the old situation, we only risked an IO exception to lose your old file. In the new situation, we would risk any unstable internet connection to fail the download and storage of the file. So now, we always store the data in a new file, and when that file is successfully saved, we update the cache information and delete the old file. This whole process of changing the content to a stream is made so much easier by the tests we have written before that we could very easily test most of the cases and are sure that it works as intended.
This change of content to a stream gave us also the option for a much-requested feature in CachedNetworkImage: the download progress. As we already have a stream in place, we can easily supply that information to the CachedNetworkImage. In CachedNetworkImage we now added the option to add a progress indicator like this:
progressIndicatorBuilder: (context, url, downloadProgress) =>
errorWidget: (context, url, error) => Icon(Icons.error),
We deliberately added a progressIndicatorBuilder to the existing placeholder as the behavior is different. The progress indicator rebuilds very often with every status update it receives, while the placeholder is built once and kept until the image is fully fetched. When you don’t use the progress to show to the user, the placeholder is thus the more efficient and preferred way. The download progress gives you the bytes downloaded, the total size of the file, and the progress as a percentage. See the doc for more information.
What the library does not fix, however, is just plain wrong content. This sounds obvious, but we still get a lot of requests to do that. There are two types of things going wrong often. First, is the server supplying the wrong content-type? I have seen servers supplying mp3 files with content-type application/octet-stream. Fixing this is not something we would even want to start with, so make sure your server supplies the right headers for the content you request. If you don’t have access to the server, you could always consider downloading the files and storing them yourselves on your server. Or, if you know the content-type beforehand, you can now manipulate the fileExtension by supplying your FileService as told earlier.
The second thing the library cannot fix for you is if you expect an image but download an html page. For example, this url looks like an image but is actually an HTML page with an image on it:
Always make sure to correctly link to the image so that only the image is downloaded. Some servers don’t serve 404 status codes when an image is not found, but a 404 web page with a status code 200. This can also lead to unexpected results as the CacheManager thinks it successfully downloaded the file and will not try to update it before max-age is reached. So bottom line, always make sure the server sends the information the way the web standards expect it.
The changes discussed in this article are released as flutter_cache_manager 1.2, and the progress indicator can be used with cached_network_image 2.1. Now that we largely improved the quality of the CacheManager, we can continue with improving the quality of CachedNetworkImage. Going through the whole process again, we will write tests, improve the code quality, and add features when opportunities arrive. Creating a good foundation will give us the possibility to quickly add new features in the future.
Read more of our blogs here