Observable Flutter: gRPC

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
CRAIG LABENZ: Hello, everybody. Welcome to another episode of "Observable Flutter." I'm your host, as always, Craig Labenz. But maybe one day someone else will host-- just "as always" so far. Anyway, today I'm excited to talk to everybody about gRPC, which, for those who don't know, stands for, I believe, Google Remote Process Call. I know it was invented by Google. Maybe my guest will be able to clarify exactly what the G looks like-- or stands for, which I realized, as I started this sentence, I've never actually looked up before. I've just assumed. But before we get too far into that, I just want to remind everybody that here on "Observable Flutter," we're all very kind to each other. This is the Flutter community. Let's live up to our good name. All right. And my guest is Gianfranco, who is the CTO and Co-Founder of Somnio Software in Uruguay. And many of you will know him as just a delightful presence on Twitter in the Flutter community and an all-around great software engineer. So Gianfranco, man, I am really excited to have you. Welcome. GIANFRANCO PAPA: Hey. How are you, Craig? CRAIG LABENZ: I'm great. I'm great. I'm very excited to talk about gRPC. Maybe, if you'd like, you could begin by just saying a few words about your history with tech and in Flutter, and how did we get here? GIANFRANCO PAPA: Yeah, of course. OK. So first of all, thanks for having me here. I'm really excited. I always see the show, so being part of the show right now is really-- I'm very excited. And yeah, maybe I can introduce myself. I am Gianfranco Papa, and I am CEO and Co-Founder of Somnio Software. And yeah, I'm a software engineer who specializes in Flutter. I really love this technology. And what else? I've been growing the community in Uruguay here in Latin America. I'm Co-Organizer of Flutter Uruguay. Yeah, so that's really cool. And yeah, in our company, we are basically a dev shop that specializes in Flutter. We are 100% focused in this technology. So yeah, we started at the same time that Flutter actually was created. We started with a beta back in 2018. And then we kept working in projects and realized, OK, this is something big. So yeah, that's how it all started. We basically trusted in this technology. And yeah, back then we were working with mobile apps. But now, as we all do now, we can work with web on desktop. So that's really great that Flutter keeps opening us doors to work in any kind of project. CRAIG LABENZ: Nice, yeah. I know we've met at a couple events. And I always love to meet and talk to folks who go all the way back to the early beta, because I don't even-- I found Flutter shortly after it went to 1.0. So you've got a longer Flutter tenure than I have. But today, we're not just talking about Flutter. We're actually hitting a more kind of Dart-centric concept. Although the concept itself is larger than Dart, we're just going to implement it in Dart, which is gRPC. So Gianfranco, you pitched this episode idea to me, I mean, a long time ago-- basically right when "Observable Flutter" started. And so you knew immediately that you wanted to talk about gRPC. Tell us a little bit about why the average developer should agree with you and also be excited about gRPC. GIANFRANCO PAPA: Yeah, OK. So yeah, I remember that we talked about even before "Observable Flutter" was a show, we were talking about maybe-- "The Boring Show," I think that I asked you to talk about gRPC because, yeah, back then, I think I saw a video about full-stack apps but with WebSockets. So yeah, the thing about gRPC is that it's another tool you can have in your toolset to create full-stack apps in Dart. Because as it has Dart support, you can create your frontend and your backend by only using Dart. And that's really advantageous for us Flutter developers who already know a lot Dart and want to also create backend applications and we don't want to switch to another language. But I mean, we all know that there are other things or tools to create backends in Dart, for example Dart Frog or Shelf. But in the case of gRPC, I think that it's slightly different because you can not only make your backend in Dart, but you can also use other programming languages seamlessly. So yeah, that will be because-- we are going to see it, but you can basically-- I mean, gRPC has support for a variety of languages. And you can basically autogenerate with a thing that is called protobuf whatever language you are targeting. So it's kind of more or less language agnostic, in a sense. CRAIG LABENZ: Yeah. The cross-language part is a huge deal because one of the driving goals or a value that a developer gets by having a full-stack Dart application is to be able to reuse a bunch of classes and not have to duplicate logic and whatnot. And there are still-- even if you use gRPC, having Dart on both ends will kind of put you in the best possible scenario there. But if you can't use Dart on the back end for any reason-- we'll be using Dart on the backend today. But if for some reason you can't, that's a great point. Because those bindings are generated on both ends in a variety of languages, whether you're using JavaScript or Python or Java or whatever on the backend, the task of communicating with yourself across that language barrier becomes so much simpler because from that simple protobuf definition you mentioned, which we'll get into, everything kind of falls out of that. And by the way, if any of you ever want to work at Google and you want to apply to Google, familiarity-- like protobufs, that is what every Google Developer uses every day. Everything at Google runs on protobufs and the code that is generated downstream of that. So you might be training yourself a little bit for your next job. All right. Gianfranco, I believe you are ready to walk us through some steps of getting started. Am I right? GIANFRANCO PAPA: Sure, yeah. Let's get right. CRAIG LABENZ: OK, let's do it. GIANFRANCO PAPA: Perfect. OK. So my idea for today, we were discussing with Craig to present kind of something from the various-- from scratch. It would be something real simple, but yeah, the idea is to make it from scratch. So it won't be something very fancy. But yeah, first of all, I wanted to have the documentation open here. So actually, I consider that gRPC is not a topic that there is so much information on, like blogs. Or it's not so common such as Rust. But yeah, here's the documentation. So you can always go. And here are the official docs. Well, "gRPC is a modern, open-source, high-performance remote procedure call." So remote procedure call would be the protocol that we are going to use. Just as you have REST or WebSockets, here we are going to use RPC. And I think the G, it could be for Google. I actually didn't ask that question. But probably it would be because of Google, right? CRAIG LABENZ: That's my thought as well. Randal tells us that it's just one of those cheeky recursive algorithms-- or acronyms. So I guess the G is officially not for Google. It's just meaningless. GIANFRANCO PAPA: A coincidence. OK. CRAIG LABENZ: But since this was invented by Google, if they picked a meaningless letter, I'm not surprised that they picked G, so. GIANFRANCO PAPA: Right. Makes sense. So OK. And here, you can get started. We have a lot of languages. Of course, we are going to choose Dart. But you can play around with any of these languages that are very common. And so yeah, another thing that I want to show is the quick intro. So here, we have an intro. And it will also talk about protocol buffers that is really tied-- I imagine that you can kind of look to protocol buffers as JSON for REST. The protocol buffer for gRPC would be kind of the same the way it communicates information. But yeah, we are definitely going to explain better this term. So yeah, I wanted to only show that. Maybe we keep in hand the documentation for some things. But we are switching into VS Code, and we are going to create a full-stack app from scratch. So yeah, the first thing I want to talk about, actually, is protocol buffers because maybe we can start our project by creating some protos-- short for "protobuf." And yeah, my idea is to have three different folders-- one for the frontend that will be made in Flutter, then another one for the backend that will be made in gRPC-- using gRPC, and another one that will be the share folder that we will call it protos. But we will use it in the backend and the frontend. And that is one of the first advantages, that we can reuse a lot of the code between the frontend and the backend. OK? CRAIG LABENZ: I love it. Yep. GIANFRANCO PAPA: Perfect. So I'm going to just create-- OK. We are here in the terminal. I'm going to create a project. I always forget what is the template, the default template, so-- CRAIG LABENZ: You want server-shelf? GIANFRANCO PAPA: No. Actually, we're going to start with the protos package. So I'm going to select a package so we can store all of our proto buffer files. So OK. So we're going to create using the template-package. Oh, maybe we can create the directory first. So let's create a protos folder. And then we can move to the protos. And here, we can create the package. So if we hit package in this directory-- OK. I might force this. So here, we have a pure Dart package, real simple. So maybe we can delete the test folder, because we are not going to have any tests. And this is going to explode sooner or later. This is not a good practice. CRAIG LABENZ: Well, you wouldn't really write tests for your protobufs, because gRPC is well tested. So unless you just didn't even define your model correctly, you wouldn't have tests in this folder, I don't think. GIANFRANCO PAPA: Yeah. I guess you are mistrusting the way gRPC works, or protobuf, yeah. CRAIG LABENZ: Right. GIANFRANCO PAPA: OK. So here, we have an example. We have a lib folder. And the first thing to do would be to actually grab our dependencies. So we are going to use protobuf and gRPC. Those are Dart packages that you can see popped up. So yeah, let's make sure that everything is in place. So here we have our dependencies. Let's put here the dependencies. And that's it, right? CRAIG LABENZ: Looks good. GIANFRANCO PAPA: Perfect. OK. So to create our first proto file, maybe we can create a folder that will be called protos. And here, we can actually create our first definition of our protocol buffer model. And I was thinking about a basic todo file, but we can choose anything. It would depend on what you want to build. But let's start with a todo. And the extension would be proto. So in here, we can start defining what would be the protobuf for our model. So I want to get back to the documentation so we can see the basic syntax of the protobuf. So let me search in here for the protocol buffers, that it has separate documentation. But here, it would be more or less the syntax that we have to use in order to create a model using protobuf. So you see that if we would like to create maybe a todo class, we will have to use the keyword "message." And then for defining every property, we'll have to use this special notation where you define the name of the property and a code. That code would be like an incremental integer. CRAIG LABENZ: And that "message" keyword, that's kind of like-- it's basically just the keyword "class" in any other language, right? But it's written as "message" here because the whole idea of gRPC is the ability to get this data on the wire, how to send this data somewhere else. So gRPC thinks of all these things as messages. Their data that starts on one computer is going to go to another computer. But it's really similar to the keyword "class" if you're looking at this for the first time. GIANFRANCO PAPA: Yeah, it could be. So yeah, the thing is that in order to have this connection, this interchange messages in an efficient way, gRPC-- protobuf, sorry-- reduced the whole concept of a message that we are passing through services, so through the frontend and the backend, so it can be more efficient in terms of size because if you know JSON-- probably you know it-- it will be something that is really easy to read. But it's not so efficient to pass over the wire. So in this case, we are not producing something so readable, but it will be very efficient to pass back and forth. OK. So maybe-- CRAIG LABENZ: Yeah, JSON wastes-- oh, sorry. I was just going to say JSON spends so much time-- or wastes so much space with all the parentheses or the curlies and the quotes and just sending things as raw strings. And gRPC also sends this in a binary format, so it's incredibly efficient. GIANFRANCO PAPA: Right. So yeah, maybe we can start defining our first message. So I'm going to copy and paste this example. We are going to use this syntax proto3 here. And in here, maybe we can rename this to Todo. So maybe we can also remove this. So if you want to also keep in hand the documentation, we have different types that protobuf supports. Let me see. But I'm searching for the types. Using the default values-- CRAIG LABENZ: Scalar value types? No, that's just-- oh, you're on the whole list here, yeah. You're looking for a single list that shows all the types. GIANFRANCO PAPA: Right, yeah. Yeah, this is the whole list of-- yeah, maybe there is a section where you can actually search all of the values-- your types. But that's not a problem. I mean, we are going to use basic types here. We're going to use int32 for our ID so we can define the name of the property. And remember to define an actual value that will be an incremental value. This is just for encoding the information. CRAIG LABENZ: It's basically like the order of the fields, right? It's nothing more than that. GIANFRANCO PAPA: Right, of course. So instead of-- right. Like, and JSON would be-- like, you could read it. I know the ID will be this field, key value. And this would be-- the number 1 will be the ID, but nothing else. And yeah, these have to be unique. So apart from that, we can define a title maybe. So this would be number 2. And then we can use another type that is bool to say if this is completed or not. So this is, in essence, our definition of our Todo message. And we are seeing this in the proto buffer notation. But the cool thing about this is that we are going to actually autogenerate this in Dart-- it can be in any language, but we are using Dart-- so you can start sharing your models, not only with your frontend team, but also with anyone that wants to implement a definition of Todo. So this is really an ultimate solution for having a single source of truth for your models because you are sharing the whole model with a lot of-- even languages, programming languages. CRAIG LABENZ: And boy, did we not write much. That is a terse definition of this Todo class, I'll even call it. GIANFRANCO PAPA: Of course, of course. OK. So here, what we have to do is to create another folder that will be generated. And maybe we can remove this. Once it's part of the library, we won't need it. Delete. And here. Here, we can export things later in our library. And here, we are going to actually export our generated classes. So the idea is to grab using the protoc command that you can download it. I mean, we skipped the setup, but the idea is that you can use this setup. Let me see. There is a quick start for this, but-- OK. So let me do a search, "quick start"-- in Dart. OK. So basically, here we have to download the protoc plugin and activate it and then export the path. And then we can start using the protoc command line in order to generate these proto files. So I'm going to copy this command because it's hard to remember. But we can say, OK, in here I'm using the protoc. Your Dart output will be the generated folder. And we are basically grabbing everything that is under the protos folder, each and every file, and we are autogenerating Dart classes. So if we hit Enter, this will generate a lot of classes, a bunch of classes in Dart. And yeah, this is not so readable. But the thing is that you have your models in Dart, and you can start using it. So you have your-- you can actually here create an object that is Todo. Great. So what we can do here right now is to actually export the-- let me see-- all of these files in the library. So maybe in this file, we can export it. So it would be src/generated and todo.pb-- protobuf-- .dart. And we can do it with the other ones as well just in case we need it. But you will find that everything is-- oh, looks like enum and then json. So great. So here, what we did is that we defined-- we have to-- maybe we can remove the example as well. We just defined our definition of our models. And we can start sharing this package in our frontend, in our backend. And we will have access to our Todo object. OK. So maybe another thing we can do right now is to create our backend and in a separate package. CRAIG LABENZ: Sounds good. GIANFRANCO PAPA: Perfect. So maybe we can go back to our full-stack app and create this time server. So I'm just creating another folder. And if we enter to this folder, here I would like to use another template because this would be a package that we can import in our frontend, but maybe we can use the console app so we have our main and we can start our server. OK. So this will be actually the default template. So we hit dart create, instead of specifying a template, we can use the default one. And we are going to force it to be created in this folder, in the server folder. So here, we have our projects. And maybe what we can do now is to import the protos package into our server folder so we can start using it. So because this is a package that we have locally, we have to basically provide the path. And we can actually say, OK, let's grab the path in the protos folder. Great. OK. It's saying that-- oh, we have to add the "publish to none" just really quick so we don't have that warning. OK. Perfect. All right. So the idea here would be we can also-- well, this is going to also explode because we have our calculate method, and inside bin, we have our server.dart that is a main function. But here, what we actually want to do is to, yeah, create the gRPC server so anyone can start connecting with the gRPC server using a client. And yeah, one of the things that maybe we didn't mention is that a cool thing about this protocol is that you can call a function, a remote function, as if you were calling directly in your frontend. So it will be a really seamless communication. We are going to see it, but when we are going to be in the Flutter app, it will be calling the server. It will be like calling a function that we have in our frontend project. It will be really, really great. You don't have to create an ECP request and parse in. It's like you just kind of call the function and that's it. But yeah. So here, we have our main file. I'm going to create the server right now. So this is also some of the code I bring just to speed up this part. So let me quickly copy and paste this. OK. So basically, we have to define a server. We're going to erase this just for now. But this server will come from the gRPC library. Oh, we have to also install this library because we are in the other project. We can actually export it from the proto library, let me see, inside lib. We can export as well the gRPC package. So we can start using it in our server. So if you go here, we can, yeah, create our server. Then we have to run our server in a port. I don't know why-- yeah, dart:html. So basically, we are retrieving the port from the environment. Or as the default, we can use the 8080 port. That's really default one. And then yeah, we have to name this function async because we are awaiting. And finally, we can simply print the server is listening in the port 8080. So I guess we don't have to use this import. And that's it. This is, like, our whole server, our whole gRPC server that we are going to connect in Flutter. And one thing, one parameter, that we are not seeing right now is this array. This will be an array of services. That is something that we actually didn't talk about so far. So maybe we can jump in and talk about the services because, till now, we only defined a model. CRAIG LABENZ: Message. GIANFRANCO PAPA: Right, the message. But we have to define also the functions that we are going to call in our frontend to communicate with gRPC. So to do that, we can get back to our proto file. And instead of defining a message, we are going to define this time a service that will be more or less like an interface that you need to also implement. But here, we can define all of our methods that we are going to use to communicate with gRPC. So maybe we can name this TodoService. And in here, we can use the notation of rpc. We have our method name, our request, and our response. So basically, this is the notation. And I was thinking about getting a simple todo from gRPC. So maybe we can name this getTodo. So here we will-- CRAIG LABENZ: Now, we won't have anything to get until we create something, right? GIANFRANCO PAPA: Right. Yeah. I mean, maybe we can hardcode something just to see it. CRAIG LABENZ: OK, sounds good. GIANFRANCO PAPA: Or maybe, yeah, we can also create-- have a method to create a todo. But let's start with getting a Todo so we can-- CRAIG LABENZ: Yeah, hardcoding is fine. GIANFRANCO PAPA: Yeah, perfect. OK. So in here, what we will have to do is to have a-- so remember the method. We have a request and a response. So we will have to return something. And this is the actual keyword "returns." And we actually have to create another message because we will have to have a request and a response that are also messages. So in this case, to create a-- we can create a message called GetTodoRequest. Maybe it could be by ID. So we can add an ID to the request. And maybe we can put this here. And here, as we did with the message Todo, we are going to provide maybe an int32 so we can pass the ID through the message. If we want to get the todo by the ID, it's a good idea to assign the ID. So here in our method, getTodo, put the GetTodoByIdRequest. And we can return just a todo that we already put it in our-- we already have a message for that. So basically, this method will be sending GetTodoByIdRequest that it has an ID, and will return a simple todo that, in this case, will be hardcoded, yeah. All right. So, perfect. And here, again, as we modify the proto file, we need to autogenerate again the generated classes. So we have to hit again the command line. And let's go back to our proto file-- proto package. CRAIG LABENZ: And normally, if we were running build_runner here, this is when we'd all get up and go get some coffee. But this is a pretty fast generation. You blink, you miss it. GIANFRANCO PAPA: OK. So maybe we can grab the 500-- so we can pass this directly. And here, we can notice that there is another class that was generated because we are including services right now. So in this case, this is not so readable, but it creates all the communication to actually call the getTodo method. So maybe what we can do right now is to export this class so we can have access in our backend. So here, we can-- CRAIG LABENZ: grpc, right? Nice. GIANFRANCO PAPA: grpc, right. Perfect. So I think we won't be creating so much other things in our proto file. But maybe we will create other methods. But we already have a message and a service. And that's it to basically have our very first example connecting Flutter with gRPC. OK. So let's get back to our server. In here, what we want to do actually is to create a todo service so we can expose the service to our frontend. So what I'm going to do right now is to actually just create the service. But the thing is that this service, we didn't create it, because we have our base service that is what gRPC-- that protobuf created, autogenerated. But we have to actually implement the service. So what we can do is to create a service class for the service. We're going to name it todo_service. And here, we can actually create the service. And we are extending the TodoServiceBase. So this TodoServiceBase, what we'll have, we have red highlight here because we are not implementing yet the getTodo method. Remember that we defined our method in our service. So it more or less works such as an interface. You need to implement those methods so we can basically override them. And in this case, basically what we can do is just to return a todo, like a hardcoded todo, so we can have basic implementation of this method that is returning a todo. So one cool thing about protobufs-- if you already have your Todo class, you can use it with named parameters such as here. Or maybe you have different methods such as getDefault. And maybe this is returning no other than the default implementation with, I don't know, like empty parameters. But we can maybe create some basic parameters. Maybe the title, we can do "title." CRAIG LABENZ: For the ID, can we pull it out of the request? Why don't we use the ID that was passed in? GIANFRANCO PAPA: Yeah, that's a great idea. So basically, if we go here, maybe we can put final id request.id. And we will use this ID instead. Perfect. So we will be like-- yeah, the frontend will generate the ID, and we will return a new Todo with that ID. And then we have the completed param that is missing. This will be false. Maybe what we can do also is to put in the title the ID so we can have our pristine title, in a sense. And yeah, another thing that we can do is to actually return the todo, because this is returning a Future of Todo. So here, what we are missing is the async keyword. And that's it. I mean, we are implementing this getTodo method and returning it. And so yeah, this is our definition. And now we can make use of the service inside our server file. So this will expose this to the service in the gRPC server. And we can start using it in our frontend app. And that's really it. OK. So the last thing we need to do, actually, is to create our frontend app, Flutter app. So we have everything running. We'll need to, in this case, create another folder. So maybe you can create a client app in Flutter. And in here, we are going to create a basic app in Flutter using the template, the create template. And as I'm running my example in a MacBook Pro, maybe we can create it using Android, iOS, and MacOS. We could use the web, but it's not going to work, because the thing is that to deal with gRPC in the web, we have to do some special things that it doesn't have full support yet out of the box. We have to do some more advanced things. But yeah we can definitely test it in mobile and desktop, OK? CRAIG LABENZ: Sounds good, yeah. GIANFRANCO PAPA: So maybe for the name, we can choose app. I'm going to use the Somnio organization. And then maybe the platforms, we can choose, as I said, Android, iOS, and MacOS. OK. Let's create everything here. OK. So this should create our app in Flutter. Really basic stuff. So we have our folders ios, android, macos. And we have our pubspec.yaml. So maybe what we can do right now is to clean a little bit this pubspec.yaml. So what I can do is to use some regular expressions to actually clean up this. So let's put-- I think it was-- yeah, the notation was just like this, right? Oh, you have to select the regex, right? CRAIG LABENZ: Yeah. You're looking to get rid of all the lines? Yeah. So .*, there you go. Yeah. And then-- nice. Is it not autoformatting for you? Oh, so generally I replace that with a new line because sometimes if you don't replace it with a new line, it can squish things up. GIANFRANCO PAPA: OK. And we are going to do the same for our main file. So here, let's replace this for the comments. CRAIG LABENZ: You know, I found-- I discovered the other day the --empty flag on Flutter create. It just doesn't put all this gobbledygook in the files for you. GIANFRANCO PAPA: Oh. So there is a flag to do that? CRAIG LABENZ: Yeah, flutter create --empty. You've already done the heavy lifting. But for the future, --empty. GIANFRANCO PAPA: Maybe we can see it. Yeah, here it is. Specify, no comments. Perfect. Yeah, that's a nice tip so we don't have to use regex to clean everything. Right. So here what we have to do is to actually import our protobuf-- proto package, as we did with the server. So maybe we can quickly put protos. And also, the path would be the same. Perfect. So we can have access to everything. And just for speeding up the example, we are not going to use any kind of state management or repository pattern or something like that. I think that we can just use the stateful widget that we have. CRAIG LABENZ: Use our imaginations. GIANFRANCO PAPA: Yeah. And yeah, probably there are best practices to follow here, but I'm going to speed up. And we kind of would like to have a client channel so we can start communicating with gRPC. For this, we need to have a client channel. And we can have this property. Maybe what we can do is to import the package protos. Protos? Yeah, because we are exporting the grpc package in that package. So we can then initialize this channel in our initState. So let's create our initState. And here what we can do is to create a ClientChannel here. And the host will be, in this case, localhost, because we're working in our machine. And the port, remember, we choose 8080, although it could be the one that we are providing to the server through the environment variables. And we also have to pass-- let me see-- in the ClientChannel the channel options. So for this, we are going to go for the ChannelOptions, like the insecure mode. But this will depend on if you want to provide some SSL certificates. But we are going to go with the fastest. And let's put everything on const. OK. So here, we already have our channel. And what we need to create after right now would be the stub. So the stub would be just an abstraction, a proxy that we can use to communicate back and forth with gRPC and call, actually, our methods. So what we can do here is to create our stub. And to do that, let's quickly review how to use that. So basically, we will need to call another autogenerated class that we have that would be the TodoServiceClient. So let's put here late TodoServiceClient. And this will be our stub. So we can also initialize this in our initState. So here, what we can do is, basically, TodoServiceClient initialize and send the channel. And that's it. We have our stub, and we can start using it to interact with all of the methods available like our getTodo method. So yeah, let's hide the terminal one moment. So well, we can notice that here we have the basic counter example. So what we can do is to maybe put a fill here to-- maybe what we can do is have the todo here. This won't be late. So we can display that todo in our main screen if we have one. So that's why it's nullable. But yeah, in our counter, this is really setting our state of our counter. We are going to delete our counter because we don't need it-- also this. And here, what we can do is get our todo from the server. So we're going to replace this in our icon. CRAIG LABENZ: Nice. GIANFRANCO PAPA: And here, maybe we can say, OK, if our todo is different than null, we can display maybe a text with the title. OK, we have to use the-- right. So this is here. And else, we can display maybe a text that it would say, "get your todo." CRAIG LABENZ: I love it. GIANFRANCO PAPA: Perfect. Really simple. We are basically testing the whole connection. And OK. So maybe what we can do also is to put this in a column so we can display more information. Actually, maybe we can display also the ID and whether or not this is completed. And so this information will be an integer, so we have to parse it, and this as well. This would be a Boolean, so perfect. I think this is complaining about the-- OK, the const. Perfect. OK. So basically, if everything works, we will be able to see actually the todo in our Flutter app only after we click on the getTodo button. Perfect. So how would we do that? We have to, on one hand, start the server. So we are going to move to the server folder. CRAIG LABENZ: Starting the server sounds very helpful. GIANFRANCO PAPA: Yeah, totally. So we have to-- if we go back to the server, we can see that we have our bin folder. And inside here, we have the server. So we can simply do dart bin server.dart. And the server should be starting. OK, the server is listening on port 8080. So yeah, it's not like we can go to Postman and start testing in Postman. It's more difficult because, yeah, this is another protocol. But we can run our app and see if everything is working. So here, I can open another tab, and we can go to our client. So here what we need to do is to run our app. And I'm going to run it first on MacOS. CRAIG LABENZ: Yeah, MacOS-- absolutely the easiest way to get started. No emulators. GIANFRANCO PAPA: No, yeah, you don't have to start it with emulators. That's right. But we can actually test it in iOS or Android. The thing with MacOS is it's not going to actually work the first time, because we have to configure something-- a special parameter in the configuration. But yeah. CRAIG LABENZ: [INAUDIBLE],, that's what it's called? GIANFRANCO PAPA: I think it is something like that, right? CRAIG LABENZ: Yeah, to be able to send network requests. GIANFRANCO PAPA: Yeah. So I'm going to show that this is going to fail, but then we are going to fix it. So yeah, so far, I mean, that is the whole example. Another thing that we are going to do that is really especially important in gRPC is that we are going to be capable of streaming different things from the server. So we are going to push information into our client. And that's really nice. It has a lot of use cases. So here, we have our "Get your todo." And if we hit the button, this won't work. But yeah, actually-- CRAIG LABENZ: Are you not getting an error for this? Shouldn't it be-- GIANFRANCO PAPA: I'm not getting an error, yeah. CRAIG LABENZ: Oh, because you didn't put anything in getTodo. GIANFRANCO PAPA: Oh, we didn't put the method, OK. So here what I need to do is-- CRAIG LABENZ: I thought I missed that. GIANFRANCO PAPA: Yeah. Here, we need to actually get our todo. So here we need to pass the todo request that we created. CRAIG LABENZ: We kind of do need that counter, actually. GIANFRANCO PAPA: Yeah, right? Maybe we can create a random ID here. CRAIG LABENZ: All right. GIANFRANCO PAPA: So maybe-- I think it's Math.random. I have to export from the-- CRAIG LABENZ: Whoa. While you're typing that, Postman added gRPC support. GIANFRANCO PAPA: Oh, I didn't know that. CRAIG LABENZ: That's crazy. GIANFRANCO PAPA: So someone is saying-- CRAIG LABENZ: Yeah, that's cool. GIANFRANCO PAPA: That's very cool. I have to test that, yeah. CRAIG LABENZ: Yeah, thanks for the tip. GIANFRANCO PAPA: So here, let me see Math-- probably there is a library. Oh, it's Random, I think, plugin, right? CRAIG LABENZ: Yep. I think you have to import math for it. GIANFRANCO PAPA: Right. So nextInt-- maybe we can have 100. So we can use the ID here. OK. So there we are getting the todo. And we have, of course, to await this. And then we can set the state of our todo to this.todo, right? CRAIG LABENZ: Oh, right, because you added the keyword "final" there. So it actually had not set it on the widget yet. Nice. GIANFRANCO PAPA: Right. OK. So now, again, if we restart, we are going to actually do something in our getTodo button. And let's go back to this. And here, we have the error. OK. So to solve this, we can enter to the-- CRAIG LABENZ: Yeah. And it said operation permitted, socket stuff, blah, blah, blah. So when you see that, you've got to do what Gianfranco is doing now-- open up Xcode. Or are you going to do-- GIANFRANCO PAPA: OK. So let's quickly open. No, I'm going to open the Xcode project. CRAIG LABENZ: I thought you were going to show me the greatest trick of all-- not opening Xcode. GIANFRANCO PAPA: Not this time. Right. CRAIG LABENZ: Not today. GIANFRANCO PAPA: So here, yeah. Here would be signing and capabilities, outgoing connections. So we have to restart everything, but this time it's going to work. And yeah, that's the whole example. Let's wait a little bit. That is loading the MacOS up. And the other thing I wanted to show, as I was mentioning, is how we can start streaming information from the server. So what we are going to do is to actually stream a todo instead of getting one todo and that's it. But let's first test this. OK. So this is working. We have our todo with a random ID. So we can keep going, and that will produce a random todo. Really basic, right? OK. So the next thing will be to stream-- CRAIG LABENZ: So before we move on-- I mean, streaming is going to be very cool, but I want to just recap kind of what we've done so far because we've been at it for about 50 minutes now, but a lot of that's been explanation, looking stuff up. You've written almost nothing. And we have completely strongly typed communication with the server. On this very show, on "The Boring Show," we've had shared code going from the client to the server, which, by the way, took more time than this to get working. But even that was not strongly typed. But here, the fact that-- the classes, of course, are strongly typed if you're sharing code. But the network requests themselves were never strongly typed before. And that is such a cool thing here. So if we change-- never before has it been true that if you change how your server works, your client will get type errors to remind you, oh, I deleted that endpoint and I renamed it something else. That has never been a thing before. So this is really, really neat. And it's super performant. And, like Gianfranco has said, we're about to start streaming. My dog is making a lot of noise here. Yeah. Really, really, really neat. Just so cool. GIANFRANCO PAPA: Yeah, that's a really nice summary. And yeah, this was really fast mainly because of the autogenerated files that we have. We didn't have to create our Todo class or service. These are also autogenerated. So that's really fast here. But yeah, to give a little bit more about more cool use case, we're going to stream this todo. So we can go back to our-- maybe to our proto file again, where everything happens. So if we go to our proto package, maybe what we can do here is, instead of having a getTodo that returns a todo, we are going to return as streams-- or a stream of todos. So maybe we can rename-- we can create another method that will be getTodoStream, maybe. And yeah, the actual request would be the same. We can pass the-- well, it won't make sense because, actually, we are not going to create a todo and send it to our client. What we are going to do is kind of have a stream of todo and send it and push it to our client. So maybe we can create a separate method that will be creating our todos and sending to our client in streaming. CRAIG LABENZ: Yeah. We could also-- the stream could just-- once a second, it could just add a random todo to the stream. And then the frontend could maybe store them all and show them in a list view or something. GIANFRANCO PAPA: Yeah, we can do that. So what we have to do here is to provide a stream of todos, and that's it. That's really simple. We only need to modify using this keyword. We can actually also stream here from-- things from the client. Maybe we can find a use case for that. But yeah, we can either have a single call. We can have streaming from the server, from the client without the server, or both. So that will depend on your use case. But you have this bidirectional communication that you can get an advantage for that. So what we need to do right now is, of course, to go to our command, protoc command, again and autogenerate-- CRAIG LABENZ: Do you-- are you intending to leave the "stream" keyword in the parameter on line 15? GIANFRANCO PAPA: Oh, that's right. Yeah, maybe we can start simple with only streaming things from the server. So now if we go and see about I can generate the proto out, we can see that our server has an error because we have to re-implement-- implement the other method. CRAIG LABENZ: Oh, I love these errors. Hey, you forgot something. Just the best. GIANFRANCO PAPA: So maybe we can create the missing override. And in this case, it is the exact same method. We have our service call and a GetTodoByIdRequest. But we have a stream of todos instead of a Future. So here, what we will need to do is to actually build a todo instead of returning. So what we can do is maybe-- what would be the best way to do this? So maybe we can have a todo controller. So here, we can use-- CRAIG LABENZ: We could just have a loop that awaits a duration of one second and then emits another one. Like, it could be while true. GIANFRANCO PAPA: So OK. So we have a while true again. Perfect. While true-- CRAIG LABENZ: Yeah, keep it real simple here. GIANFRANCO PAPA: Yeah. And we can yield a todo. So maybe we can grab-- CRAIG LABENZ: Just copy. GIANFRANCO PAPA: --everything from here. CRAIG LABENZ: Well, maybe a random number. GIANFRANCO PAPA: Right. So maybe here-- CRAIG LABENZ: Although it's funny. The request specifies a number. So our hypothetical is getting a little fuzzy here, but bear with us. GIANFRANCO PAPA: So maybe here we can, yeah, nextInt. CRAIG LABENZ: nextInt, yeah. GIANFRANCO PAPA: And 100. And yeah, we have our todo here. And we need to yield this todo. So actually, for doing this, we have to-- CRAIG LABENZ: Async*. GIANFRANCO PAPA: --have the async*, yeah. CRAIG LABENZ: And for anyone who's not aware, async*-- first of all, there's a great series on YouTube called-- nope, I forget the name. But Andrew Brogdon, one of the original Flutter DevRel people, has a five-part series on YouTube. I think the series is called "Flutter in Focus." Anyway it's asynchrony and Dart, goes through all this. But async* means that, A, your function returns a stream, and every time you use the "yield" keyword, the object that you yield just goes on the stream. And so that's what we're setting up here. GIANFRANCO PAPA: Perfect. So yeah, we forgot about the delay. So if not, this is going to provide a-- CRAIG LABENZ: Oh, it's fast. GIANFRANCO PAPA: Yeah. So let's delay it a little bit this. Maybe a second would be reasonable enough to notice. So maybe we can put it-- CRAIG LABENZ: Perfect, yeah. After the yield-- either the beginning or the end, honestly, either one. GIANFRANCO PAPA: OK. Perfect. So here, we need to modify our client, our Flutter app, so we can start receiving this messages from the server once in a period of a second. So if we go back to our main file, we had our todo here. And we will need to have a stream of todo this time instead of a single one. And maybe what we can do is to then create a stream of todos. So maybe-- CRAIG LABENZ: I was going to say, I haven't used the StreamBuilder widget in so long. Foresee it in our future. [LAUGHTER] GIANFRANCO PAPA: Yeah. That's right. I mean, I used to work a lot with StreamBuilders before using the Flutter Bloc library. But then it kind of was pointless because Flutter Bloc kind of hides the whole implementation. But yeah, back in the old days-- CRAIG LABENZ: It's still a StreamBuilder. It's just well disguised. GIANFRANCO PAPA: Right. So yeah, back in the old days, I used to deal with a lot of streams and [? abrupt ?] [? starts. ?] So yeah, this was like remembering how I used to work, in a sense. But yeah, the todo stream, maybe we can put it as null also so we don't have to initialize this here. And what we can do is to initialize our todo stream. But let's put it private. And we, of course, have to call the stub, right? So the method would be getTodoStream. And our request would be the same one that we use here. We can copy. And now, this idea would be pointless, but we would just have to put it there. And here, what we will have is our stream of todo. So what we can do now is to have a StreamBuilder. Instead of our Column, maybe we can put here a StreamBuilder. And let's first provide the stream. The stream will be todoStream. And then we have to provide the builder, right? So for the builder, we need the context and the snapshot. This is getting really long-- one line. But I think that's it. OK. So here in the snapshot, what we can do is to actually see if there is any data. And else, we can print or show, maybe, "Loading," because we will wait-- yeah, we have to cover that case because nothing else is in the stream. So if this has data, we can probably grab our todo from the stream. So let's do that. And we can display a column with our todo. And you have to actually return the column this time. And that's it. I don't know if we have to actually parse this or we did already. CRAIG LABENZ: Yeah, I'm not going to lie, I've not done this in a minute. So I'm not really sure either. Let's find out together. GIANFRANCO PAPA: Yeah. So here, we can remove the null operators. That's cool. And here, I have an error. Let me see. Oh, I think I didn't return this. CRAIG LABENZ: Oh, return. Yeah. GIANFRANCO PAPA: Perfect. CRAIG LABENZ: I think this is going to work. I think this is right. GIANFRANCO PAPA: OK. Let's cross our fingers. So I'm reviewing a little bit. This getToDo one will call-- I mean, can be called, but it will be the other method. And we are calling directly the stream and using a StreamBuilder to present the data. So if we go, we can-- Let's rerun our server because we made some changes. CRAIG LABENZ: Good idea. GIANFRANCO PAPA: And let's rerun-- oh, we could actually have used the-- called restart. CRAIG LABENZ: Probably restart it. Yeah. GIANFRANCO PAPA: Yeah. But that's my bad. CRAIG LABENZ: It's called a cold restart. GIANFRANCO PAPA: Yeah. So here, yeah, if everything works, we are going to see really fast-- not really fast, like one second-- how random todos are heading to our app. So apparently it worked. CRAIG LABENZ: Nice. Well done. Flawless, by both you and gRPC. Maybe the G stands for Gianfranco. Have you thought about that? GIANFRANCO PAPA: It could be, right? Yeah, that's another coincidence. CRAIG LABENZ: Yeah. No, we'll have to check. We'll have to ask. Look at this. We are streaming data from the server. This is so cool. Think about what you have to do to get this functionality any other way. You have to definitely use a WebSocket. And that's not the end of the world, but, whoo, boy, was this easier. And like you said, it feels like you call a native method that's just locally available. We're just dealing with a stream on both ends. And then all of the guts to serialize stuff and get it on the wire and manage the connection-- never had to think about it. Never thought about it at all. And it was type safe across the boundary, across the network request boundary. Just really, really neat. Really cool. GIANFRANCO PAPA: Of course. OK. So yeah, that kind of wraps up the whole implementation of the full-stack app, like with our client, with our server, with our proto package, everything. We can share this package between client and server. We saw the implementation of a method that is streaming information. Maybe what we can do now is test the other streaming capabilities. We mentioned that you can not only stream something from the server, but you can also stream from the client. So you could potentially start streaming things from the client to the server. I don't have a really well-thought example of this, but it could be something useful. So-- CRAIG LABENZ: No, I think folks get it. Yeah. No, I think we get it. But if you did that, on the frontend you'd just have a stream, and you'd just add stuff to the stream, and then it would show up on the backend? GIANFRANCO PAPA: Right, yeah. Yeah, totally. I mean, you will be basically initializing your stream in your frontend. And then you will be streaming information to the backend, gRPC, and then something will happen. You can log everything, print it, we can see it, or yeah. I mean, we can either do that to test. Or another thing I was also wanting to show is how this can work in different platforms. Maybe we can open iOS as well and play around. Maybe we can send messages between both of them because, here, we are streaming information to one client. But maybe we can stream information to multiple clients at the same time, and they all are going to see the same todo instead of different ones. CRAIG LABENZ: Right, right. Well, I think that is also a pretty good-- a good example. But I think we've covered stuff quite nicely here. There are a few questions that we should-- we could get to. Folks have asked a couple of good ones. And I think that is probably a nice way to finish. In terms of on iOS and Android, it's going to look just like this, you know, which is amazing. But there shouldn't be like a big jump there. So we did get one comment earlier. I kept referring to the message as a class. And Tokhchukov says, "message is more of a struct than a class, I think, as it doesn't contain any logic." And I think that's a nice way to put it. The message itself is definitely a struct, but it is mapped one to one to a class after you run the protoc command. So the boundary between the message and the class is-- it feels thin sometimes. But I think you are technically-- that is a better way to put it, for sure. Now, Gianfranco, I think the next few here, I'd love to get your takes on first, and then maybe I'll add commentary at the end, whatever I'm-- whatever thoughts I might have. But I think there's some good questions here. So Tim asks, does state management-- Bloc or Riverpod or others-- work the same with gRPC on the frontend and the backend? So I think this question is like, how does using this impact your state management? GIANFRANCO PAPA: Right. I mean, I know that in our client app, we actually only present a simple example where we play around with a stateful widget. But when you deal with this, you have to actually have a state management solution such as Bloc or Riverpod. And if you're following a good structure or good architecture, that won't impact really, because your bloc will be kind of calling probably a repository, and that repository will call ultimately gRPC by using what we saw that is a client channel. So if you have everything in place, you can still use Bloc or Riverpod in Flutter, and that won't be-- I mean, the whole point of bloc is just to uncouple your frontend-- your presentation layer from whatever data source you have. So if you are using REST or WebSockets or gRPC, that would be the same. So yeah, that won't impact, yeah. CRAIG LABENZ: Nothing to add. Well stated. OK, next question. This one came up a couple of times. Oh, actually, let's stick with one from Tim here because it's a little more related. Then we'll jump to some other questions. Tim asks later on, "how are your typical status codes returned in a traditional REST API architecture?" So I interpret this as saying, like, how do I differentiate from a success, from an error? Normally, you can check, is it a 200, is it a-- if I created something and maybe expect a 202 or 204, or kind of however you set it up-- if it's not there, it's a 404. How does that work in this setting? GIANFRANCO PAPA: Right. Yeah, I interpreted the same because you clearly have status codes in REST. And every one-- each of them are really telling you if you're not authenticated, or if you have, I don't know, different errors such as 404. But in this case, I think that you of course-- I mean, we didn't-- so error handling, there is a separate documentation for that. But you can always throw your exceptions because you will have the same problems. Like, you will have to authenticate the user. So you can-- if the authentication is not working, you can throw a gRPC error, and you can handle that into your client. Or you can send custom exceptions. You can always do that. But I don't think that there is a map between status codes to actually gRPC. But yeah, definitely do that. Have maybe-- we didn't see this, but maybe we can have an interceptor that is authenticating each call if something is not working, I mean, you're not authenticated, you just send a gRPC error. And then if the user is authenticated, you can perform your logic and throw custom exceptions. What it could happen is that the channel connection is not working. So you have to handle that in case you are, for example, streaming something. So you might present something to the user. Actually, I remember-- we didn't also cover this case, but remember, when we opened the channel, we could have closed it whenever the stateful widget is exposed. I mean, that wasn't the case, but whenever we exposed the widget-- CRAIG LABENZ: You would certainly need to do that, yeah. GIANFRANCO PAPA: Yeah. You will need to close the channel so you don't have something streaming unnecessary things. And yeah. CRAIG LABENZ: Now, part of the way this works where Google uses protobufs-- so like, for example, Firebase SDKs. All the communication happens using protobufs. And instead of just returning a todo, which was a perfectly reasonable kind of shortcut for this demo, instead of returning a todo, also if you look at the documentation, you'll see something like the whatever request and the whatever response. And so instead of just returning that raw object, you can return something that wraps it. And then you can have an error code in there. The error code can be its own enum, essentially, which is also a gRPC object. And then you-- GIANFRANCO PAPA: A proto buffer. CRAIG LABENZ: Yeah, exactly. Yeah. And then on your client you'd check, did I get my .todo, or did I get my .error? And you wouldn't necessarily-- you could if you wanted, I guess. You could set 200 equal OK and 404 equal not found if you just kind of wanted to keep that. But however you set up your errors at that point. All right. Another question. This came up a couple times about data migrations. As your data evolves over time, you add new fields-- I think this was the first time I saw it in the chat. Daniel says, "What's the recommended way to handle API changes, for example, adding new properties to models or changing optional to required? Is there a way to version these changes?" GIANFRANCO PAPA: Yeah, actually, there's a way of versioning. In the documentation it says. But yeah, it's true that whenever you make some changes, you will impact into your clients that are using the same protobuf models. So I think you should see what is the best strategy. This is always something that is a problem, especially if you're using, for example-- in REST, it's also a problem because if you add something, you have to-- you will have your client that is breaking because it doesn't have the last parameter. But in this case, as everything is autogenerated, at least your client will notice that change really soon, as we saw. Like, everything is typed. So yeah, I guess versioning your protobuf would be a really good solution to implement. I think I'm not hearing you. CRAIG LABENZ: Yeah, sorry. I muted myself. GIANFRANCO PAPA: Were you-- CRAIG LABENZ: My chair makes noise. Yeah. So yeah, Pascal asked this question with another good angle. He said-- this gets at an awkward point when you're always talking about updating a distributed system that has to talk to itself. So how does this deal with updates to the model? Will old apps be able to talk to the updated server? So yeah, what about people who don't click that Download New App Version and they're running the old protobuf? I know some thoughts about how this is handled at Google, but I'd love to first hear how you handle this at Somnio. GIANFRANCO PAPA: Yeah. Because I mean, the thing is that for me, something that we didn't mention is that this kind of gRPC is also used in microservices architecture. We saw an example of using a client and a server, so really communicating the frontend with the backend. But this is really how lots of systems that uses microservices architecture-- it's really handy because, as we saw, you can-- especially microservice architectures where you can really choose whatever language you need for your use case, that's really handy because you will be sharing your definition, and you can share it across multiple programming languages. So yeah. In our case, we deal mostly with Dart. So in our case, it's really easy because when something changes in the protobuf, maybe because of the server change, you have to only update your Flutter app and that's it. But I guess that if you're having a microservice architecture, you have to have that strategy well placed because maybe not all your services need to update. Maybe they have to continue working as they did without new definitions or changes. So yeah. I think that there will be a real problem mostly in your microservice architecture, especially in the backend, but-- CRAIG LABENZ: Yeah. A way that this works at Google is, essentially, required fields are swear words. And you're not allowed to add required fields because you can't trust when clients will be updated-- even a web client. Someone could just have an open session and not refresh the page. And if the server changes out underneath them, all future visitors will be fine, but current viewers could be totally hosed. So no required fields are allowed. Now, that creates its own thing where now you can't trust any of your fields, even if you know this field is actually required. I just pretend it's optional for backwards compatibility. Do you really want to carry this burden into perpetuity of, is the required field there? Oh, weird, it was again. So you could version it for sure. You could just add a new endpoint that returns the response v2 or something, and it returns the updated model with the required field there, and then old clients just keep calling v1. And once in your logs you haven't seen v1 called in a month, then you can finally delete it or something like that. But that's a tricky thing, for sure. All right. There was another question, this one a bit lighter. Whoo. Wojcieszekk? I don't think I was close there, but I wish I was closer-- said slash asked, "I thought of creating a chat in Flutter using gRPC. Is this a good idea?" What do you think? GIANFRANCO PAPA: Well, I mean naturally, you will think about that because you have a bidirectional support. So yeah, if you're dealing with a chat, the most common way to have, for example, a frontend communication with the backend is REST, right? And yeah, REST is not a particularly good use case for a chat, because you have to send information between your frontend and your backend so that it can be actually pushed to another client that you need to chat. So in this case, as we can push information, this could be something that you could actually implement with gRPC, why not, because-- well, we didn't see it. But if we started another client, we can actually play around a little bit and share the same streams so we can push messages between one client and another one. It could be done. But yeah. Also, I guess that a more subtle analogy for doing that would be also WebSockets. So you can also try WebSockets to have that communication in a chat. CRAIG LABENZ: Yeah. I would say in a world where gRPC didn't exist-- in a world-- there would be-- WebSockets would be essentially the only kind of sane way to do things. Like, if you just tried to use old-school HTTP requests and pulling and whatnot, it's like, you'd have the worst chat app ever. So WebSockets would always rule the day. But I think gRPC is essentially-- you get that kind of low-latency immediacy of WebSockets. And it's just like-- it's not the same as WebSockets. The implementation is different. And where the 1's and the 0's are on the wire is different. But that doesn't really matter for most of us. I honestly just think of gRPC as WebSockets where I got a lot of code generated for me and it's type safe across the boundary. I mean, it might literally be on an actual WebSocket under the hood and then-- I don't really know how it works. That's one of the great things about gRPC. Like, you don't have to know every single part of the stack all the way down. But it just-- it's really, really, really efficient in a way that we kind of all expect out of a chat app. So I think it's a great choice for that. GIANFRANCO PAPA: Yeah. I guess that's right, that it will be more efficient, probably, in your mobile phones, that maybe-- like nowadays, you could really have a really good mobile phone, but there are other ones that-- other mobile phones that are not so advanced. So you can push the binaries instead of string-based messages more faster. And that would be a huge improvement in terms of efficiency by only using gRPC. CRAIG LABENZ: Yeah. Absolutely. Well, Gianfranco, I think we have gotten to the end of the questions. And the demo was perfect. Everything worked first try, except when we knew it wasn't going to work because of the entitlement. So bravo there. And, man, I had a great time. I hope you had a great time. I think our-- I think the viewers had a great time. But this seems like a good time to call. Any last thoughts, Gianfranco? GIANFRANCO PAPA: No. I mean, yeah, I had a great time. I really enjoyed it. I was actually doing a live demo, which was pretty cool. And yeah, any last thoughts-- I don't know. I guess that maybe if you haven't tried this technology, I guess this is a really great starting point for doing that. It really caught my eye, especially because of the, basically, sharing code between frontend and backend. As Flutter developers, we always struggle with, OK, we have to deal with a different programming language in another backend. So why can't we use Dart? And yeah, that's why I keep exploring different alternatives for doing everything full stack. And in terms of gRPC, you can really use Dart or whatever language you like. So that's really another point on top of gRPC for the advantages we named. So yeah. And for me, it's really cool to autogenerate these things. It works like magic. And you can share your proto files, and that's awesome. So if you can try it, give it a try because it's a cool toolset you could have it. And as you said, Craig, this is used by Google, so it will let you know how they work. So yeah, I really enjoyed to be here, and, yeah, hope to be here soon. CRAIG LABENZ: Awesome, yeah. Well, I think that's a great place to leave it. So everybody, I think I'm going to be off next week doing some prep for Google I/O coming up in May. But we'll be back after that. So until the next episode, have a good one, everybody. GIANFRANCO PAPA: OK, bye.
Info
Channel: Flutter
Views: 17,780
Rating: undefined out of 5
Keywords:
Id: jCbclWBV32o
Channel Id: undefined
Length: 89min 14sec (5354 seconds)
Published: Thu Mar 23 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.