Flutter Design Patterns: 4 — Composite
An overview of the Composite design pattern and its implementation in Dart and Flutter
In the last article, I have analysed the Template Method design pattern. This time I would like to represent the pattern which is pretty simple to understand (comparing to the other design patterns) and is related to the implementation of the Flutter framework itself — the Composite design pattern.
Table of Contents
- What is the Composite design pattern?
- Other articles in this series
- Your contribution
What is the Composite design pattern?
The Composite is one of the structural design patterns. Its intention in the GoF book is described as:
Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
To understand the Composite design pattern, you should be familiar with the tree data structure.
In simple words, the tree data structure consists of nodes (elements) and edges (relations between nodes). Each node could have multiple children nodes, but each child could have only one parent node. The root node is the base of the tree, which has no parent node. The leaf is a tree node, which does not have any children. In the Composite design pattern context, we use two types of nodes — leaf (a component, which has no child components) and composite (a component, which contains one or more child components). Basically, any hierarchical data could be represented and stored as a tree structure. The main problem — how to implement this kind of structure in code? A very inflexible way is to define leaf and composite objects differently, treat the composite object as a container for the leaf objects by specifying a specific logic, interface for it. This leads to the issue when a client should treat leaf and composite objects differently, hence making the code very complex especially when the data structure is constructed dynamically. That is one of the main reasons why the Composite design pattern is used — to define an abstract class (interface works as well) that represents both leaf and composite objects uniformly hence letting clients treat every element of the tree structure in the same manner.
Doesn’t it sound familiar? “In Flutter, everything is a widget!”, “Flutter widget tree”, no? The Flutter framework builds the UI of the application as a Widget tree, allows you to put widgets inside other widgets or their containers (widgets, which contain the children property, e.g. Column, Row, ListView, etc.). That’s pretty much a Composite design pattern, well, on steroids and with some additional Flutter magic…
The general structure of the Composite design pattern looks like this:
- Component — declares the interface for objects in the composition. This interface allows the client to treat leaf and composite objects uniformly.
- Leaf — represents leaf objects in the composition. This object does not have sub-elements (child components), defines behaviour for primitive objects in the composition and does most of the real work since they don’t have anyone to delegate the work to.
- Composite — stores sub-elements (children) and implements child-related operations in the Composite interface. Differently from the leaf component, composite object delegates the work to its child elements, processes intermediate results ant then returns the final result to the client.
- Client — uses the Component interface to interact with objects in the composite structure. This allows the client to work with simple and complex elements of the tree in the same way.
The Composite design pattern should be used when you want to represent part-whole hierarchies of objects and you want clients to be able to ignore the difference between compositions of objects and individual objects. In my opinion, the most difficult part of this pattern is to identify where and when you can apply it in your codebase. A general rule of thumb — if you have a set of groups or collections, this is a big indicator that you might be able to use the Composite design pattern. The easier case to detect — you are using a tree structure. In this case, you should think where you can apply the pattern to make the work with this data structure easier. If you detect these cases in your code, only the implementation details are left, which I will describe next.
This time the implementation of the design pattern is more visual (finally!) and would make more sense in Flutter context (yes, I do consider your feedback, so do not hesitate to share your insights about the series — it helps me improve the quality of the content a lot!). Let’s say, we want to represent the structure of our file system. The file system consists of directories and files of various types: audio, video, images, text files, etc. Files are stored inside directories, also, directories could be stored inside other directories. For instance, our file structure could look like this:
Besides, we want to show the size of each file or directory. It is easy to show it for a concrete file, but the directory size depends on the items inside it and should be calculated. To implement this, the Composite design pattern is a great option!
The class diagram below shows the implementation of the Composite design pattern.
IFile defines a common interface for both File (leaf) and Directory (composite) classes:
- getSize() — returns size of the file;
- render() — renders the component’s UI.
File class implements the getSize() and render() methods, additionally contains title, size and icon properties. Directory implements the same required methods, but it also contains title, isInitiallyExpanded and files list, containing the IFile objects, defines addFile() method, which allows adding IFile objects to the directory (files list). AudioFile, ImageFile, TextFile and VideoFile classes extend the File class to specify a concrete type of the file.
An interface which defines methods to be implemented by leaf and composite components. Dart language does not support the interface as a class type, so we define an interface by creating an abstract class and providing a method header (name, return type, parameters) without the default implementation.
A concrete implementation of the IFile interface which matches the leaf class in the Composite design pattern. In File class, getSize() method simply returns the file size, render() — returns file’s UI widget which is used in the example screen.
Concrete classes extending File
All of these classes extend the File class and specify the concrete file type by providing a unique icon for the corresponding file type.
A concrete implementation of the IFile interface which matches the composite class in the Composite design pattern. Similar as in File class, render() returns directory’s UI widget which is used in the example screen. However, in this class getSize() method calculates the directory size by calling the getSize() method for each item in the files list and adding up the results. This is the main idea of the Composite design pattern which allows the composite class to treat all the elements in the containing list uniformly as long as they implement the same interface.
To represent the file size in a more appealing format, FileSizeConverter helper class was created which provides a static method bytesToString() which converts the value of the file size in bytes to the human-readable text.
First of all, a markdown file is prepared and provided as a pattern’s description:
CompositeExample widget contains the buildMediaDirectory() method which builds the file structure for the example. This method illustrates the Composite design pattern — even though the components are of a different type, they could be handled in the same manner since the implemented interface of IFile is the same for all components. This allows us to put Directory objects inside other directories, mix them along with concrete File objects hence building the tree structure of IFile components.
The final result of the Composite design pattern’s implementation looks like this:
As you can see in the example, the file size is shown for each file directly and for directories, it is calculated by adding up each file size inside the directory.
All of the code changes for the Composite design pattern and its example implementation could be found here.
Other articles in this series
👏 Press the clap button below to show your support and motivate me to write better!
💬 Leave a response to this article by providing your insights, comments or wishes for the series.
📢 Share this article with your friends, colleagues in social media.
➕ Follow me on Medium.
⭐ Star the Github repository.