17 Dependency Management I How to Use Image Configuration and Fonts in Flutter

17 Dependency Management I How to Use Image Configuration and Fonts in Flutter #

Hello, my name is Chen Hang.

In the previous article, I introduced to you the theme setting in Flutter, which is a mechanism for centralized management of visual resources and configurations.

Flutter provides ThemeData that follows the Material Design guidelines, which allows for customization of styles. It can be used to achieve a unified global visual style when initializing the app, override the theme for a specific widget container using Theme, or extract the corresponding attribute values from the theme when customizing components, enabling visual style reusability.

An application is mainly composed of two parts: code and resources. The code focuses on logical functionality, while resources such as images, strings, fonts, and configuration files focus on visual functionality. If the previous article focused more on how to manage resource configurations from a logical perspective, today’s article will start from the physical storage to introduce Flutter’s overall resource management mechanism to you.

Externalizing resources, that is, separating code from resources, is the mainstream design concept of modern UI frameworks. This not only facilitates the separate maintenance of resources but also provides more accurate compatibility support for specific devices, allowing our application to automatically organize visual functionality based on the actual runtime environment, adapting to different screen sizes and densities, etc.

As various terminals with different configurations become more and more numerous, resource management is becoming increasingly important. So today, let’s take a look at the management mechanism for images, configuration, and fonts in Flutter.

Resource Management #

In mobile development, common types of resources include JSON files, configuration files, icons, images, and font files, etc. They are all packaged into the app installation package, and the code in the app can access these resources at runtime.

In Android and iOS platforms, images and other raw resources are treated differently to differentiate between different resolutions of mobile devices:

  • iOS uses Images.xcassets to manage images, and other resources can be directly added to the project;
  • Android has a more detailed resource management granularity, using folders with drawable+resolution naming to store images of different resolutions separately. Other types of resources also have their own storage methods, such as layout files placed in the res/layout directory, resource description files placed in the res/values directory, and original files placed in the assets directory, etc.

In Flutter, resource management is much simpler: resources (assets) can be any type of file, not just images.

Regarding the location of resources, Flutter does not predefine the directory structure for resources like Android, so we can store resources in any directory in the project. We just need to use the pubspec.yaml file in the root directory to explicitly declare the location of these resources to help Flutter recognize them.

In the process of specifying the file path, we can specify each file individually or use the batch specification method with subdirectories.

Next, I will explain the difference between specifying each file individually and batch specifying with a subdirectory using an example.

As shown below, we place resources in the assets directory. Among them, two images background.jpg and loading.gif, as well as the JSON file result.json, are in the root directory of the assets, and another image food_icon.jpg is in the subdirectory “icons” of the assets.

assets
├── background.jpg
├── icons
│   └── food_icon.jpg
├── loading.gif
└── result.json

For the directory structure of the resource files mentioned above, the following code demonstrates the two methods of specifying each file individually and batch specifying with a subdirectory: For individually specified files, we need to fully expand the relative path of the resources; while for the batch specified method, we just need to add the path separator after the directory name:

flutter:
  assets:
    - assets/background.jpg   # Specify the resource path individually
    - assets/loading.gif  # Specify the resource path individually
    - assets/result.json  # Specify the resource path individually
    - assets/icons/    # Batch specify with subdirectory
    - assets/ # Root directory can also be batch specified

Please note that the batch specified method does not recurse, only the files in that directory can be included. If there are subdirectories below, the files in the subdirectory need to be declared separately.

After declaring the resources, we can access them in the code. In Flutter, the way of handling different types of resource files is slightly different, and I will introduce them separately.

For image resources, we can use the Image.asset constructor to load and display image resources. In the 12th article “Classic Widgets (1): How to use text, image and button in Flutter” you should have already learned the specific usage, so I won’t go into it here.

For loading other resource files, we can directly access them through the Flutter app’s main resource bundle object rootBundle.

For string file resources, we use the loadString method; for binary file resources, we use the load method.

The following code demonstrates the process of obtaining the result.json file and printing it:

rootBundle.loadString('assets/result.json').then((msg)=>print(msg));

Similar to Android and iOS development, Flutter also follows a density-based management approach, such as 1.0x, 2.0x, 3.0x, or any other multiple. Flutter can load image resources that are closest to the device pixel ratio based on the current device’s resolution. To help Flutter recognize them better, our resource directory should separate the 1.0x, 2.0x, and 3.0x image resources. Taking the background.jpg image as an example, this image is located in the assets directory. If we want Flutter to adapt to different resolutions, we need to place the images of other resolutions in their corresponding subdirectories, as shown below:

assets
├── background.jpg    //1.0x image
├── 2.0x
│   └── background.jpg  //2.0x image
└── 3.0x
    └── background.jpg  //3.0x image

When declaring this image resource in the pubspec.yaml file, only the 1.0x image resource needs to be declared:

flutter:
  assets:
    - assets/background.jpg   #1.0x image resource

The 1.0x resolution image is the resource identifier, and Flutter will load the corresponding resolution image based on the actual screen pixel ratio. At this time, if the primary resource is missing a particular resolution resource, Flutter will choose the closest resolution resource from the remaining resources to load.

For example, if our app bundle only includes the 2.0x resource, for a device with a screen pixel ratio of 3.0, it will automatically fall back to loading the 2.0x resource. However, it is important to note that even if our app bundle does not include the 1.0x resource, we still need to declare it in the pubspec.yaml file as shown above because it is the resource identifier.

Fonts are another commonly used type of resource. Mobile operating systems generally only have a few default fonts, which can meet our normal needs in most cases. However, in some special cases, we may need to use custom fonts to enhance the visual experience.

In Flutter, using custom fonts also requires prior declaration in the pubspec.yaml file. It is important to note that fonts are actually mappings of character graphics. So, in addition to normal font files, if your app needs to support bold and italic fonts, you also need corresponding bold and italic font files.

After placing the RobotoCondensed font in the fonts subdirectory under the assets directory, the code below demonstrates how to add the RobotoCondensed font that supports italic and bold to our app:

fonts:
  - family: RobotoCondensed  #font name
    fonts:
      - asset: assets/fonts/RobotoCondensed-Regular.ttf #normal font
      - asset: assets/fonts/RobotoCondensed-Italic.ttf 
        style: italic  #italic
      - asset: assets/fonts/RobotoCondensed-Bold.ttf 
        weight: 700  #bold

These declarations correspond to the style attributes in TextStyle, such as the font name family corresponding to the fontFamily attribute, the italic italic and normal normal corresponding to the style attribute, and the font weight weight corresponding to the fontWeight attribute, etc. When using them, we just need to specify the corresponding font in TextStyle:

Text("This is RobotoCondensed", style: TextStyle(
    fontFamily: 'RobotoCondensed',//normal font
));
Text("This is RobotoCondensed", style: TextStyle(
    fontFamily: 'RobotoCondensed',
    fontWeight: FontWeight.w700, //bold
));
Text("This is RobotoCondensed italic", style: TextStyle(
  fontFamily: 'RobotoCondensed',
  fontStyle: FontStyle.italic, //italic
));

Figure 1: Custom font

Resource Setting on Native Platforms #

In the previous article “Starting with the Standard Template to See How Flutter Code Runs on Native Systems”, I introduced Flutter applications to you. As a matter of fact, in the end, Flutter applications are packaged and run on Android and iOS platforms as native projects. Therefore, Flutter relies on the native Android and iOS runtime environments when it starts.

The resource management mechanisms mentioned above are actually within the Flutter application. Before the Flutter framework runs, we have no way to access these resources. Flutter requires a native environment to run, but there are some resources that we need to use before the Flutter framework starts. For example, if we want to add icons to the application or if we want to add a launch image while waiting for the Flutter framework to start, we need to complete the corresponding configuration in the corresponding native projects. Therefore, the following steps are all completed in the native system.

Let’s first take a look at how to change the app’s launch icon.

For the Android platform, the launch icon is located at android/app/src/main/res/mipmap in the root directory. We just need to follow the corresponding pixel density standards, keep the original icon name, and replace the icon with the target resource:

Figure 2 Changing the Android launch icon

For the iOS platform, the launch image is located at ios/Runner/Assets.xcassets/AppIcon.appiconset in the root directory. Similarly, we just need to follow the corresponding pixel density standards, replace it with the target resource, and keep the original icon name:

Figure 3 Changing the iOS launch icon

Now, let’s take a look at how to change the launch image.

For the Android platform, the launch image is located at android/app/src/main/res/drawable in the root directory, which is an XML layout description file named launch_background.

Figure 4 Modifying the Android launch image description file

We can customize the launch screen in this layout description file, or we can replace it with another image. In the example below, we replaced it with a centrally displayed launch image:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- White background -->
    <item android:drawable="@android:color/white" />
    <item>
        <!-- Embed a centrally displayed image -->
        <bitmap
            android:gravity="center"
            android:src="@mipmap/bitmap_launcher" />
    </item>
</layer-list>

For the iOS platform, the launch image is located at ios/Runner/Assets.xcassets/LaunchImage.imageset in the root directory. We need to keep the original launch image name and replace the images one by one according to the corresponding pixel density standards with the target launch image.

Figure 5 Changing the iOS launch image

Summary #

Okay, that’s it for today’s sharing. Let’s briefly review the content of today.

Separating code from resources not only helps maintain resources separately, but also provides more accurate compatibility support for specific devices. In Flutter, resources can be any type of file and can be placed in any directory, but their paths need to be explicitly declared uniformly through the pubspec.yaml file.

Flutter provides a management method for images based on pixel density. We need to manage resources for 1.0x, 2.0x, and 3.0x separately, but only need to declare them once in pubspec.yaml. If the application lacks resource support for high pixel density devices, Flutter will automatically degrade.

For resource files such as fonts, which are based on character graphic mapping, Flutter provides fine-grained management mechanisms that support not only normal fonts but also styles such as bold and italic.

Finally, because Flutter relies on the native system runtime environment when starting, we also need to set the corresponding app launch icon and launch image in the native project.

Thought Questions #

Finally, I’d like to give you two thought questions.

  1. If we only provide resources images with a pixel density of 1.0x and 2.0x, what resource set will Flutter automatically degrade to for a device with a pixel density of 3.0?
  2. If we only provide resources images with a pixel density of 2.0x, how will Flutter handle it for a device with a pixel density of 1.0?

You can refer to the experience of native platforms and try experimenting on simulators or real devices.

Feel free to leave a comment in the comment section to share your thoughts. I’ll be waiting for you in the next article! Thank you for listening and feel free to share this article with more friends to read together.