Flutter + Source Generation: The birth of a Magical Widget [Part 2]

In the first article, I shared with you my experience as a Flutter developer, and what carried me into creating the magical widget. 
And at the end of the article, I showed you the magic that this widget can perform. With just a simple enum definition, complex UI interactions and state management could be automatically orchestrated.
No need to read the first article in order to understand this one, because this article is the main one that discusses the technicalities of the magical widget and how to use it. However, I urge you to go back and read the last paragraph of the first article entitled Magical Widget. This will show you what this package is capable of, and hopefully will make you read this article with more enthusiasm.

The GitHub repository of the Magical Widget package can be found here. The repository also contains a detailed README file and the code for the example discussed here.


Prerequisites

The magical widget package depends on source_gen to automatically generate code. It also implements the BLoC pattern, so it needs the rxdart package to operate properly.

Therefore after creating your Flutter project, you need to include the following dependencies in your pubspec.yaml file:

dependencies:
rxdart:^0.20.0
magical_widget:1.0.1
 ...
dev_dependencies:
magical_widget_generator:1.0.1
build_runner:^1.1.2

The Annotation

With dependencies out of the way, we can discuss the concept behind the annotation. The magical widget extends the GeneratorForAnnotation class provided by the source_gen package. This means that the build_runner will generate my custom code whenever it encounters a specific annotation.

In our case, the annotation is called Alakazam, and it should be used to annotate an enum. If you try to annotate something else, the code will not generate and Flutter will complain. This annotation takes an optional argument called withProvider, its default value is true. This argument and if set to true, will generate the code for an inherited widget that will help to pass the generated BLoC through the widget tree.

The elements of the annotated enum needs to follow a specific format which is: element_name$element_type$default_value. So basically, three fields exist, and they are separated by a $ sign.
Now the only required field is the first one, element_name. If the other two fields are omitted, then default settings will apply. 
Supported types are String, bool, and num. If the element_type is not present, then the type will be defaulted to String.
The default_value and if not specified depends on the type of the element in question:

  • If it is a String, the default value is an empty string
  • If it is a bool, the default value is false
  • If it is a num, the default value is 0

To start demonstrating, we will structure our project. 
Usually, I like to create a src folder inside the lib folder, and inside the src I create different folders to keep my code logically separated. For our toy example, I have created this structure:

The blocs folder will contain our bloc dart files. The pages will contain the page dart files. And clearly the widgets folder will contain the UI widget of my project.

My main.dart file, contains nothing other than a call to a page in the pages folder. The code of the main.dart is:

import 'package:example/src/pages/mypage.dart';
import 'package:flutter/material.dart';
void main() => runApp(ExampleApp());
class ExampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: MyPage(),
)
);
}}

Now let us go back to the annotation and the enum. Usually, you want to create your file in the blocs folder. So let us create one that is called first_page_bloc.dart.

This file will contain the enum. The enum should contain controls to manipulate your UI widget. These controls are specific to your app, you can add as much controls as you want, the code will be correctly generated. The file will look something like:

import 'package:magical_widget/magical_widget.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:quiver/iterables.dart';
part 'first_page_bloc.g.dart';
@Alakazam() // The default @Alakazam(withProvider=true)
enum firstPageControls {
enableFirstBtn$bool$true,
enableSecondBtn$bool, //this defaults to enableSecondBtn$bool$false
txtField1Input, // this defaults to txtFieldInput$String$
txtField2Input$String$Magic
}

For our toy example, I only want these controls, but I believe that you are already familiar with the idea here. It all depends on your app logic. If you are a bit overwhelmed, keep going and it will all make sense later on.

You may have noticed the imports, they are all required in order for the generated code to properly work. However, if you set withProvider to false, then no inherited widget will be created, and it will be no more necessary to import material.dart.
The part 'your_file_name.g.dart' is required for the build_runner to properly generate the code, if you forget it, don’t expect to see any generated code. Ignore the error when you write this part syntax, it will go away when you generate for the first time.

Now, take a deep look at what we did so far, can you see how simple it is? Our code is just an enum with elements following a specific syntax. OK, that is all you need to do. That is literally all things required on your behalf, in order to have a proper BLoC implementation that will work for you as you want it to.


Generating the Code

Now that we have our annotated enum, we can generate the code by running this command in the terminal (the working directory should be the repository of our flutter project):

flutter packages pub run build_runner build

This command will trigger a process that will search for the Alakazam annotation in our code, then it will create the generated file where the generated codes sit, and finally the process will terminate. So when you do another change to the enum, you need to trigger the process again using this method.

Another command that could generate the code is:

flutter packages pub run build_runner watch

This command will trigger a process that will do just as the one above, however it will not terminate when it finishes. Rather it will sit there listening to any new changes you make to the annotated enums in your project, and then it will reflect the changes directly. So basically you run this command one time regardless of the changes you make, unlike the previous command.

After the generation, we will have another dart file, with the .g.dart extension, next to our original one.


The Generated Code

Before discussing the generated code, just a few words about BLoC for those who are new to it. The BLoC pattern is a reactive programming pattern that will let you handle events and interactions in a 100% reactive and asynchronous manner. I can’t really teach the BLoC pattern or its fundamentals, but for what it matters, when you are using the magical widget package, you wont care much. The BLoC code will be automatically generated, and you only need to know how to employ it in your widgets (usually through a StreamBuilder). 
In the simplest form, you can think of a BLoC as a stream of events. Observers (like your widgets) listen to this stream, and whenever a new event is added to the stream, the observers are notified and they can react accordingly and asynchronously. Just this bit about BLoC is enough to use the magical widget.

Now, If you go to the generated file, you will see that it first creates an enum. The name of the enum is the same as your original enum but prefixed with MAGICAL_. This generated enum will be used later on to tell the package what control you want to change.

enum MAGICAL_firstPageControls {
txtField1Input,
enableFirstBtn,
enableSecondBtn,
txtField2Input,
}

As you can see, the generated enum is deduced directly from the enum we entered at the beginning. It has the same elements but without the type and value information.

Other than the enum, you will always find a class called MagicalController. This class is the type of the events that will fly through the stream. The generated code for this class contains a lot of boilerplate code, and you will not need to explicitly use this class. 
You will only reference it when you will use the StreamBuilder widget, and you can use its empty constructor MagicalWidget() if you ever want to employ the initialData property of the StreamBuilder. 
Basically, a StreamBuilder is the main flutter widget that you will employ whenever you want to use the BLoC pattern for state management, and the initialData property is used to initialize your UI widgets that depend on the stream, in case the stream is still empty.

Next, we have the main class which is the MagicalBloc. This class contains the BehavioSubject that you will use to manipulate your UI widgets asynchronously. If you don’t know what a behavior subject is, don’t worry, with the magical widget package it doesn’t really matter. You can just think of it as the stream that will contain our MagicalController events.

In the MagicalBloc class, there are two properties and two methods that you will want to use.

  1. Property 1 is called magicalValue, and it gives you access to the current value of the stream
  2. Propery 2 is called magicalStream, and it lets you reference the stream. You will only want to use this property to fill the stream property of the StreamBuilder.
  3. Method 1 is called changeUIElement(value, control). Through this method, you can change any control you initially created in your enum. The controls are basically the elements of the generated enum that is prefixed with MAGICAL_
  4. Method 2 is called changeUIElements(values, controls). It is the same as the method above, but it lets you submit a list of values and controls to change your controls in batches. The size of the values list needs to equal the size of the controls list, otherwise an exception will be thrown.

The two methods above are the most important API of the magical widget package. These will let you change anything you want by using the same method over and over. And since we are using the BLoC pattern, the changes will be automatically and asynchronously reflected in your UI widgets. It is just like magic 😉.

At this point, you are ready to take of, the BLoC class is available, you can just instantiate it and use it as you wish in your widgets.

Before showing the example code, we need to cover one last case. Remember the withProvider argument? if it is set to false, then no further code will be generated. However, if it is set to true, then an inherited widget will be generated for you.
The inherited widget is called MagicalWidget, and it contains a MagicalBloc called magicalBloc as an instance variable. The MagicalWidget is created to your convenience so you could easily propagate the magicalBloc in the widget tree. You can always get this BLoC by calling the static of() method of the InheritedWidget class.

Take a look back at the enum that we just written, it is only a simple enum, yet it took us many lines to discuss what it generated. So, we agree it is simple, but it is powerful. Now, we will continue the example to demonstrate this power.

In the pages folder, I only have one dart file called mypage.dart, it contains the following:

import 'package:example/src/widgets/Button_one.dart';
import 'package:example/src/widgets/button_two.dart';
import'package:example/src/widgets/txt_field_one.dart';
import'package:example/src/widgets/txt_field_two.dart'; import'package:flutter/material.dart';
import 'package:example/src/blocs/first_page_bloc.dart' as first_bloc;
class MyPage extends StatelessWidget{
@override
Widget build(BuildContext context) {
return first_block.MagicalWidget(
child: Column(
mainAxisAlignment:MainAxisAlignment.spaceEvenly,
crossAxisAlignment:CrossAxisAlignment.center,
children[
BtnOneWidget(),
BtnTwoWidget(),
TxtFieldOneWidget(),
TxtFieldTwoWidget(),]
));
}}

This file contains a column, which in turn contains the different widgets. These widgets exist in the widgets folder as shown in the picture. 
The main thing to notice in this file is that we wrapped the column with the automatically created MagicalWidget. Now all our widgets within this tree will have access to the MagicalBloc provided by this MagicalWidget, as we will see next.
Also pay attention to the import of the first_page_bloc.dart, I gave it an alias, and this is a good practice to follow. The magical widget package will always create classes with the same name (they are MagicalController, MagicalBloc, and MagicalWidget), so aliases is the only way to differentiate between them in case you imported more than one magical widget into the same file.

Now we develop our application logic. 
Going back to our enum definition, we want button one to be enabled by default, button two to be disabled, text field one to have empty string, and text field two to have the word Magic by default. We also want:

  • When we click on button one we enable button two
  • When we click on button two we change the text of input field one
  • The text of button two is what we write in input field two
  • When we write enable in text field one button one is enabled but if we write disable then it is disabled

These are just dummy interactions to demonstrate the package. The state and the interactions will depend on your specific app.
Even thought, these are dummy and simple, but good luck writing the code from the start. If you use setState() your code will easily become gibberish, and if you use any other state management technique, and want to write everything from the beginning, then you have a lot to do.
Let’s use the magical widget in our case, and let it do the trick. Remember, we only wrote a simple enum for our BLoC implementation, nothing else.

In Flutter, whenever we want to declare our widget as a listener to a stream, we use the StreamBuilder widget, and this way, if something changed in the stream our widget will be updated. 
So let’s use StreamBuilder to declare our widgets as listeners on specific controls, and let’s change these controls as we stated above:

btn_one.dart

Look at the code of button one: First as the good practice states, I imported first_page_bloc.dart with an alias called first_bloc. Second in the overridden build method we get the MagicalBloc instance that is provided by the MagicalWidget. You almost always want to do this to get a reference to this instance. 
We can do this because we wrapped our main column widget with a MagicalWidget in first_page.dart, you can recheck it above.
Next we want to return a button, but this button is enabled or disabled according to our enabbleFirstBtn control. So this button is dependent on the stream and therefore we wrap it with a StreamBuilder
The generic type provided to this builder as you can see is first_bloc.MagicalController. This tells the builder that the events in the stream are of this type. This is done on this line return StreamBuilder<first_bloc.MagicalController>(...);.
You need to provide a stream and a builder property for the StreamBuilder, initialData is optional. However, you can use initialData and call an empty MagicalController constructor to initialize the state of your widgets.
The stream property is always bloc.magicalStream, we are just referencing the stream of MagicalController.
Then your custom logic is written in the builder property. You need to provide a function to this property. This function takes two parameters (context, snapshot) and returns a widget. context is apparently the context of the page, and snapshot gives you access to the current event in the stream through its data property (so snapshot.data will return the current MagicalController in the stream).

In our code, we are getting the enableFirstBtn field from the MagicalController event, and we are using it on the onPressed property of the RaisedButton. If enableFirstBtn is true, then we assign _onPressedFirstBtn to onPressed, otherwise we assign it null so it will disabled.
The _onPressedFirstBtn, and as you can see, sets the enableSecondBtn control to true, using the changeUIElement method. 
That’s it, now any change in enableFirstBtn will update the state of this widget immediately because it is wrapped in a StreamBuilder and the BLoC pattern is already implemented for you on the other side. No need to add any other code.

I will add the code for the other widgets, and let you understand them. I bet it is not that hard right now. It is just a logical flow of events, as stated by our requirement above.

button_two.dart

The second button depends on the stream to set it to enabled or disabled, but also to change its shown text. When button two is pressed, it changes the text of input field one.

txt_field_two.dart

The code for the first text field is shown above. Basically, its text depends on the control txtField1Input. This control is changed in the button_two.dart when we press the button.
Then we test the entered text here, and if it is ‘disable’ then we disable button one, else if it is ‘enable’ then we enable button one. And I am showing how to use changeUIElements as well to change more than one controls at a time.

txt_field_two.dart

I didn’t wrap the text field with a StreamBuilder, because its state depends on no changes coming from interactions with other widgets. And if you noticed I used the magicValue variable to reference the current value of the stream, this is only useful when we first build the widget to have the default value which is ‘Magic’ as stated by our original enum.

Now if we run the app, and as expected we will have this screen:

You can see that everything is as expected. Every widget has it default value that we specified in our original enum.

The first button is enabled by default. The second one is disabled and its text equals to the content of the second text field, which is ‘Magic’.

The first text field is empty, and as expected, the second text field is defaulted to ‘Magic’.

Now, and if you implemented the code all along, try to play with these widgets on your device or emulator and see if the interactions are as expected.

Check the GIF below, to see these interactions in action.

I hope this article gave you an idea about what you could do with the magical widget package. With just a simple enum, you can now manipulate your UI widgets and manage their states with ease.

The number of controls in the enum depends on the complexity of your app, but you should add as much as you want, don’t use one control for two things for example, after all you are not writing the code yourself.

The type of interactions that you could achieve is really limitless, it is up to you what controls to include and how to use them.

At the end, I want to say that this package saved me a lot of time developing my app, and it will save you time too. And if you didn’t see the first article, then go back and just check the last paragraph, to see how much a simple enum could offer you in a real app.

If you missed part one, you can read it here: