[APPLAUSE]
PAOLO SOARES: OK.
So I'm Paolo Soares, and I'm a software engineer
at the AdWords Express team at Google.
And today I wanted to share with you
how we got to share around 50% of our client code
among the Flutter and AngularDart apps.
And yeah, it doesn't seem like much,
but I'm not including all the Google infrastructure,
because if I did that, I'll get this crazy high number,
and you'd not believe me, so 50%.
Google has a lot of apps, and few of them
you may not have heard of them.
They are AdWords and AdWords Express.
Yes, there is an AdWords Express,
and yes, they are different.
But they have some things in common.
And two of them are, they make a lot of money.
And the other thing is they are built on AngularDart.
And they also have Android and iOS native apps,
which means that we have three code bases.
Awesome.
Awesome because I get to do the same thing three times,
every single time, and hate myself a lot.
So because we don't really like hating ourselves,
we are constantly trying to find ways to share
more code among those apps.
And Flutter came out.
And we were like, this is awesome.
This is awesome because first, Flutter doesn't--
OK, not going to use that word, but Flutter is good instead not
like Android and iOS.
And it is an elegant, efficient framework
that will let us have a truly single code base for iOS
and Android apps.
And because Flutter is written--
you write Flutter apps in Dart, we just
happen to have these gigantic web client
lying around that is written in Dart.
So I thought, hmm, maybe all I need
to do is import the model from the Angular app
and pass that to the Flutter app,
and go have a beer or something, because I'll be done.
But of course, that's not the case at all.
And that's not the case at all for a lot of reasons,
but the important reasons are: first,
we cannot just import the model, because in reality,
the business logic is all over the place.
And we as software engineers, we like
to think that all apps are like this.
You have the UI, you have the model,
and they have clear boundaries, and the interactions
are simple and clear.
But in reality, you don't really have that.
You may even start with this, but over time, slowly
but surely, you get this.
And you may tell yourself, no, that doesn't happen for me.
Well, you go to the AngularDart website
and go to the getting started tutorial, and you have this.
This is the main app component.
It shows a list of heroes.
And that's very simple, right?
You have a list of heroes.
You can select a hero.
That's it.
Well, there's a bunch of business logic on this thing.
The first thing is, it knows how the heroes are fetched.
It knows how many heroes can be selected.
Let's say I want to change this app
to select more than one hero.
Now I have to change the Angular application
and I have to change the Flutter application.
And lastly, it knows that to manually select a hero,
you have to change the selected hero, which sounds redundant,
but it really isn't, because in practice, you
need to do more stuff, usually.
So even this very simple four line component
has its problems.
So yeah, we have no hope.
Second thing is, remember that infrastructure
thing I talked about?
It was written for the AngularDart app.
And because it was written for web,
it relies on a bunch of stuff that
is not available with Flutter.
For example, like the browser window
to get the correct location, or the JavaScript
context to do some caching or event pass.
And it doesn't seem like much.
I mean, you could say: just rewrite those things
and be good.
But turns out that our infrastructure is quite big,
and it has a ton of stuff that we actually want
to use for many, many reasons.
Among them, API clients, crash and performance monitoring,
activity tracking, and some secret sauce.
And if I want to launch this thing before 2050,
I should not be writing stuff.
So OK, we can't just use infrastructure.
That's bad.
And lastly, we can't just import the model,
because yes, Flutter is magic.
But you know what?
Angular is more magic.
[APPLAUSE]
And it all boils down to state management.
In AngularDart, you don't really need
to bother about what happens when state changes.
It magically, sort of, not really,
it magically is going to update things for you.
But Flutter tells you, you know what?
We need to be efficient.
And if you want to be efficient, you
need to call "setState", basically.
And again, the model was implemented
for an AngularDart app.
And that means that whatever interface it has,
it is not optimized for this use case.
So now I have to write adapters to wrap the Angular model,
and I'm going to have this bloated Flutter app,
and I'm going to hate myself again.
And this time it's going to be my fault. So, no. Oh, no.
Hopes dashed.
But hey.
[LAUGHTER]
Look on the bright side.
We get to do some refactoring.
And we as software engineers, we love refactoring,
because our code is always awesome,
and the other people's code is not awesome.
So by definition, when I rewrite something,
it's going to be better, right?
[LAUGHTER]
But we cannot rewrite everything from scratch.
So, oh no, we have to live with other people's code somehow.
So what I am going to do now is I'm going
to go through the Flutter app.
And I'm going to use Flutter instead of Angular
because of "setState".
So whatever we do, it has to be compatible with Flutter.
And then let's see what I have to do
to get this thing working.
And I make a mistake of live coding.
OK, so I have this app called Purrveyor.
It's a search engine for cats.
It is a heavily untapped market that we really
need to do something about.
So here you can search for basically what, tuna?
And then you get a list of search results,
right? like non-GMO tuna, organic, less fat.
So this is a very simple app.
Oh, the font is too small.
Let's see.
Preferences, Fonts.
4, Apply.
That better?
OK.
So let's go straight thru this search screen, like the tuna,
the fish box, and the results.
So we have this stateful widget.
And I'll tell you why it is stateful soon.
And it is very simple.
You build the input.
You build the preamble, which is this string results
for tuna.
And you build the results.
And the way you build the input is you
have this SearchQueryInput widget that takes "onChanged".
And then you change the query.
You have to update the query.
And you search as you type, so you "_performSearch".
And you build the preamble by just interpolating the query
with the "Results for:" string.
And you build the results by just creating a Column
with mapping the results to SearchResult widget.
So that looks really clean.
That looks OK-ish, but that doesn't cut it.
Because, again, that's a bunch of business logic in here.
It knows, for example, by having this "_performSearch" on change,
you're incorporating this knowledge
that search happens as you type.
And by having this preamble here,
the widget knows how to build the preamble.
So again, I think that the benchmark
is like, if you want to change something on the app
and have to change it at two places, that
shouldn't be two places.
They should be at the same place.
It's probably business logic.
So that's the case for the preamble.
So if you needed to change the preamble for the Flutter
and AngularDart apps, with this design,
you need to change both of them.
So, yeah, NO. Right?
And "_buildResults" seems OK.
Nothing bad happening here, I guess.
And "_performSearch" is really bad.
Again, because it knows that you have to use
an API to search the results.
And when the results get there, you have to "setState".
And we still have this stuff here.
We have this "BinaryApi", which doesn't exist in AngularDart.
And then you have to keep track of the query in the search
results.
And similarly, if you have data in two places,
like let's say we extract these to a businessLogic class.
But now this data is in two places.
You have to maintain the create query on the widget,
and maintain the create query on the business logic.
Now you have data in two places.
And believe me, they're going to get out of sync,
and it's going to be really awful to track that down.
I've been there multiple times.
So yeah, this is bad.
This looks sort of OK, but it is bad.
So one exercise I like to do when
I'm trying to refactor or redesign the API is I go here
and I just start throwing stuff away and rewriting stuff.
And it's not going to build, of course.
I hope so.
It shouldn't build.
And basically, I just want to see how
I want the code to look like.
And then from there, I try to make it actually happen.
So OK, I don't like the API, these variables here.
I will remove them.
I don't like this that I have to do the "_performSearch" on query.
So let's say I have this businessLogic class,
and then I do "onQueryChanged".
And when building the preamble, again,
I just have this businessLogic class.
And I've got the preamble before that.
And I also get the results on the businessLogic.
And that means that I don't need to have these anymore.
The widget doesn't know how to perform the search.
And, see? It's much more concise.
And let's say I have this class called SearchBusinessLogic.
That looks better.
That looks better because now, basically,
the widget is really dumb.
It doesn't do anything besides just display the data.
And if I went to change, for example,
how the preamble is built, I can just
change it on the businessLogic and it's
going to magically work in both Angular and Flutter.
But yeah, this doesn't compile, so it's useless.
So you know what I'm going to do?
I'm going to just delete it and ignore it.
And I'm going here and, yeah, I know.
I don't use this thing.
Let's do this.
And did someone tell you that hot-reload is awesome?
This is awesome.
I just changed a heading, it just works.
Crazy.
OK, so this is gone.
So let's try to make this thing work.
This is basically the same code, more or less.
And then I moved everything to the SearchBusinessLogic class.
And it keeps track of the query, it keeps track of the results,
it builds the preamble, and it updates
the query when you perform the search,
so the preamble is updated.
And I don't know if you folks, how many of you
have actually any Flutter experience?
The last 24 hours also count.
OK, that's good.
All right.
So I'm scrolling but maybe you won't see.
But this will not work.
And this will not work because even though I am performing
the operations on the business logic,
I have to update the state on the Flutter widget,
or else it won't be built.
So really what I need to do here is
I need to change-- this is so beautiful, but yeah,
we have to get rid of it.
I love tear-offs, so...
OK, so I have to change this.
So when I "performSearch", when the text changes,
I "performSearch"
passing the "query".
".then()" I'm going to do this awesome thing.
"setState" passing nothing.
What?
OK, "await".
I'm using "FutureOr<>".
So the advice here is to not change your code five minutes
before the presentation.
"async" because I have "await".
And then "setState".
And again, because I changed this thing five minutes ago,
it should work.
Please, God.
Yes, it does.
[APPLAUSE]
Right?
This is awesome.
This still looks mostly the same,
but I just have this thing here which, come on, really?
No.
Whenever you see "setState" passing nothing,
that's a bad thing.
That means that you're flagging the widget
to be rebuilt when actually nothing changes.
And please don't do that, really.
So yeah, this is bad.
So what can we do from here?
So I think that the problem here is
you have the input, the query, that changes over time.
And based on that input, you have
the results, and the preamble that also change over time.
And one way of--like how do you represent
things that change over time in Flutter or in Dart?
With a Stream.
Yeah, awesome.
You should [INAUDIBLE] awesome.
You should come here.
So Streams, right?
So let's say, can I just change these things to streams
and see how it looks like?
Sorry, the query is an input, and the counterpart of a Stream
is a Sink.
I think you didn't know about that.
And turns out that a StreamController is a Sink.
OK.
And the results will be a Stream of SearchResults.
And the preamble would also be a Stream of String.
And yeah, let's do "empty".
So of course, it doesn't work.
Oh, now it doesn't even build.
Back to square zero.
So let's go step by step.
I need to update the query every time the input changes.
So that means I have to do "businessLogic.query.add"
Oh, now I can use tear-offs again.
I'm happy.
But now I have this pickle here.
Text is string.
Preamble is a Stream.
And "async" is not going to save me.
Oh no.
But what if I had something that I could have a stream,
and I could pass it a builder, and it builds something.
There is something called StreamBuilder,
which does a little of that.
This is more?
OK.
And a StreamBuilder is something that takes a stream,
and in this case it could be the preamble,
and it takes a builder--
Stream, builder.
And this "builder" takes a "context".
And it takes a "snapshot".
But Paolo, what is a "snapshot"?
A "snapshot" is the last known state of the stream, sort of.
I need to do another talk about that.
And it has something called "data", which is not null
when there is data.
So if we don't have any data, don't show anything.
And here we have the same problem on the results.
I have to wrap this with a StreamBuilder.
And this takes a Stream.
It's going to be then "businessLogic.results"
And then this has to be a "builder".
Hey, [INAUDIBLE] people, this should be automatic.
And then from the snapshot, I map this snapshot.
Or empty if there is no data.
Oh yeah, so about that.
"Ggg"
OK, that's something.
Yeah, I have fat-fingered something here.
But anyway, let's ignore the fact that it doesn't compile,
because it's not going to work anyway, because it's not
doing anything.
But let's look at the final code.
And I promise that I have a working example, I guess.
No.
So now I can [INAUDIBLE].
It's awesome.
It's very simple.
I can directly pass in the data to the business logic.
And I don't need to wrap the results
in anything too complex.
I mean, they are going to be async one day anyway.
And StreamBuilder is something provided by Flutter,
and it is idiomatic.
So this still looks pretty clean and close
to what I wanted it to be, besides the StreamBuilder
stuff.
So yeah, but you don't believe me, right?
This does work.
That's why I have another file.
I have many files.
So I add in my "done" and hot reload because it is awesome.
And then, yeah.
Let's go to "done", because you still don't believe me.
It could be the other thing you just copy paste.
This is too small.
Also IntelliJ should scale.
So this is the "done" implementation,
which is basically the same thing.
You add and have StreamBuilders.
And you have the search business logic here.
Oh, one last thing.
You know the binary API?
That has to go away.
So the modus operandi for that is that you
strike this cross-platform interface,
and you inject the implementation.
You have a binary implementation for Flutter
and a JSON implementation for AngularDart.
And then we have this--
so injected like this.
Not exactly like this, but like this.
We create a search BLoC.
And what is a BLoC?
Suspense.
And if you folks want to see how I implemented this thing,
it's really simple.
It's like it's React, reactive programming...
Anyway, I did too much already.
So yeah, so that's how I want it to look like.
And that was a process, more or less.
There was lot more tears in this process,
but that's what we got to.
And let's go back to these lights.
And they say, OK, Paolo, that's awesome and all,
but you didn't say anything about Angular,
and you know that 90% of us care more about Angular
than Flutter.
OK, fine.
Let's say you have this SearchScreenComponent,
and the only thing it would have would be
the BLoC or the business logic.
And then if you had the template that when you search--
you have a MaterialInput that when it changes,
it sends the event to the component,
and then just shows the preamble and the results.
There is something called "async pipe"
that lets you do this.
So instead of calling a method on the component,
you directly add to the BLoC.
And instead of showing properties from the component,
you access these strings from the BLoC.
And what the async pipe you do, it
lets you use a Stream as if it was a property.
So your Angular code, yeah, you had to add some characters.
But come on, that's good.
So good.
So we have this pattern now.
We have this class.
Oh, I have this thing now.
So we have this class.
When we are refactoring, we extract the business logic
to this class.
And it has a well defined set of inputs,
and a well defined set of outputs.
And if you happen to have any platform-dependent
dependencies, you have to extract a common interface
and then inject the different implementations.
Nice, right?
But there's a thing.
If you don't give it a name, especially with an acronym,
people are not going to use it.
So we gave it a name.
We called it BLoC, which is the Business Logic
Component, like the building blocks of application.
Yeah, I know, it's right there.
But we had to name it.
So good.
So we went from this to this.
But Paolo, you might ask me, because there's
no Q&A, but Paolo, you may ask me,
the whole point of a presentation
was that over time, things are going to get more complicated.
So how do you prevent this from happening again?
Because it has happened before, and it will happen again.
Well, some good old aggressively pedantic rules.
And basically, we have a set of guidelines.
And if you don't follow them, you are wrong,
and there's no negotiation.
That's the best set of rules.
All right, so the guidelines for designing a BLoC are:
number one, the inputs and outputs
are simple, Streams and Sinks.
You cannot have platform-dependent concepts
and constructs.
That's kind of obvious.
But the inputs and outputs are only streams.
Can I have a function?
No.
Can I have a closure?
No.
Sinks, Streams.
Second, dependencies must be injectable,
and if you don't do that, you have more trouble than this.
And they have to be platform-agnostic.
And by that I mean, the interface you inject
doesn't have to depend on Flutter or Angular
or any concept like, for example,
having a browser window.
Third, inside the BLoC, you cannot branch on platform.
You cannot have "if mobile do this", "if AngularDart do that".
Because again, when you start doing that,
that's a rabbit hole that's going to make your code much
harder to track.
And lastly, if you follow the rules number one, two,
and three, you can do whatever you want.
You can implement your BLoC using imperative programming.
You can program it using functional programming.
You can program it using smoke signals.
I don't care.
Just follow the three rules.
But may I suggest you try reactive programming
in these trying times?
So we have guidelines for designing a BLoC,
but we also need guidelines for designing UI.
And those are: there is a more or less one-to-one pairing
between UI component and widget in a BLoC.
So in this case, we have a search screen, and we would have a SearchScreen BLoC.
And of course, you're not going to create the BLoC
to every single widget, especially Flutter.
Yeah, you don't want that.
So you have to make a judgment call.
And that's OK.
You have to define what complex enough is
and what makes sense for you and create
a BLoC for each widget that is complex enough.
Second, components should send inputs as they are.
If you have a BLoC that takes inputs
that we present as text field, you
are going to hook up that on change directly.
You are not doing anything else.
Because the moment you start doing
stuff, that's business logic.
You actually don't do anything.
And the counterpart for this rule
is: outputs should be displayed as close to "as is" as possible.
And you're going to say, but Paolo, you just
said that there is no compromise.
And I'm not a savage.
I know that sometimes, for example,
in Flutter if you want to render text, like a styled text,
you have to somehow massage the text.
But it should be as crude as possible.
You should not add anything to some other thing.
If you're going to display an amount in dollars
and you want to format that, you do not format that in Flutter.
You format that on the BLoC.
And lastly, if you have any branch,
and again, I'm not a savage.
Sometimes you need to do it inside a widget.
That's fine.
But that if can only have one condition, and that condition
is a BLoC output.
One BLoC output.
Because the moment you start doing "and"s, like BLoC output
A and B, the fact that you need to merge those two, that
is also business logic.
So to wrap it up, that's kind of obvious, bu
you have to move all your business
logic to BLoCs following that pattern that I just said,
that I just showed.
Secondly, you have to keep UI components simple.
Because the moment you start making them complex
or making them think for themselves,
then things get complicated.
And lastly, design rules are not negotiable.
And that's for the sanity of everyone.
So with that, I say thanks, and I hope that's useful for you
folks.
[APPLAUSE]
Không có nhận xét nào:
Đăng nhận xét