Code Generation In Flutter
What is Code Generation? What are its Use cases? How to use build_runner and json_serializable? What are the Disadvantages of Code Generation?
What is Code Generation π οΈ?
- When developing an application/software, we want our code to be clean,readable , and reusable. The DRY principle is followed in software development. DRY stands for 'Do not repeat yourself.' It is the idea of reducing repetition in code.
- We may achieve the same result with less and more legible code if we use Code generation. The generator just has to be written once, and it may be reused as many times as you need.
- Let's now understand the use of code generation in development.
Use cases of Code Generator π€
- When you create a Data class in Dart, you manually have to define the
toJson
andfromJson
methods for serializing. For example : User.dart
class User { User({ required this.name, required this.city, required this.age, }); String name; String city; int age; factory User.fromJson(Map<String, dynamic> json) => User( name: json["name"], city: json["city"], age: json["age"], ); Map<String, dynamic> toJson() => { "name": name, "city": city, "age": age, }; }
- This looks good. Right? Yea, I agree. But think about it this way, In general, we have an application where it has tons of data coming from the APIs.
- Now if you're thinking to write the Data class for more than
50
or100
properties, then..... - π
. Because it will take a long time to write all the properties in the class and then initialize it in the factory function constructor and
toJson
method. It will also raise the number of lines of code while also increasing the complexity. - This is one of the reasons why, when developing medium/large-scale apps, you should consider using a code generator.
- Another example could be:
- If you want to compare two objects you have to override
==
andhashCode
methods. If we consider the above example, void main() { final user1 = User(name:"Abc",city: "zzz",age: 12); final user2 = User(name:"Abc",city: "zzz",age: 12); print( user1 == user2); // false }
- When you compare
user1
anduser2
, you will getfalse
as output. It's because you have to override the==
andhashCode
methods. class User { User({ required this.name, required this.city, required this.age, }); String name; String city; int age; // .... @override bool operator ==(Object other) => identical(this, other) || other is User && runtimeType == other.runtimeType && name == other.name && city == other.city && age == other.age; @override int get hashCode => name.hashCode ^ city.hashCode ^ age.hashCode; }
- This appears to be suitable for small-scale applications once again. However, in medium/large size applications, modifying all of these attributes will result in an increase in complexity and code readability.
- These are the two scenarios I've chosen. However, code creation may be used for a variety of purposes.
- So, how do you use a code generator in a Flutter/Dart app? Let's see next.
What is build_runner βοΈ?
build_runner
is a Dart package that provides a concrete way of generating files using Dart code.- This package is essential for generating a code inside a dart file. It has a different built-in command which is used to generate code.
- We generally use the
build
andwatch
commands. - The
build
command builds the file only once. And after that, it exists. - Whereas, the
watch
command continuously watches for the change in the files. If any changes occur then it build that file again. - To use this package, we have to put it under the
dev_dependencies
inside thepubspec.yaml
file. dev_dependencies: build_runner: <latest_version>
- Sometimes, when you build a file, It gives an error that it found conflict in the file. To solve it, add
--delete-conflicting-outputs
argument after the build command. - Now as we've added the
build_runner
which is responsible for generating the code. We can now use different code generation packages like,json_serializable
,freezed
, etc.
Serializing JSON using json_serializable
- This package automatically generates the code for converting
toJson
andfromJson
by annotating Dart classes. - We've seen above that writing
toJson
andfromJson
by self is a tedious and lengthy process. So let's use this package to generate those two functions using this package.Install Pacakge
dependencies: json_annotation: <latest_version>
dev_dependencies: build_runner: <latest_version> json_serializable: <latest_version>
- If you see in the above code I've also added
json_annotation
package dependencies. It is used to annotate the classes. You'll see the use of it further. Let's now go to theUser.dart
file.Importing required packages and Part the User class
- Inside
User.dart
file. class User { User({ required this.name, required this.city, required this.age, }); String name; String city; int age; }
- Let's now import the
json_annotation
package. import 'package:json_annotation/json_annotation.dart';
- Now if you don't know about the
part
in Dart. Basically, withpart
you can split one library into several files and private members are accessible for all code within these files. - So as our generated file have to access all the code within the
User.dart
file we have to usepart
like below. part 'User.g.dart';
Annotating the Class
- After this, the use of this
json_annotation
comes into the picture. To tell the generator that this User class needs the JSON serialization logic to be generated. We need to annotate it by simply annotating the User class by@JsonSerializable()
. @JsonSerializable() class User { User({ required this.name, required this.city, required this.age, }); String name; String city; int age; }
Implementing
toJson
andfromJson
- To generate the
fromJson
andtoJson
for all the properties available in the class. We simply need to write the following code inside the class. @JsonSerializable() class User { User({ required this.name, required this.city, required this.age, }); String name; String city; int age; // Responsible for creating a instance from the json. factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json); // Responsible for converting the map to json. Map<String, dynamic> toJson() => _$UserToJson(this); }
- That's all you have to do in order to create
fromJson
andtoJson
. You will see some error saying this class is not defined, etc. To resolve this, you have run build_runner commands in the terminal. So let's do it.Generating the Code
- As discussed earlier, we can generate the code in two ways, either generate the file once or continuously watch the file and then generate.
- Let's first generate it using the first option. To do that open your terminal and run the below command.
flutter pub run build_runner build
- This triggers a one-time build that goes through the source files, picks the relevant ones, and generates the necessary serialization code for them.
Or flutter pub run build_runner watch
- This will continuously watch for changes. It is safe to start the watcher once and leave it running in the background.
- After running it you will now see the generated file inside the folder.
- And it will look something like below
Using User model Class
- Now to use the
fromJson
andtoJson
, you can simply first decode thejsonString
with the help of thejsonDecode
function. And then you can pass it inside thefromJson
constructor. final userData = jsonDecode(response.body); final user = User.fromJson(userData);
- If you want to know what other things
json_serializable
provides. You can goto the below link:
Disadvantages of Code Generation π
- When you utilize a code generator, your code becomes reliant on it. A code generation tool must be kept up to date. If you created it, you must keep upgrading it; if you are simply using an existing one, you must either hope that someone maintains it or take over yourself.
- Generated code is almost certainly less optimal than hand-written code. Sometimes the difference is minor and insignificant, but if your application requires every bit of performance, code generation may not be the best option for you.
- When you use code generation, the number of files rises and they become dependent on one another.
- If you make any changes to the main class, you must either execute the build command again or use the watch command, which is always running in the background.
Wrapping up
- That's it !! Thank you for taking the time to read this article. π. If you found it useful, please share it with others.
- Also, if you have any suggestions, please leave them in the comments sectionβοΈ.
- See you in the next article. Until then....
Β