CRAIG LABENZ: Hello, everyone,
and welcome to another episode of "Observable Flutter." I am your host, Craig
Labenz, and today, we've got a pretty exciting
episode in store with a great guest and a
really rich topic, I think. So let's get going here. First of all, code of conduct. Just remember, folks, this
is the Flutter community. Want to treat each
other warmly, but also treat adjacent
technologies warmly. Treat patterns that
we don't particularly use ourselves warmly, all
that good kind of stuff. Now, today's guest should need
not a ton of introduction, but all the same, I will still
let her introduce herself. Anna Leushchenko is a
Flutter and Dart GDE-- extremely prolific,
a lot of great talks at different events. And, Anna, I am so
excited to have you. Welcome to "Observable Flutter." ANNA LEUSHCHENKO: Hi, everyone. Thanks, Craig. CRAIG LABENZ: Would you
like to say a few words and introduce yourself? I know you've
prepared this slide. ANNA LEUSHCHENKO: Sure. I'm Anna Leushchenko. I'm from Ukraine. I'm Google Developer
Expert in Dart and Flutter. And I've been with Flutter since
the first stable release, which makes it over five
years now, I think. I've been using it for pet
projects, but also production enterprise applications. And one topic I've been
really applying over and over again is code generation. And I'm here today to
share my experience with using this practice,
demonstrate a few generation packages, but also
sharing advice on the efficient maintenance
setup for a project that heavily rely on code generation. Today, we will have a
live coding session. So I have prepared a
repository with the source code in case anybody wants
to check it immediately. Otherwise, be sure to check
it later because definitely, today, we will not be able to
cover everything I have there. I have two folders here. The same application
is implemented without code generation but
also with code generation. And here, I have a
source code from the without_code_generation folder. And today, we'll
be refactoring it to using some code
generating packages. But first, let me show you the
application that we have here. This one displays a
list of spaceflight news from the publicly available API. And what you need
to know about it-- and we'll dive into the
details of the implementation later as we go on. But basically, it's a two-pager. The first page displays
a list of articles. And once you tap
on any of those, it will display article details. And some articles may
also contain a link to the space launch this
article is about, in which case, we can review a list of all news
related to this space launch. And here is the Flutter project. Currently, it doesn't have any
code-generating dependency. It's just there for API
requests, Flutter block for state management, get_it
for dependency injection, and basically that's it. So I will-- yeah? CRAIG LABENZ: One
question for you, Anna, as we dive into this-- I wonder if we should spend
a second talking about what code generation is and
the philosophy behind why you would use it. I just was talking with
someone on Twitter yesterday, and they had-- they expressed a kind
of blanket hesitation to use any code generation. And we're talking about
this fear of loss of control and having all of
this code in their app that they didn't write. And that's a sentiment
that I can honestly relate to as I think back to
earlier stages of my career. So I wonder, How do you think
about code generation just as a tool and what
are its tradeoffs, and why do you like it? ANNA LEUSHCHENKO: Absolutely. As we will see through
today's live coding, code generation is
a tool that allows you to write a minimum code
with the help of annotations, a special syntax, then run
a code generation command and have some code
generated for you. And, in most cases, I
would say it's similar or at least it
does the same thing as if you were solving
the same tasks by writing the code manually. And I can understand this fear. However, the code
that gets generated is generated in your project. You can definitely read through
it, understand what it does. And if the code
that is generated is not perfectly suited to
your needs, you can either-- what would I do-- first of
all, read the documentation because most probably there
are already some customizations created for the purpose. And you just have to
adjust it a little bit. Or, well, it's open source. You can always open Dart with
some additions to the project. So my goal here
is, among others, today to remove this
fear because we'll explore some code
that is being created. It's readable. It's all the same Dart
code that probably you would be more or less
writing yourself. CRAIG LABENZ: Yeah, I
like to think of it-- that's a great answer-- the way it accelerates your
and anyone's development, you can just blast
yourself into this space of a really rich solution
that would have taken forever to type up yourself. I also think of code
generation, sometimes, as algorithmic
compression, where you can compress an image. You can zip a file or whatnot
and then decompress it later. And this is almost like the
compression of an algorithm, where the code generation part-- like, whoever wrote
the code generator, they've written the
ability to compress the algorithm down into
just the little annotation that you write yourself. And Anna is going to
show us a bunch of that. And then, when you run
the code generator, that decompresses that minimum
annotation that you wrote-- the very simple
class, the whatever-- and decompresses it back
into the full algorithm. And then you get all of that. But it's tailored to the
specific needs that you had, like the fields on
your data classes or the API requests
that you'll make. I don't know if that's a
helpful thought for anyone else, but it kind of tickles my brain. So, Anna, back to you. ANNA LEUSHCHENKO: To add to
what you said, I completely agree, and that's exactly what
I value about code generation. It enables me to focus on what
I want to do and not how exactly because throwing just a bunch
of annotations in a class gets me so much done. And this way, it facilitates
the creation of software. But also, because there is
a mechanism of regenerating the code, whenever I change
the notation, this way, I can easily update the
code and not forget about, hey, I added this
field here, but I forgot to mention it in
some list and so on, which potentially causes bugs later. CRAIG LABENZ: Yeah, absolutely. Shall we dive in? ANNA LEUSHCHENKO: Yeah,
I was about to show some code, as it is now
without using code generation. And then it's easier
to compare with what it will look like later. But I'm afraid it's impossible
to do without some context about overall the
project architecture. So very high level,
very briefly, the app consists of two pages. One is ArticlesListPage,
and somewhere down the road, it has a BlocProvider. And it uses a bloc
for state management. And here, we see, when I
create a bloc, I have this-- I call the container
of this type of bloc. And diContainer
here is just a name I like to give to a
global GetIt instance. And this GetIt instance is
here not for state management, not for persisting any objects. Its only purpose is
to allow me to create objects that have
a lot of parameters with one line of code. And what I want you to
remember about this bloc-- basically, it injects
an API object, which is another class
responsible for implementing our APIs. And this API class
operates on a few objects-- plain old Dart
models, like Article. And Article also may contain
a list of SpaceLaunch. So just a couple of Dart
objects, a couple of classes that do some stuff,
and you may immediately see that some layers of the
architecture we probably are used to are
missing, like repository between bloc and API, or
data transfer objects. And disclaimer-- this
project is not here to demonstrate all the best
practices of developing Flutter applications, but its
purpose is to highlight the benefits of
applying code generation in a day-to-day development. So I have just dropped
some things that are not helping with this purpose. CRAIG LABENZ: Yeah, I
think that's totally fair. Also make it easier to focus
on the parts where we will-- or where you will
add code generation. ANNA LEUSHCHENKO: Exactly. So now I'd like to
focus on the first piece of the refactoring
we will be performing is with this Article model. Let's check its implementation. We see a list of fields. We see a manual
implementation of fromJson. We also see equality operator,
a hashcode, and a useful method copyWith. And this hashcode and equal-- operator equals were generated
for me by Android Studio. It's a pretty typical
implementation. And there are a
couple of problems with this implementation. First of all, whenever I add
a new field to this object, I have to remember to
update all of these methods. Android Studio will no
longer update or maintain these methods for me. Also, as you see here,
this comparison-- the way it was generated, it
uses just operator equals, which for object
launches, which is a list, would mean that lists
would be compared whether two references are
pointing to the same object, not whether two lists
contains the same data. And also, this class is
missing a toString method implementation, which
means, by default, calling toString on this object
would just produce instance of article, which is not very
informative in case you want to see what fields
it exactly had, which is primarily
useful for tests if you try to
compare two articles. The test output would say
you had instance of article, but you expected an
instance of article instead of displaying all the
fields, which would help you easily identify the difference. And also, this manual
copy-- this implementation is pretty typical. However, this way, it
would be impossible-- CRAIG LABENZ: You
can't set null, right? ANNA LEUSHCHENKO: Yeah, exactly. Some of the fields are nullable. You can't update
its values to null. CRAIG LABENZ: And
the reason for that-- let's say you wanted
to copy an article, and you wanted to pass
null as the image. Well, this method
couldn't distinguish that from if you just never
passed a value at all. So on line 82, it would be
like, the image parameter? Nope? All right, we'll
keep this dot image. So you cannot zero out
the field via copyWith. ANNA LEUSHCHENKO: Exactly. So for this object, I definitely
prefer using code generation, and the solution is to
use the freeze package. And I will be
shamelessly copy-pasting the code from another
project I have in that repository,
which is implemented with code generation. But for demonstration
purposes, we'll do it together. So, first of all, I
add freezed_annotation somewhere here, and
also add build_runner. And build_runner
is a z package that enables all of this
mechanism of code generation, so it definitely has
to be a dependency. And finally, the freezed. CRAIG LABENZ: And can
you say a little bit about the difference between
freezed and freezed_annotation? ANNA LEUSHCHENKO: Absolutely. So it's a pretty typical pattern
for code generating packages to consist of two, and
the reason is, typically, you would have to
add annotations to your production code, like
we'll see just in a minute. I would annotate
the article object, which means these annotations
have to be among dependencies. But the generator
mechanism doesn't have to be a part
of your application. It just sits in the dev
dependencies, which means it doesn't get into the build. It just provides some
generators for build_runner to execute once I run the
code generating command. And here, I have a first advice. As you see, I'm using
versions which are-- I'm specifying exact versions
of code generating packages as opposed to specifying the
range with special symbol. And the reason for this
is that for this package, it won't really matter because
I have a log file committed. But if you are developing
a package for which the recommendation is
not to commit log files, you may end up in
the situation where your colleague would
pull the same code base on their machine. They would do pubget,
and your version of a code-generating
package may be downloaded, which generates code
slightly differently. And so whenever they generate
a code, it gets updated. Then you generate the code with
older version, it gets updated, and you would have this back
and forth without any reason-- CRAIG LABENZ: Or for
the rest of time. ANNA LEUSHCHENKO: Yeah. So now we can focus on actually
refactoring the model Article. CRAIG LABENZ: This is where you
add copying and pasting your-- code to paste? ANNA LEUSHCHENKO: Yeah. Unfortunately, it's
a bit granular, so we'll have to
do it one by one. First of all, annotate the
class with freezed_annotation. Add a proper import. And then I also have to
declare a part of this class. This is where the code
will be generated, so for now, we don't
have this file. We have compilation errors. And because freezed
requires a special syntax, this is where I will
just copy-paste. CRAIG LABENZ: Yeah,
the syntax for freeze does feel a little arcane the
first time you encounter it. And it's gotten less arcane
over the years, but yeah, there is still-- it does still feel
a little funky. ANNA LEUSHCHENKO:
Yeah, that's right. We'll get back to
this in a second. I will, so far, remove the
declaration of the fields. I will also remove
all the equals hash and copyWith implementations
because we no longer need them. It's a bit of a spoiler-- sorry. All right, so this is
now a freezed model. It consists of part
file freezed_annotation, and a special syntax for
mixing that will be generated. And we list all of the
files, all the properties in constructor, and it
will be generated for us. I think I can immediately do
the same exercise for launch object. So Dart-- CRAIG LABENZ: That's
a simple class. ANNA LEUSHCHENKO: Yeah. Import freezed, and then
compute the transformation. CRAIG LABENZ: Oh, we just-- we had a couple of good
questions that I've queued up, but we've got one
right now that I think we should cover immediately. Will the generated code be
submitted to version control? ANNA LEUSHCHENKO: Oh, I
have the entire section dedicated to this question. We can indeed
discuss it right now. As you will see
later, indeed, we will have quite a number
of generated files. And let's do a
thought experiment. Imagine we don't commit
them to source control because, hey, we didn't
write them manually. Whenever you pull the source
code in a new machine-- let's say, on CI-- it's not compilable. You have to generate
all the code, which can be seen as a good
thing, as a bad thing, because the good thing--
you have to regenerate it. It will definitely be the
freshest of the fresh, and it will correspond to the
configuration you have here. The downside is it takes
time, and depending on the size of your
project and depending on how many packages
you have in the project, that can be a lot of time. I don't want to
scare anybody, but I work in a project that
currently has over 300 packages. And if we run code
generation on all of them, it takes about two
hours to complete. Another downside of
not committing them once you have generated them--
imagine you work in a team, and later on, someone pushes
an update to the configuration, like, any of these files. And either you have to
regenerate the code every time you pull from main,
which takes time, or you have to really
look through the changes and identify packages
in which those changes happen and regenerate
the code only there. So on the other hand,
you can commit them to the source code, which will
increase the size of your code base and, also, sometimes may
cause a situation where someone forgot to commit the
regenerated files. But from my experience,
in a couple of months, the team will learn
to have this attitude and to always commit
generated files, and it will be only like once
in a couple of months someone may forget to commit
updated files. One more downside to
committing files is-- we'll see later today-- there are some packages
that generate code in a single file per
package, and this file will be the one that
is prone to having merge conflicts the most. And the solution to this is
simply pull the latest changes and run code generation
in that package again. Anyways, with those
downsides mentioned, I still believe
that committing code generated files is the
way to go just because it saves so much time on CI on
your local machines everywhere. And your project is almost
always in the compilable state if you do commit them. CRAIG LABENZ: For what
it's worth, I agree. I'd say yes on committing
the generated code. ANNA LEUSHCHENKO: So it's time
to run the code generation. dart build-- nope-- run build_runner build
--delete-conflicting-- CRAIG LABENZ: That sounds
like a haiku or something. ANNA LEUSHCHENKO: [LAUGHS] Yeah. CRAIG LABENZ: Or
like spoken word. Dart, run! ANNA LEUSHCHENKO: Yeah. So now we are about to run
the code generation with this build parameter, which
is running the code generation only once. And later, we'll see, it can
also run it in the watch mode, acting like a
daemon, sitting there and watching the changes
to the file system and updating accordingly. CRAIG LABENZ: So my
question for you now, Anna, is do you actually
type out that command, or do you use Control-R? ANNA LEUSHCHENKO:
That's actually where I get jokes
out [LAUGHS] of me because I use aliases for this. And mine, I believe it's
pretty straightforward, but it's dart-- no, it's dart, run,
build, runner, build. And it's a fun abbreviation. CRAIG LABENZ: Ah. Yeah. I want to know what DTR is. BRB is obvious, be right back. Maybe that's what you're going
to be right back while the code generation runs. ANNA LEUSHCHENKO: Exactly. CRAIG LABENZ: [LAUGHS]
So this is great. ANNA LEUSHCHENKO: So we
see that we no longer have code compilation errors
because our files were generated. And we can actually
check the generated code. It's quite a bit. But once again, as I said,
many code generating packages, including freezed,
provide some type of a way to modify
the generated code, eliminate parts
that you don't need. Here I have the most simple
example with no configuration. And what we have here is the
same operator equal method I used to have written manually. But it doesn't have this
downside of comparing lists by reference. It properly uses
DeepCollectionEquality objects. It will look in the lists'
content and compare those. We also have a
toString object method overridden to actually output
the values of the fields. And, also, the copyWith method
I used to write manually previously is implemented here. And, as we see for
nullable objects, the default value is provided,
which means when we actually provide it as null, it would
be treated as a provided value and will be preferred
over the default one. I can actually-- CRAIG LABENZ: Yeah, that's a
huge, huge win right there. ANNA LEUSHCHENKO: Yeah. I'm rerunning the app just
to demonstrate that it keeps working as it used to work. And I want to point out here
that my generated files are nicely displayed
under the source file. And this is not the
default behavior. However, it can be configured
in Android Studio, or IntelliJ, under File Nesting. Give me a sec. Using this-- [INTERPOSING VOICES] CRAIG LABENZ: I love this
game, trying to find the-- get your cursor
position just right. [LAUGHTER] ANNA LEUSHCHENKO: Never mind. Here we see, in a few files,
the file extensions were here already. But I also have a freezed
and .g, and .gr, and .config, because all of those are the
types of generated files I get when I use the full list of
code generating packages. And, in VS Code, there
is a configuration to simply hide these files
from the Project Explorer. CRAIG LABENZ: Yeah,
that is a nice-- I think IntelliJ
probably gets the win here for that particular
feature, which I say as a VS Code Stan, but
point to IntelliJ on this. [LAUGHTER] ANNA LEUSHCHENKO: No, for
me, IntelliJ wins almost every time. CRAIG LABENZ: Oh,
someone's saying, VS Code also has file nesting. I was only aware of file-- oops, I double-clicked that. I was only aware of file-- ANNA LEUSHCHENKO: I
think it's a plugin. CRAIG LABENZ: --hiding. But apparently it has it. ANNA LEUSHCHENKO: All right. freezed, by the way, also
have other interesting cases, which I typically use for
block states and events. Here I won't
demonstrate it for the-- being mindful about the time. But, in Dart, we now
have sealed classes, which provide almost the same. We can have exhaustive
switch over the children of these classes. But freezed package
offers the functionality of unions which
does the same thing. But, also, for every
class it works with, it also generates equals,
hashcode, toString. So those things are
important for me because if, for example, I'm
writing unit tests for a block, I want to compare its current
state to some expected result. It's very beneficial to have
equals operator overridden on states. But to keep on the topic
of improving our code base, now I want to deal with
this fromJson method. What I would like to
highlight immediately here is, first, our ID is String. But in the API, it's int. And it's a bit
artificial example. But it's not a
rare case where you would like to change the
type of the object returned from the API, especially if
you don't have separate domain models and data
transfer objects, which, with all honesty, I
always have only one set of models, domain models. And I just use a set of
converters and other techniques to convert the API model
right to the domain model without having a
separate set of models and the mappers between them. CRAIG LABENZ: I
do the same, yeah. It just feels so tedious
to have the data transfer layer when, yeah, these
JSON converters can do it. ANNA LEUSHCHENKO:
And another thing is image field has a name
of the JSON key different. All the rest of keys
match the property name. It's important
because we'll see how we need to address those when
we use code generation for this. Also, our image
field is of type Uri. So when you're
using Uri.tryParse-- we also have a DateTime,
and we have DateTime.parse. And for the array,
the launches array, we transform a JSON
property to a list and then call it from JSON
in each item of this list. So for transforming
this into less code, I'm going to use
json_serializable. And, once again, I'm
copy pasting some code. CRAIG LABENZ: The
one interesting thing here, while you're doing this-- and feel free to ignore
me while I pontificate-- you just showed
us a toJson method that had a lot of custom stuff. I think that's why
you were spending time on the type conversion
for the ID field and the name conversion
for image to image URL. Because that kind of stuff
is pretty unavoidable and may feel like, well,
how is code generation going to do that? And those are
reasonable hesitations that one might have. But, Anna, I have
a feeling you're about to show us how
code generation can still handle this very bespoke
specific toJson method that you had. ANNA LEUSHCHENKO: Exactly. And, for this purpose,
I have referenced JSON annotation
among dependencies and json_serializable
among dev dependencies-- once again, two packages. One contains annotations. Another contains the generator. And, on its own without
freezed, json_serializable has specific way of using it. It is described on the
official documentation. But because I'm using
freezed, and freezed already thought of these scenarios,
it's actually even more easy. So what I do here is I
remove this manual from JSON. CRAIG LABENZ: See you later. ANNA LEUSHCHENKO: [LAUGHS] I
have to add in the g.dart file, because that's where the JSON
serializable package generates its code and to bring back
some of this custom logic we used to have. So here I am applying
the converter to id. And we'll look at
it in a second. And here I'm also changing
the key of the JSON in where the value would
be looked for, this URL. By default, JSON serializable
will use the names of the properties as JSON keys. CRAIG LABENZ: Oh, I
think you put that on-- I think you meant to put that
on line 14 instead of 13. ANNA LEUSHCHENKO: Yes. You are right. Thank you. CRAIG LABENZ: Mm-hmm. ANNA LEUSHCHENKO: So
let's check the converter. Before, it was just a
class list from JSON into JSON, which conveniently
almost matched the [LAUGHS] way that json_serializable
converters have to be implemented. So now my JSON converter has to
implement this JsonConverter. And it specifies the types of-- go away-- this is the type
of the value in the JSON int, and then string is
what we want it to be. And now I have to just add
the override annotation to all of its methods. CRAIG LABENZ: Yeah, I love
the JsonConverter API. It is just the most
straightforward thing in the world. And then freezed simply uses it. ANNA LEUSHCHENKO: Yeah. And the last bit I have to
do is to declare a factory constructor called
from JSON with, again, special syntax, which
we have to get used to. CRAIG LABENZ: Yeah,
this is one of those that I always just
have to look it up. I've never remembered
what to type here at any point in my life. [LAUGHTER] ANNA LEUSHCHENKO: I'd like to
point out this problem here. So we have a warning saying that
the notation JsonKey can only be used on fields or getters. And this comes from
json_serializable, which is unaware of freezed syntax. And, thus, the only way I could
think of getting rid of this is actually configuring
the analyzer to ignore this kind of warning. CRAIG LABENZ: Telling
it to shut up. [LAUGHS] Now, you be quiet. ANNA LEUSHCHENKO: No, just a bit
for invalid annotation target. CRAIG LABENZ: Oh, interesting. Yeah. Now you can put annotations
wherever you want. [LAUGHS] ANNA LEUSHCHENKO:
It won't do much if it's not applied properly. So now I will run
the code generation in watch mode, which
stands for dart, run, build, runner, watch-- little conflicting outputs. CRAIG LABENZ: Naturally. ANNA LEUSHCHENKO: Yeah. CRAIG LABENZ: Obviously,
that's what that stood for. [LAUGHS] I'm excited to see. ANNA LEUSHCHENKO: Oh. Yeah. This is all planned for
demonstration purposes, of course. CRAIG LABENZ: Brilliant. ANNA LEUSHCHENKO: So the
problem is that now we're trying to generate the
code to convert the article object to and from JSON. But it doesn't know how
to convert SpaceLaunch to JSON because our
SpaceLaunch object only has fromJson method, not toJson. So I have to do the same
exercise for the launch object as well-- so dart and a constructor. Yeah. I save. Code generation is
running in watch mode. Let's see. Now it succeeded. And-- come on-- I now have another pair
of files generated for me. So I'd like to explore
this generated code. We see here the
fromJson implementation, which looks pretty much the same
as we used to have it before. It applies converter
to the ID field. It uses properties'
names as JSON keys. But, for image, it
changed the image URL. It knows about Uri
type and DateTime type and knows how to
properly convert them. And, also, for launches list,
it immediately converts it to a list and calls
from JSON on every item. So almost the same as we
were typing it previously. And we also have the
toJson message generated. It can be configured that
this method is not generated. But for most parts,
it looks almost as if we were writing it manually. It knows about URL. It knows how to convert
date back to string. However, here we
have a misalignment. The launches field is a list. But, here, it's put into
the JSON as a plain object. So instead of taking
each item and converting it to JSON and put
it into a JSON list, it just puts it
as a plain object. And there is a configuration
to JSON serializable for that. Now I will have to
create a build_yaml file in the root of my project. CRAIG LABENZ: This is where
we start to really centralize your configuration. ANNA LEUSHCHENKO: Exactly. So this file is serving the
configuration for all code generated packages. We'll see later
what can go there. For now, I'm only pasting
a single configuration. So what will fix the
problem with launches array for us is this
explicit_to_json set to true. And this configuration
can be applied on top of every model
with annotations. But, to me, it makes the most
sense to configure it once for the entire project. And another configuration
I like to always put for json_serializable
is to not include fields that are null when we
are converting this object back to JSON. So because our code generation
is still running in watch mode, we can immediately see
the changes in article.g. First of all, we see that
now launches is mapped, and every item is
converted with this toJson. And because I have set this
non-null configuration, there is a simple
writeNotNull declared. So it only puts
values into array when there is something to put. And this is how all nullable
fields are put into the out-- the final JSON. CRAIG LABENZ: And, of course,
the decision to do this is probably dependent
on what your API you're talking to expects. Some APIs might reject
requests that omit fields, even if the value
would have been null. And so, for those APIs,
you wouldn't do this. ANNA LEUSHCHENKO: Yeah. CRAIG LABENZ: But if
you can, then you'll send less data on the wire
and this would be better. ANNA LEUSHCHENKO: So since we're
looking at build_yaml here, I'd like to also
demonstrate a use of generate for configuration. So the way the build_yaml-- the build runner works, and
the code generation in general, is it takes all of the Dart
files in your package that matches the file name-- most
probably it's still all Dart, but files-- and it puts them individually
through a generator which is provided by
code generating packages. Which means if we limit
the number of input files by using generate_for, we
get faster code generation. So it supports some
kind of wild cards for-- for me, it's quite useful
because I have my models only listed in domain model folder. And what I find it
particularly useful for is it enforces the package structure. If you put a model
that you annotate with json_serializable
outside of this folder, the code will not be generated,
and it will be one more reason to think whether you're putting
the model in the right place. CRAIG LABENZ: Yeah, you'll
be reminded very promptly you've done it wrong. [LAUGHS] ANNA LEUSHCHENKO: Yeah. And, once again, I'm
rerunning the application just to demonstrate that it keeps
working as expected and parses all the values properly. CRAIG LABENZ: Nice. We deleted a ton of-- yeah, I counted. There was six or seven places
in the original model definition for Article where, if
a new field was added, there would be six or
seven places where you had to remember to make an update. And now, that number
is down to just one. ANNA LEUSHCHENKO: Yeah,
just a list of fields, without which you won't be
able to develop, actually. CRAIG LABENZ: Nice. ANNA LEUSHCHENKO: So we are
done with optimizing our models. We looked at freezed
and json_serializable. Maybe there are
related questions. Otherwise, we would move on
to the next demonstration. CRAIG LABENZ: A lot
of general stuff. I guess this question
from 20 minutes ago relates to the thing
you were just getting at with folder structure. And it does seem
like you are maybe on team clean architecture. Is that right? ANNA LEUSHCHENKO:
Not to the book. [LAUGHS] I do apply some of the
clean architecture principles. But I am not in
favor of doing things just because it's recommended
but it doesn't make sense in Dart. For example, in
Dart, every class can be used as an
interface declaration. So there is no point in
declaring abstract classes and then having a separate
implementation class. You can easily use those
classes directly in production. And when you are writing
tests and mocking them, this exact class can
be used with implements keyword to mock its behavior. So somewhere in
between, but definitely solid and other good principles
of software engineering. CRAIG LABENZ: Great. Yeah. Oops. I just hit my mic. I don't follow it letter
to the law either, but try to get some
of its good concepts. Next, code generator. What do you have
in store for us? ANNA LEUSHCHENKO: Yeah, next
I'd like to refactor and improve this API class. And, honestly, API is
such a great topic. I like it so much. On its own, I could
have hours and hours of demonstrating all
the nice tricks you can do with what I'm about to
show, but on a deeper level. So here is the class responsible
for performing those API requests. And we have a few methods-- getArticles, a list of articles,
getArticle by articleId, and getArticles list by
specifying the launch ID, which corresponds to the
request when we search for related articles that
describe a certain SpaceFlight launch. And if you take a
closer look, all of them look pretty much the same. They perform a get
operation on the object. dio is a wrapper over HTTP
client with some friendly API. And in case of getArticles,
it fetches a list. And every item of
this list is then converted with the
help of fromJson call to an article object. For getArticle,
it expects a map, which is the JSON for
a certain article. So we immediately call
Article.fromJson on it. And this is some repeated code,
which we can significantly reuse. The few things I'd
like to point out here is the way the
SpaceFlightNewsAPI injects the API object on which
it has to do the calls. But, also, it allows to
override the base URL. And the way I use it is the
provided base URL is preferred. If it's not provided, we
use base URL from dio. It is sometimes
useful for scenarios where you have the same
base URL but your API provides different versions. And, for some versions, the base
always has some suffix to it, and it's easier to put
it inside the base URL. So not a common
scenario, but something I really used in my
production development. So for getting rid of
all this repeated code, I'm going to use another
package called retrofit. CRAIG LABENZ: And is retrofit
used specifically for dio, or is it a replacement for dio? ANNA LEUSHCHENKO: No, no, no. Retrofit is a code generator
that works with dio. CRAIG LABENZ: I see. ANNA LEUSHCHENKO:
Likewise, if you would not use dio and use
HTTP clients, there is a code generator called
chopper as an alternative. CRAIG LABENZ: Oh,
right, because dio is just the really low-level
network [? Duff, ?] and retrofit is your domain
wrappers around it, right? ANNA LEUSHCHENKO: Yeah. So here I included
retrofit_generator in dev dependencies, retrofit
in among dependencies. And we notice that
code generation stopped running, even though
it was running in watch mode. And the reason for that is
that the dependency graph has changed. I will have to just
rerun it again. So to improve my API
class, first of all, I add the annotation
because that's how code generation works. [DOG HOWLING] RestApi annotation. Our class now becomes abstract. Ah, of course, dart file,
that's where our code will be generated later. And I hope I'm
making this right. CRAIG LABENZ: It's so easy. I mean, the one tricky
part about code generation is this phase where
you have to remember for each package what are
the funky things that you type this time. And when I add a new code
generator to a package, there's a few minutes
of, Why isn't it working? and then get it working. And then, for the
rest of that project, I just copy and paste
out of the working file. [LAUGHTER] ANNA LEUSHCHENKO: And
for these situations, what I would recommend is-- once again, I'm opening
the ID settings. And I won't do
this mistake again. I will first find the
configuration-- the settings, and then I will move the window. For code generating syntax and,
in particular, for freezed, because it's the most
interesting one-- no. [LAUGHS] I do configure Live
Templates in ID-- in Android Studio or IntelliJ. For VS Code, once again,
there is a configuration JSON for that. But, basically, I have a lot
of syntax covered for me here. So if I try to demonstrate it
pretty quickly, for example, in a new file-- CRAIG LABENZ: Yeah, I do
this as well [INAUDIBLE].. ANNA LEUSHCHENKO:
Let's call it test. So I can say Details. And then I'm all set. We have dart files listed,
all the annotations applied, syntax, fromJson constructor. And I'm ready to provide
some fields, and that's it. CRAIG LABENZ: Anna, I have
just a fun question for you. Do you have a dog? ANNA LEUSHCHENKO: [LAUGHS]
No, it's from the outside. However, I do have a cat. And I'm surprised he's not all
over here because, apparently, he loves cameras and
he once in a while would pop up on a meeting or in
a stream, [LAUGHS] hanging out. CRAIG LABENZ: There's some
dog in the background who's serenading us, and folks in
the chat are enjoying it. ANNA LEUSHCHENKO: Unfortunately. [LAUGHTER] Sorry. CRAIG LABENZ: No worries. ANNA LEUSHCHENKO:
Apparently some dog here likes to come shopping-- I mean, their owner
would go in the store, and they would leave it outside. CRAIG LABENZ: Also plenty
of cat lovers in the chat. ANNA LEUSHCHENKO:
[LAUGHS] All right. So now that I have figured
out the syntax for the API constructor, I can actually
replace all of these three methods-- four methods I have-- all of the code I have here-- with simple method declarations. So what you see here is
our getArticles method that will still return
a list of articles. It would perform a get request
on the same path as before. And it would use either base
URL from dio or provided one. And retrofit allows to implement
get, post, put, patch, delete, all of those methods. In case we need to
pass a path parameter, retrofit has a
annotation for that. You would declare it
parameter like this and then also highlight
where it should be placed in the final URL. CRAIG LABENZ: Your line 18
there doesn't have a comma after Path id? Oh, no, that's just the-- that's an annotation. It's not its own thing. OK, sorry. You're good. ANNA LEUSHCHENKO: And same here. And we can immediately-- hopefully-- see
the generated code. Yeah. And it looks more or less
the same as we did before. So here we have a dio.fetch. We fetch a list. We perform a get method. Where is the path? We have /articles as a path. And the fromJson is
called as it was before. And the same goes on
for these three methods. And this fromJson
method is exactly the method that was
generated for us by freezed. It's not some custom method
generated by json_serializable. It just uses what it
finds in the code base. And I demonstrated only a simple
scenario with get annotation. However, retrofit also
supports various annotations for providing custom
headers and a custom map that can be then
intercepted in dio interceptors. And if I would use
them here, they would end up in this
extra and headers maps. So it's pretty flexible
and rich and allows developers to do quite a lot. CRAIG LABENZ: Yeah, this
is just super, super neat. And, like you said,
those interceptors-- Is that what they're called?--
where you add the-- you can add into the headers and whatnot. ANNA LEUSHCHENKO:
Yeah, interceptors. CRAIG LABENZ: But this amount
of networking code, this is almost even more powerful
than doing it for your models, because this networking
stuff gets pretty hairy before you know it. [LAUGHS] ANNA LEUSHCHENKO: Yeah. So, once again, to prove
it's all working as expected, here is our application again,
doing all the same stuff. And here I-- CRAIG LABENZ: Can we break it? ANNA LEUSHCHENKO: What? CRAIG LABENZ: Can you
just break one of the-- instead of articles, can it
be articles one, and then we'll just watch it crash? ANNA LEUSHCHENKO: Yeah. [LAUGHS] Do you really
want to check if I did error handling properly? CRAIG LABENZ: No. I just want to show-- I mean, I know you didn't,
which is totally fine. But just to prove that-- ANNA LEUSHCHENKO:
No, actually, I did. Actually, I did. CRAIG LABENZ: Oh. [LAUGHS] Wow. Now I'm just super impressed. But just so everyone knows,
yeah, these code-- every time she deletes all this code and
puts in the tiniest little bit, the app is actually still
rebuilding and rerunning. ANNA LEUSHCHENKO: Yeah. CRAIG LABENZ: Incredible. [INTERPOSING VOICES] CRAIG LABENZ: Sorry. I put you on the spot, but
you passed with flying colors. ANNA LEUSHCHENKO: [LAUGHS]
I like to highlight here is this warning. So why this happens? My project currently
uses Flutter lens. It's a certain set of literals. But your project may have other
packages used for later rules. You can add your custom rules. And it's simply impossible
for a code generating package to know them all, know them in
advance, and comply them all, because some of the rules
contradict each other, like using single
or double quote. And the way out of this
situation I prefer doing is simply ignore all
the generated files in the analysis option. CRAIG LABENZ: That's
what I do as well. ANNA LEUSHCHENKO: So
this is done like this. It has a downside
to it, of course, because if something went wrong
and the code wasn't generated properly, you would only know
of this when you try to actually run the app instead of having
all these warnings immediately highlighted with Dart analysis. But also, this way,
the analyzer is not loading our CPUs too
much with analyzing generated code because, most of
the time, it's probably fine. CRAIG LABENZ: Yeah. So we've got our API now
generating all of the methods to actually get on
the wire and load the articles and the launches. There was some dependency
injection in there. Is that on your roadmap as well? ANNA LEUSHCHENKO: Yeah,
that's my next topic. CRAIG LABENZ: Brilliant. It's almost like I'm a plant. [LAUGHS] ANNA LEUSHCHENKO: You
are totally right. I would like to go back to the
page code I demonstrated once. So, once again, it is a
GetIt instance, global one. And it is capable of creating
an instance of ArticleListBloc and accepting some parameter
along the way in a single line, despite the fact that bloc
itself has some parameters. And this is possible because,
when my application runs, I actually call
some initialization on this diContainer on
this GetIt instance. And this is how it looks like. So I kind of teach GetIt, here
is-- whenever someone asks you for a dio object, this is
what you have to return. And this is a simple method
that creates dio and does some configuration. And the method I call on GetIt
is registerLazySingleton. I think it's obvious,
LazySingleton. There is also
registerFactory method, which means every
time someone asks to create an instance
of SpaceFlightNewsApi, please create a new instance. And here it accepts
an expression, and calls the constructor,
and gets the dio from itself, and will already know-- it knows how to create this
dio, and so on and so on. And here is also-- CRAIG LABENZ: And
SpaceFlight API-- so one thing I
want to hit on here is the difference
between registering a factory, and a lazy
factory, and a lazy singleton. And I guess really the
singleton and the factory are the interesting parts. So you mentioned that there's
a new SpaceFlightNewsApi instance created every
time one is asked for. ANNA LEUSHCHENKO: Yes. CRAIG LABENZ: And those
are stateless classes. They don't really hold anything. They barely do anything. So that's totally OK. And then, if you
look at line 21, that SpaceFlightNews
thing needed an instance of dio,
which comes from line 19. And that one is not
recreated over and over again because it is giving
you the same dio object after you ask it
the second, the third time. So do you have
any other thoughts on that, about how you
think about factories versus singletons and keeping
that straight in this code? ANNA LEUSHCHENKO: Initially,
early in my career, I would prefer using
singletons on stateless classes as well as stateful. And the rationale was like,
well, I already created it. Why not keep it for the
next time it's used? However, with Flutter
and Dart being so good at managing small objects, and
recreating them, and collecting the garbage in a
pretty efficient way, later I moved to the strategy of
making everything, registering everything as factory, meaning
it would be created every time someone asks for it,
unless we are talking about special
objects like cache, like dio, maybe GlobalKey
for a navigator-- so things that really should be
a single item per application. CRAIG LABENZ: Yeah,
that's a good point on not stressing about holding-- saving Dart the trouble of
recreating another cheap class. It doesn't matter. Let me think about how many
widgets are coming and going-- like one SpaceFlightNewsApi
class in terms of performance. ANNA LEUSHCHENKO: Yeah. So a few things I'd
like to point out here is, when GetIt works
with primitive types, if we have a single unique
instance of string registered for application,
it probably will be fine to just register a string. But, typically, if
we need to provide some kind of configuration,
like base URL, like maybe API key
or something, it's typical to give this
registration entry a name. So, here, when we create
a SpaceFlightNewsApi, we say that, for base URL,
please find a string that is named with a certain name. And then someone somewhere has
to actually teach GetIt what value this name would have. And here is exactly what
I'm doing on line 24. However, I'd like to highlight
here the environment thing. So I have declared an enum
emulating the environments we might run our application in
work in progress, staging, production, dev test, whatever. So, for demonstration purposes,
here I have this environment. And if the environment
was passed as test, I would register this
weird-looking URL in GetIt. But if the environment
was a production-- CRAIG LABENZ: Not a test. ANNA LEUSHCHENKO: --yeah-- [LAUGHS] I would use
the real API URL. And, so far, it's
manual implementation. So I just compare
this environment here. And when I call this method in
main, I pass the prod value. And yeah. Probably I should also
highlight the way blocks are registered in GetIt, because
those require some parameters. And, by default, GetIt
supports up to two parameters. You can simply specify
registerFactoryParam. And this will be a parameter. This is how you
pass it in the bloc. And when you try to create
an instance of this bloc, this is how you
actually pass it. And I think now it's time
to refactor and get rid of all of this code. CRAIG LABENZ: Let's do it. So, to be clear here, yeah,
you hand-wrote all this. ANNA LEUSHCHENKO: Yes. CRAIG LABENZ: But you're
going to use code generation to generate all this. ANNA LEUSHCHENKO: I
honestly suffered a bit. CRAIG LABENZ: Oh, interesting. ANNA LEUSHCHENKO: Because I
wouldn't be writing it manually since I don't know-- since when I
discovered injectable. But now, for this demo
purposes, I had to-- CRAIG LABENZ: [LAUGHS]
So the suffering that you did was writing this
non-code generation version merely so it could be deleted. ANNA LEUSHCHENKO: Exactly. [LAUGHTER] Well, come on. Deleting code is the most
pleasant thing in our job. CRAIG LABENZ: A lot of folks
do really love deleting code. ANNA LEUSHCHENKO: So what
I do, first things first, add new dependencies,
injectable_generator, and injectable-- get_it is already here. It's been here before. Once again, I have to
rerun code generation because I got new dependencies. And now I'm going
to copy-paste again. So I'm replacing this
entire file with just this. CRAIG LABENZ: Nice. ANNA LEUSHCHENKO: No, make this. Yeah. CRAIG LABENZ: Feel like
it's easier to maintain. ANNA LEUSHCHENKO:
[LAUGHS] Almost. Well, so far, our
application doesn't know how to do dependency
injection anymore. CRAIG LABENZ: Yeah. ANNA LEUSHCHENKO: But it already
has generated a file for us where the code later
will be generated. So far we see-- so it is an
extension over GetIt. And it will return a GetIt. But it will do some
registrations here. So far it doesn't
know how to do that. So I think I can immediately
add this file to a list of ignored files in later. And first I will start
with a pretty basic type. This is a class responsible
for opening the URL, and it doesn't have
any parameters. So I simply add
injectable, and that's it. And now, if we check
our generated code, we see that it does
register a launcher in GetIt with factory method. So whenever someone will
be asking for launcher, please create a new instance,
and this is how you do it. CRAIG LABENZ: What's the
i1, i2, i3, the underscores? ANNA LEUSHCHENKO: It's just the
way injectable generates code. It gives all imports, prefixes. CRAIG LABENZ: Alias. ANNA LEUSHCHENKO: I guess
it is valuable in case you have classes with the
same name in different files. This way, it will be
able to distinguish, and you will not have
compilation errors. CRAIG LABENZ: Yeah. ANNA LEUSHCHENKO: Next I'm
going to do the same exercise, let's say, for this navigator. This navigator is-- screens
aware wrapper over the router, which is a plain navigator
2.0 implementation. So this one I also
annotate with injectable. And I think here
we see the error. "Navigator depends on
unregistered type router" because we didn't teach GetIt
and injectable how to create instance of this class. And, for this class, I'm
going to use LazySingleton because why not have a
single router for the app? And now, we have a
different warning. So router depends on
unregistered type GlobalKey. And I inject this
GlobalKey in order to be able to do the
navigation without the need to get the context object
all the way through UI bloc and so on. I inject a GlobalKey, which is
provided to the material app. And there is a problem with
this because GlobalKey is not my class. I cannot just go there and
annotate it with injectable LazySingleton. But, for that, we have
a mechanism provided by injectable called module. So this is a place
where, previously, I had a key getter that would
just return a new instance, and I would call it once
when I was registering this object in GetIt. Now I can annotate it with
special connotation module and remove static and say
this should be lazySingleton. So what happens now? The generated code-- through
generating an inheritance of my module, but
it's a minor detail-- it says that whenever
someone is asking for a GlobalKey
of NavigatorState, please return a lazySingleton. And you get the instance by
calling these key methods that I declared here. CRAIG LABENZ: And then how do
we tell your router to use this? Or does injectable just
already figure that out? ANNA LEUSHCHENKO: Yeah,
it did because it knows that the router accepts a key. We have already
annotated the router. So here is the record
for the router. CRAIG LABENZ: I can see
it, yeah, grabbing it. ANNA LEUSHCHENKO: Yeah, it
asks GetIt for a GlobalKey. And, previously, we taught GetIt
how to create such instance. CRAIG LABENZ: And
so it all works. ANNA LEUSHCHENKO: Yeah. So yeah, succeeded. Now, no problems here. Next we should work on our blocs
because this is something we cannot apply partially. We need to go through all
the instances in the app to make this all work together. So, for blocs, I also
would use injectable, which corresponds to factory call. So whenever someone would call-- create a bloc instance,
please create a new instance. And it will be responsibility
for this BlocProvider widget to actually persist
this with this instance. And here we have a launchId,
which is a parameter. So I have to have a
special annotation just for that, like this. And I would immediately do the
same thing for another bloc. So factoryParam here
and injectable here. Let's check the
generated code again. Now it knows how
to create a block. And it knows that it should
be accepting a string parameter, and same here. And it uses the
real constructor, and it creates all these
parameters instances because it already
knows how to do that. CRAIG LABENZ: Yeah, you can
see a little bit of a learning curve, I think, on
this one maybe more so than the others, where the
magic feels pretty intense. ANNA LEUSHCHENKO: [INAUDIBLE] CRAIG LABENZ: Yeah. But once you internalize-- and opening up the
generated code, like Anna keeps doing
here, and showing us what it does every time
she makes a change-- super, super helpful
for internalizing what's going on and getting over that
awkward fog of war feeling around your annotations
versus the generated code and just knowing
what it's generating. Then, once you get
into that space, you can just write so
little and get so much. ANNA LEUSHCHENKO: Yeah. What I also like about
injectable, in particular, now, with these annotations,
you can tell how it's going to be used
by looking at the class declaration. You know it's going to
be registered in GetIt. And it's going to be
created in a particular way. Previously, we would have
a bloc declaration here. And there would be
some method that would init GetIt and teach it
how to create these instances. And we may not be aware
immediately of what is there. And now, to me, we
improved readability, even though it might
seem like we made things a bit more complicated. By the way, it sounds
like the dog went home. CRAIG LABENZ: Yeah. [LAUGHS] We're no
longer being serenaded. Folks thought it was maybe my
Husky, but he's taking a nap, I think. ANNA LEUSHCHENKO: So we have
a new warning in the code generation because bloc
injects SpaceFlightNewsApi, and this is an object we
have not yet annotated. So this is very useful
to check the output. It oftentimes contains very
meaningful error messages. So for SpaceNewsApi,
it's a bit interesting because it is already
a generated class. We already have some
annotations here. But that doesn't mean we just
annotate it with injectable. And, by default, injectable
uses default constructors to create objects. Here we have a
factory constructor. We can also have static
methods that create instances. It doesn't matter, as long
as we annotate it with-- is it factoryMethod? Let me check. Yeah, factoryMethod. So now, back to
the generated code. Where would it be? Whenever someone tries
to create an API class, get dio, and get base URL. And new warnings. We didn't teach how
to create a get dio. And, once again, dio is a third
party object I cannot simply go and annotate. But I can create a module. We already have a module
where we provide key. I think it's fine to
have different modules for different purposes. So here I have an API module. Let me check if I did
everything correctly. Yeah, the problem is
these static methods. Yeah. We lost the generated file
because we have problems. And it complains
because it has-- this module has two strings. And what it would
do, it would try to register all of
the class properties, or getters, or methods. And this means it
would try to register two strings without knowing how
to distinguish between them. And if you remember, previously,
I had this environment enum, where I would use
one or another, depending on the
environment I passed. And injectable supports
environments as well. And I think it's doing
it in a very nice way. So I can just say prod
and def, use this base URL but for test use this base URL. CRAIG LABENZ: Yeah, interesting. So this isn't going
to use your enum, but it's something equivalent. ANNA LEUSHCHENKO: The injectable
provides the same enum-- prod, dev, and test. It might not exactly
match to your scenario. So it's not that I use it
this way of customizing implementations based on the
flavor or launch configuration, but it's here. The downside to this
mechanism is that-- let's see. I hope I have some
code generated. Yeah. It uses registerFor and
deep inside injectable. It's a simple if-else statement,
which means it's not const, which means the tree shaking
will not work for these cases. So even though I'm running the-- or building the application for,
let's say, prod configuration, this value still will be
built into the package. CRAIG LABENZ: Oh, interesting. So for a tiny string,
that's not a huge deal. But worth keeping in mind for
potentially larger objects. ANNA LEUSHCHENKO: And not
only whether the objects are large or not, but
also whether it actually is something that you want to
be built into your app bundle. Maybe there's potentially
a security issue if you-- CRAIG LABENZ: [INAUDIBLE]. Yeah, yeah, yeah. ANNA LEUSHCHENKO: Yeah. And, also, these annotations can
be applied not only in modules over plain types, but
also the full classes can be annotated with it. I would say, here is
the implementation of SpaceFlightNewsApi you
have to use in production, and here is the one for testing. The last thing, if you
remember, our baseUrl was injected with certain name. Because if we are to register
more than one string in GetIt, it needs to somehow
distinguish between them. And this is done pretty easily. I would place Named
where the parameter is. And I would also place
Named where I declare the values for this parameter. CRAIG LABENZ: Ah. ANNA LEUSHCHENKO: Let's see. Is it running? Yeah, it succeeded. CRAIG LABENZ: And how are
we currently distinguishing whether or not we're
in prod, dev, or test? Because I remember
you had that function. ANNA LEUSHCHENKO:
That's exactly where I have compilation
errors, because I deleted my environment enum. But if I inject-- import injectable, it's gone. So it's the same environment. CRAIG LABENZ: I see. ANNA LEUSHCHENKO: And
actually have to quickly fix the same in tests because-- but I won't go into tests
today, unfortunately. But it's a fascinating topic. And the repository I
showed in the beginning contains the code generating
package for tests. So go check it out. CRAIG LABENZ: All right. So is all of the
dependency injection setup now being successfully
generated again? Looks like it. ANNA LEUSHCHENKO: Yeah. So instead of having written all
of this manually in this cloud file, we only have
a couple of lines. And this is being generated. But, also, we moved some
metadata about classes closer to their code. And, once again, the
app is still running. And we have a new-- CRAIG LABENZ: Yeah, it's
still launching rockets. ANNA LEUSHCHENKO: --new news. CRAIG LABENZ: Oh my gosh. Yeah, that wasn't the
top article earlier. ANNA LEUSHCHENKO: Yeah. CRAIG LABENZ: What
did we get here? "Air Force rocket cargo
initiative marches forward despite questions
about feasibility." Riveting. ANNA LEUSHCHENKO:
The joy of demo. [LAUGHTER] CRAIG LABENZ: Incredible. So we do have a handful of
pretty interesting questions. So we can go to those or-- no, well, at a minimum, you
want to show us barrel files. But do you have anything
else before barrel files? ANNA LEUSHCHENKO: Just
maybe a short advice here. As I mentioned previously,
some packages, like injectable, generate all of their
code in a single file. So this file is the most prone
to having merge conflicts. And, if you can, the
advice to avoid this would be to split your
project into smaller packages. However, you have to be mindful
about analyzer performance, which is another side of
having smaller packages. It reduces the scope. It helps with encapsulation. It helps with running
code generation faster because, even though you
can limit the scope of code generation in build_yaml,
still all eligible files will be looked through. So if your package
has fewer files, the code generation runs faster. But it affects
analyzer performance, so there has to be a
balance between those. CRAIG LABENZ: Yeah. Every file can't
be its own package. Now, you have-- well,
so, first of all, are you ready to move to barrel? Because I don't want
to short-circuit if you have more stuff. ANNA LEUSHCHENKO: No. No. Questions have higher priority. CRAIG LABENZ: OK. Well, no, I think it'd be-- I think you should at least
just show us barrel files. ANNA LEUSHCHENKO: All right. CRAIG LABENZ: And then
we'll do the questions. ANNA LEUSHCHENKO: All right. The reason we speak
about barrel files so much is because I
created a code generation package [LAUGHS] recently. It's called barrel_files. For this project, the
example is a bit artificial, just because this is
the main application, and this is not
where you typically have a source folder and
a barrel file exposed in just a few items out of it. But if you are developing
a package, that is really useful because you
can write all of your code in the source folder,
create a barrel file. Typically, it is called by
the name of the package. And whenever someone imports
some code from the package, if they do so from
the source folder, they would have a
little warning saying, you shouldn't actually do so. And, instead, they would
have to import the barrel file of the package,
and you would be in control of
what you actually expose from the package. And the one problem with
this approach that I felt was that when you're looking
at the class declaration, you are not aware of whether
it is exposed from the package or not. You have to go check
it in another place. As opposed to when we,
for example, create private classes, private to
a library, to a Dart file, we would name it
with underscore, and we immediately know
about its visibility. So let me add a new dependency. CRAIG LABENZ: What a flex,
adding your own package as a dependency. ANNA LEUSHCHENKO: [LAUGHS]
Once again, this one contains of two parts, barrel files
annotations and barrel files. Run get, and rerun
code generation. It may seem like I'm rerunning
code generation too often. But, actually, we are just
having it too intense of a demo here. Typically, you would either add
all of your generating packages all at once, or you
would spend more time between adding new ones. So it's not a typical
development experience with it. CRAIG LABENZ: Yeah,
10 times an hour. ANNA LEUSHCHENKO: [CHUCKLES] So imagine that I here
have a source folder, and I want to expose
only certain classes. What I would do-- and I hope I can remember
my own annotation-- it's very simple one. [LAUGHS] includeInBarrelFile,
that's all. CRAIG LABENZ: Good. Good. ANNA LEUSHCHENKO: We immediately
have a barrel file generated. It's named after the package,
which is SpaceFlightNews. But it also supports
configurating the name of this file. And it exports only the type we
have annotated from this file. And it supports all
top-level elements, like classes, enums,
functions, typedefs, whatever, if they have a name. So if this class had a
couple of annotated classes, you would see them
listed here with comma. CRAIG LABENZ: So
what if you didn't want to use the show keyword,
you just wanted to use-- you wanted to expose
every class in the file? ANNA LEUSHCHENKO:
I intentionally implemented in this way. So you would be very explicit
about what things you want to expose,
because, this way, you will be mindful of things
you don't want to expose. Because another side of manual
management of barrel files is, let's say you decided
that you no longer need to expose some class. You have to remember
to go to barrel file and remove the record for
it, which is easy to forget. But when you're looking at
the class implementation, it's kind in front of your eyes
that it's going to be exposed. CRAIG LABENZ: Nice. Incredible. This is a minor preview. The thumbnail for this stream
said code generation part one. Part two next week is
going to involve building-- writing a code
generator ourselves. So we'll be walking
the path that Anna had to walk when she made
the barrel files package, which I'm excited about,
because I've never done before. Very cool. ANNA LEUSHCHENKO: Yeah,
it's an interesting topic. I just set it-- this same annotation on
top of another class just to highlight it's
possible to stack annotation one over another. They would not
contradict each other. And we have it reflected
here immediately. CRAIG LABENZ: Nice. Nice. All right, question time. Let's see here. The oldest question I
have goes back to 9:47-- well, that's California time-- 47 minutes into the stream. So maybe we've
covered some of these. "In terms of
architecture, I know we're looking into
a sample project. But with a larger
project, should we consider avoiding using JSON
serialization-specific logic if we have data transfer objects
and entities separately?" ANNA LEUSHCHENKO: Well, if you
have the two sets of models, clearly it makes
sense to only apply JSON serialization
to those which you use to communicate to API. And then, in your domain
models, you would only use, let's say, freezed
or an alternative without json_serializable. It's possible, as you
have seen, freezed models can exist without
JSON's realization. [LAUGHS] CRAIG LABENZ: Yeah. The one thing that
I always think about with that, though,
is then you're going to have to write
the methods to translate between the domain and
the data transfer layers. And nothing's going to
generate that code for you, so you're going to have
to write it by hand. And then you're
back in the place of having awkward
[INAUDIBLE] functions. It's easy to forget something. And so if you're willing
to take on that pain, then you could have just
handwritten the toJson and fromJson. But then, why would you do that
if you can generate the toJson and fromJson? So that's where I come back
to not having a data transfer separation myself. ANNA LEUSHCHENKO:
I can understand it might sound strange
not to have them. But come on. We have so many conversion
logic of using different types or changing the
name of the JSON key as opposed to the name of
the domain model property. But, also, the structure from
the backend may be different. And over time, it may change. Why would we need to make all
of the changes in our code base? I mean, these changes should not
affect our blocs and UI, right? CRAIG LABENZ: Mm-hmm. ANNA LEUSHCHENKO:
But there is a way to address all or
98% of cases, even with different structure of
the model coming from backend. There are ways to address this. And if the structure on the
backend change, once again, you tweak it with converters,
with toJson and fromJson adjustments. And your model, your
domain model interface remains the same. And your UI remains unchanged. CRAIG LABENZ: Yep. This question came
up a couple of times. "Question about
'injectable.' When using it, we don't have one place to
explicitly inject dependencies. Instead, the declarations
are spread all over the code. Is that OK?" ANNA LEUSHCHENKO: You can see-- you can look at
this from two sides. Someone might see an
advantage in having all of these in
single place, while I believe, when developing, you
would rather be focusing on-- I need to answer this question
when I look at the class that I'm writing,
whether it is going to be created every
time, whether it's going to be a singleton. So if you prefer doing
this, then injectable is actually working
to your advantage because you see the
annotation immediately, whereas the class is declared. CRAIG LABENZ: Yeah, I've
never used injectable specifically before. But I do use GetIt. And so what you get to
do if you use injectable is you write a
constructor that's clean. It just takes whatever
parameters it takes. Of course, you often
have to annotate-- yeah, you add annotations, but
just the code itself is clean. And in the dependency
injection setup code, those values are passed
in by looking at GetIt. Now, that's injectable. I've not really used
injectable before. And so I have to
do it differently where I'll write a constructor,
and in the constructor, there might be a default
fallback that goes to GetIt. So now, all of my
class constructors will reach out to GetIt and
find what they need as well. And so it is just-- you're going to have something
spread around your codebase, whether it's the going and
getting of the dependencies that you need by having
GetIt calls in your class constructors, or
it's the annotations that define that this thing
should be registered in GetIt. And then your class
constructors are often cleaner. And when you want to instantiate
something in your code, you don't have to do
anything with the parameters because they're all shopped
around for within the code that injectable wrote. So I think there's something
is going to be spread around one way or the other. Did that make any sense, Anna? ANNA LEUSHCHENKO: Absolutely. I think I would do
it a bit differently. And I would still keep
individual parameters in inside constructor,
not a GetIt instance and resolve them
in the constructor. I would still accept them. But then there has to be a
place where I would teach GetIt how to do so, how to
create instance of my class in one line. And I would keep these
parameters individually because I would also-- maybe I would like
to test the class, and I would like to
mock these dependencies. And I would either have to
pass mocks in the constructor or register them
in GetIt in test. CRAIG LABENZ: Yeah. Now, we didn't
talk about testing. But there were a
couple questions-- oh, wait. Sorry. I needed to scroll up further. I had some older
questions as well. One goes all the way back
to the very beginning. There's actually
a couple on this. "With macros on the horizon,
will most of code generation be deprecated? Would it be possible to migrate
a package like equatable or auto_route from code
generation to macros?" So code generation
is the concept, and macros is more
code generation. So, from a high
level, macros won't deprecate code generation. But maybe you
meant build_runner. And will macros deprecate
build_runner is a soft yes, probably. Of course, macros may
not actually land yet. But the Dart team is very
hopeful and optimistic that it will land. But there's still some
developer experience questions to continue exploring
and make sure are gotten right. So it's not 100% yet that
it will even come out, even though it is in the
master channel right now. And then, also, in
terms of will it be possible to migrate things,
yeah, the goal of macros is that it will be able to do
everything that you can do. If you can do it
with build_runner, you should be able
to do it with macros. But do you have any
thoughts on that, Anna? ANNA LEUSHCHENKO: Yeah. So I have the same
idea as you do. And, on top of
that, I'd say that for functionality,
like potentially freezed and json_serializable,
which is what we all want, what we all agree on, which is
a standard of what we do daily-- we want operator equals to
look into property values. We want copyWith to allow
nullables and so on. We want JSON serialization. It makes sense for Flutter
team to put effort there. However, for things
like injectable, which represents one
way of doing things, while there can be
different solutions to it, I'm not sure it even
will be on the radar for macros support of these
kind of problems to solve. CRAIG LABENZ: Yeah,
that's a super good point. And it relates to this
question that I also starred. Would code generation
still be useful if Dart supported data classes? And just like Anna was
saying, data classes are but one area where code
generation is currently used. Retrofit and injectable
both offered a ton of value in the walkthrough we just had. And they're obviously
different than data classes. So I think the answer
there is a definite yes. Let's see here. Continuing to read through. This is potentially
maybe the last one. And it's a fun
question, I think. Someone's head was in an
interesting and good place. "What performance gains
does this package bring?" And they said this at the
end, so they were probably talking about injectable. But I think we could
expand this out to build_runner and all
of the code generation that you just did. Performance gains--
Anna, were there any? ANNA LEUSHCHENKO: If we
say about performance of the applications in
runtime, maybe, actually, it's not a gain even
because unless-- some functionality of the
generated code may be redundant and you will be building
it into your project. Maybe some of the operations
you're used to doing will be done in a
less efficient way. But if we speak
about efficiency gain for developers, for spending
time on writing code, I think it's a huge boost. I mean, you saw how much code
I have deleted and replaced it with a couple of annotations. And I left all the
work to build_runner. It still generated
the same code. It's still a part of my project. But it wasn't my
time spent on it. CRAIG LABENZ: Yeah,
perfectly said. In all likelihood, runtime
performance-- nothing, no change. And, yeah, it can be a
little less efficient. I think that that will also
functionally not matter. ANNA LEUSHCHENKO: Yeah. CRAIG LABENZ: But, yeah,
developer efficiency is often very important. Until you're an
extremely successful app, developer efficiency
is basically the only thing that matters. And code generation
helps a ton there. Anna, let me check. I don't think I've-- let's see here. Just looking at messages
at the very bottom. Let's see. What does it say? "I wonder if it would be
possible to create a migration tool for code
generation to macros." That would be extremely hard. Possible, yes. Realistic, I wouldn't advise it. I think a lot of code
generation packages will just be rewritten. Hm, hm. Oh, this one's kind
of interesting. Maybe this can be
the last question. About injectable,
"How do you control the lifecycle of objects?" You both are an expert
on this and gave a talk about the garbage
collector, so you're really suited for this question. ANNA LEUSHCHENKO:
First of all, it's not that you really control
the lifecycle of an object. If the way you registered it was
every time someone asks for it, please create a new instance,
as soon as everyone-- every object that was
holding in reference to this object in question--
as soon as all of them are garbage
collected, your object is also a candidate
for garbage collection. And you don't bother
about its life cycle. It will be collected over time. If you created an object like
lazySingleton or Singleton, once it gets created,
it will pretty much stay alive until you either
close the application or you do some explicit
operation on GetIt instance, which I doubt you would do that
during the application life cycle. There are some configurations
that GetIt supports where you can provide
methods that are called when the object is created. And which are--
and that are called when the object is being
unregistered from GetIt or destroyed,
however you call it. So you can react
to these events, but you cannot control them. CRAIG LABENZ: Perfect answer. OK, folks. This has been-- I had a blast getting a tour of
how Anna thinks about Flutter development and how Anna
thinks about offloading all the TDM that can go into
just writing all the layers and all the stuff that goes
into building an app these days. Anna, thank you so,
so much for joining. Any last thoughts on your end? ANNA LEUSHCHENKO: First of
all, thanks for the invitation. I think it will be
beneficial for the audience if I briefly go
through the advices I gave throughout
the live coding-- CRAIG LABENZ: Oh, sure, yeah. ANNA LEUSHCHENKO:
--and also maybe add a couple of which I
couldn't demonstrate. So we talked about lock-in
versions of code generating dependencies in
[INAUDIBLE] as opposed to using a version range. We spoke about using
aliases or other ways to speed up the process of
launching code generation. We looked into how to configure
IDE to collapse or hide generated files, how to
configure the build_runner input with build_yaml
file to limit it to certain folders,
how to exclude generated files from
static analysis. We talked about adding
the code generated files to source control and the
advantage of splitting the project into
smaller packages. That's all I could
fit in the demo. However, a few other
advice that you might want to apply if
you're extensively using code generation. It's creating instances of
frequently used annotations. Sometimes you may need to
configure the order in which code generators are run. Like, json_serializable
should run before freezed and before retrofit. It's pretty rare cases,
but sometimes it's needed. By the way, we looked into live
templates for code snippets to create freeze models. Or you can create
similar live templates for other code generations. And remove generated
files from code coverage can also be beneficial if
you drag these metrics. But I hope, if you take one
thing from this presentation is, code generation,
it's not scary. It boosts developers'
efficiency. And don't be afraid to go
into the generated code, read it, understand
what it does. And, eventually, you may even
figure out some improvements you can do right on your side. Like, oh, here's a settings
I didn't know about. I will apply it, and
it will improve some of the code that was generated. Or you can end up
contributing to open source and opening issues and
PRs to the packages that save your time daily. CRAIG LABENZ: Yeah,
there's no better way to give back to the community
than to actually act on any friction or any bugs
that you find in some packages and at least file an issue
that clearly describes them or open a PR for
real extra credit. So, folks, next week, I'm
going to have the one and only Kevin Moore on the stream. And we are going to
write a code generator. Our current thinking
is that we're going to write something
that generates a toString function on a class-- so, obviously, a very,
very simple thing. But we're just going to
walk through the steps, understand what
we're looking at, and bring it all
together, hopefully, to offer just that tiny bit
of freezed functionality in the two-hour
stream next week. So if you found this
interesting and thought, oh, maybe there's some other
places where I could stand for a different code generator,
but it's really specific to my case-- so, of course,
no one's written it-- next week should be helpful. And then, also, we're
definitely going to be talking about
what subset of the APIs and the concepts
that we're looking at will still apply in macros. The macros API will be
different than code generation. But how you think about
it, how you approach it, the kinds of problems
that you solve, why you would reach for code
generation, that will all be the same, of course,
from code gen to-- or from build_runner and
source_gen and those packages, the macros in the future. So, Anna, thank you
again for joining. And, everybody else,
I'll see you next week.