Observable Flutter #34: Code generation

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
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.
Info
Channel: Flutter
Views: 19,729
Rating: undefined out of 5
Keywords: pr_pr: Flutter;, gds:Yes;, type: Livestream Video;, observable flutter, flutter latest, flutter updates, flutter developer, flutter developers, developer, developers, flutter, google, Craig Labenz
Id: jYWOyMZamcY
Channel Id: undefined
Length: 114min 42sec (6882 seconds)
Published: Thu Feb 01 2024
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.