[MUSIC PLAYING] CRAIG LABENZ: Hello, and
welcome to Building Next Gen UIs in Flutter. My name is Craig Labenz and I'm
a developer relations engineer on the Flutter team. Today we have something
special in store for you. Over the past few
months, the Flutter team worked with our good
friends at gskinner, creators of the Wonderous app,
to build a ground breaking demo. If you follow along
with this codelab, you're going to see some
really neat strategies to build a dazzling UI
in Flutter, something you'd see in a futuristic
sci-fi video game's main menu. Let's take a look at
the finished product. It's called Outpost
57, Into the Unknown, and it features this
spacesuit clad character here in the middle
staring up defiantly at a mysterious orb
pulsing in the night sky. Notice how there's energy fields
emanating off the orb as well? The orb grows and
shrinks, by the way, as our cursor moves in and out
from the center of the screen. And also, very neatly,
the walls on the side reflect that light
in a realistic way. When the orb shrinks
and its light dims, we see that reflected
on the walls. Lastly, the title
in the top left seems to be being bombarded by
some sort of cosmic radiation. So if you have no
idea how you'd even approach building
something like this, then you're right where
I was before we started this project a few months ago. Join me as we work
through this codelab and see what the good folks
at gskinner cooked up. If this is your first
thing in Flutter, you will need to set up your
development environment. But luckily, just for
the sake of this codelab, you can skip installing the
iOS or Android emulators because this codelab
targets web and desktop, and neither of those
require special emulators. But a note for all
you Flutter experts out there that are
working through this. You will need to use the
latest version of Flutter 3.10, so be sure to pause this video
and run Flutter Upgrade if you haven't yet already. OK. We're now ready to get started. The first thing you need to
do is download all the code and load it up into your IDE. Now I've already
done that, but you'll need to drill into the
next-gen-ui folder and load step one. Your editor will probably offer
to install all the packages by running flutter pub
get behind the scenes. Of course, you
want it to do that. Now the next thing the
codelab tells us to do is run the starter app, so I
am going to do that as well. First I'll close
the finished product that we were just looking at,
and I'll switch to my code, jump back to the starting
branch, and build. Now a note on how I'm
going to do this codelab. I'll spend most of my
time in the codelab UI that you will also see
at home on the website, and I'll only jump
over to my editor when it's time to
change to a branch and rerender the next milestone
that we see in the codelab. OK, here is the starting point. It's not much to
write home about, but that's OK because
that just means you get to take all
the credit for when it looks great at the end. Now there are a few
interesting details in the code that you
start with, and I want to spend a second
to talk about those. The first is the
assets folder, and it's got three subdirectories--
fonts, images, and shaders. Especially images and
shaders are of interest here. The shaders folder contains
those raw fragment shaders that we're going to use later. One is going to apply
that cosmic radiation effect to the title
and the second will, of course, be
the intimidating sky orb in the center. But the images folder is also
surprisingly interesting. If we open this up,
we'll see a handful of different
categories of images in here, starting with
these background images. Then if we scroll down, we
see some midground images, and above that, foreground. So we'll open these in a
second, but later on those are going to come together
to compose our scene. Now within these categories,
background, midground, and foreground, you'll also
see emit and receive images, light emit and light
receive, and these are going to create the
different effects of the scene and the room that we're
in reflecting the light from the orbit and natural way. So I'll focus on the
midground and just pull up some of these images. So we can kind of see
what we're talking about. Midground base is
dark here, but it captures the frame of the
room that we're looking at. Now contrast that with
midground light receive. This has light
surfaces everywhere, and we'll later use
a neat color filter trick and a nice blend
mode to recolor this image and apply a new shade
to the whole scene. And then lastly there's
the emit flavor of image, and these draw lines
that cut through the orb and walls of this
scene that we're in, and these emit images
are going to create the effect of some other
material in the room. Maybe it's getting charged
up by the orb in the sky, but it's going to emit
its own light as if it's being animated by the effect. So one last note on them. They're all the same resolution. They're all 2,080
pixels by 1,170 pixels. So when we lay them
over top of each other and they all take up the
same amount of space, they won't be able
to help themselves but to recompose our scene. The last thing I
want to point out is that we have a couple
special images that will help us draw buttons and borders. So we've got-- these
aren't crosshairs, but they always make
me think of crosshairs. We've got this left side
effect and right side. And then also our Start button
had a really unique border as well, and that's
actually just an image. This would be very complicated
to draw with a plain widget, and I think us Flutter
developers often overlook the value in
using a widget like this. We see something like
this and think, well, how would I do that with a
custom clip and a raw painter and whatnot? But you can just use an image. OK. So we've explored
most of the code base. The last thing I want to look
at before we dive into it is the pub spec file. At the top we simply
see all the dependencies that we'll ever need
throughout this, so you don't need to run flutter
pub get ever again until we get to the end, and at the
bottom of the pub spec file our assets are pulled
in from the folders we were just looking at. Our fonts, shaders, and
images are all ready to go. So now back to the codelab,
and we see we've run the app. We saw that starter
UI, and we have explored the different
milestones in the code base. So we're ready to get
started and paint the scene. Now, the first thing you'll
need to do here is create a new directory in your lib
directory called title_screen, and populate it with a new
file called title_screen.dart. And then I recommend
you simply grab all of this code in
the first snippet here and paste it
right into your editor, and let's talk about
what this does. It's a new full
screen widget, which we can tell because
it uses a scaffold. And then inside it
quickly delegates to a stack, which ultimately
arranges the images that we were just looking at. And remember, these images
all have the same size, so our background
images appearing first means they will be
the most distant. They'll be covered up by
the mid and foreground. And then midground appears
next, and lastly we have the foreground
images at the bottom. So we've created our first
image but we can't see it yet. We, of course, have
to use it first. So if I scroll down, we see
that the codelab directs us back to main.dart for
our second change. You've got a few
imports that you'll want to update at
the top of this file, but then after that, down
in the Next Gen UI app, you can rip out everything
that was under this home parameter in your material
app and replace it simply with the title screen widget. All right. This, the codelab
indicates, should get us ready to view
the first milestone. So I'm going to
switch back to my code and jump to the Git
branch that matches this point in the codelab, which
for me is step three, paint the scene, milestone one. I'll now rebuild, and
when I load my app we see all of the images
composing this scene. Now they are grayscale. Of course, what we saw
earlier was not grayscale. So the next step
we're going to do is going to apply the
color to these images and begin to really bring
some life to this scene. OK. If we scroll down, we see
the next part is called add an image coloring utility. So we're still in
title_screen.dart, and at the bottom of this file
you'll see a new widget called lit image. And this lit widget
takes a couple of interesting parameters. First it takes a color, then it
takes an actual image source-- and this needs to be made
available by our pubspec.yaml file-- and then it takes this
parameter called light amount, and this is the
intensity of the color. So remember, our orb is
going to grow and shrink, and as it grows, it casts
more light on the scene. We'll reflect that through
this light amount parameter. So how does this widget work? Well, in its build method it
uses a color filtered widget and a blend mode of modulate. And if you're not familiar
with blend modes, that's OK. If you are, this
modulate blend mode is similar to multiply
from Photoshop, but essentially
this whole concept involves taking an
image as your base and then applying some
kind of modification to the whole thing
pixel by pixel, and the blend mode
is the algorithm by which you compare
each corresponding pixel in your base image and
your modification layer and ultimately
collapse those down into your final pixel color-- your value for every pixel
in your resulting image. So the modulate
or multiply blend mode, that's ultimately going
to darken the image that we're looking at. And this is really interesting
because if we go back to those images
quickly and I open up, for example, the
receive midground image, we see a bunch of
white areas here. So those white areas
are going to cleanly be able to be color filtered
by the modulate blend mode. And then the gray areas
around the bottom, they'll create the
effect of something that was actually
gray in real life and is being pseudo
illuminated by green light. It's a really neat effect. All right, back to the codelab. Once you add the lit image
at the bottom of the file, like always, first
you add the widget and then you have to use it. So back up at the
top of title_screen there's a few more imports
that we need to juggle, and then we're going to make a
few modifications to the title screen widget itself. First, we've got a
couple parameters here. These are going to control
the strength of the light that is applied in our receive
and emit images respectively. They're hardcoded for
now, but later they're going to incorporate
what the orb is doing, how big it is,
how powerful of light it is casting. Then down here we see two
new variables in the actual build method or the
color and emit color. Now eventually, the
color of this whole scene is going to be dictated
by the difficulty that the user has chosen. In the bottom left there were
three difficulty buttons, casual, normal, and hardcore. Well, we don't
have those buttons yet so we can't drive
the color according to our business logic yet. So for now, we're just grabbing
the first one off the list-- that's green corresponding
with the casual color-- and assigning that
to the whole scene. So if you copy this
code and paste it over your old definition
of title_screen, you will be ready to
view the next milestone. So if I scroll down, we see
lit image here appears again. There's no changes. You just need to
modify title_screen. We're ready to see
what we've got. So I'll again switch
back to my code, jump to the correct branch. Step three, milestone two. Rebuild, and we see
the UI now being basked in this
strong green light from an orb that
doesn't even exist yet-- but it will before too long. OK, back to the codelab. We've reached the end of
step three, paint the scene, and we're ready to move on. Step four, add a UI. Now for the purpose
of this codelab, the word UI just means the
title and all the buttons. Obviously you could think
of all of this as the UI, but that's what it
means for our purposes. OK. To begin, we need a new
file, so you need to create title_screen_ui.dart in that
same title screen folder right next to the previous
file we made. And to begin, you can
copy in all of this code and fill it
completely with that. So this adds a new title
screen UI widget and then another widget below that. First, what does the
title screen UI widget do? Well, it quickly delegates,
again, to a stack. This is a really common
pattern in this codelab. Sometimes Flutter
developers think, maybe is it-- am I
using too many stacks? Is this too expensive? I've got stacks in
stacks in stacks. I've got stacks of stacks. Is that OK? It absolutely is. It's a really nice way to lay
out everything just right, and you don't have
to worry about it. So we're using another
stack, and ultimately the main first child here
is this title text widget. It's decorated by
two other widgets. Top left is simply a
readability utility to place this in the
top left of our stack, and UI scaler is going to grow
and shrink the child widget proportionally to the
size of the actual window. This will keep, on large desktop
displays or high resolutions, our UIs from looking
kind of distant and stretched out
from each other with really small chunks of UI. They'll all grow and
shrink accordingly. OK, but TitleText. That's the character here. So if we scroll down,
we see TitleText, and this build method
isn't too complicated. It's basically a column
with two children. This row draws Outpost
57, and the second child is simply a text widget
that draws Into the Unknown. So once we have
added this new file and populated it
with title_screen_ui, we'll be ready to
actually use it. So continuing on, we'll
return to title_screen.dart. And if we scroll
down in here, you can add the import to the
top, and ultimately, in the build method at the
very bottom, you'll fill the entire stack with
the title screen UI widget. And of course, this makes
sense because putting it last means it will be in front of
everything else in the stack, and we wouldn't want any
of our background things to sit-in front of our
menu buttons or title. OK, so once we've
done that, yes, I believe we will be ready
to view another milestone. So I'll switch back
to my code and jump to the next branch, which
is step four, milestone one. And when I rebuild here,
we'll see the title appear. And just as a quick note
on that UI scaler widget, when I shrink
everything like this, we see UI scaler is what's
responsible for keeping our proportions right. OK, back to the codelab. Scrolling down, we're now ready
to add the difficulty buttons. So we're staying
in title_screen_ui, and the first
thing we want to do is import this focusable
control builder package. That's already been included
from the pubspec.yaml file so you don't have to
do anything there, but focusable control builder is
a really nice utility package. It's written and distributed
by the same folks that wrote all of this, gskinner. And what it does is it allows
you to create fully featured, custom form controls from a
blank, unopinionated starting point. So what do I mean
by fully featured? Well, it's a
button, for example, that will respond
to focus events, so a desktop user could press
Tab and move the focus down to your button. It'll also register itself with
the semantic system of Flutter, so a screen reader can find
and correctly read your button. But it doesn't have any opinions
about how your button should look, which is
great for you if you want a radically custom looking
button like we have in this UI. So we're adding the focusable
control builder package and scrolling down to look
at some changes we're going to make to title_screen_ui. First, we have a
few new parameters that we're going to
add to the constructor. Remember, we're adding
buttons here so this implies storing some state. Which button was last clicked? What did it do? What's the handler for
the next click event? So the title screen
UI widget is now going to need to accept
some parameters to deal with all of this stuff. Then down in the build
method there's a few keywords that you'll need to juggle
around and reposition, but ultimately we're adding this
new difficulty buttons widget in the bottom left
of our screen, again wrapped with the UI scaler
and bottom left positioning widget. OK. Difficulty buttons doesn't
exist yet, so let's keep going. The next step is the codelab
stays in title_screen_ui and we're adding those
difficulty buttons widgets. This block adds both the
difficulty buttons, plural, and below, difficulty button. So you can just grab it
all in one go and paste it into the bottom of your
title_screen_ui.dart file. So what does this do? Well, in the build method
it ultimately delegates to a column and draws
those three buttons. And if you've ever made your
own custom buttons in Flutter before, you've probably
seen this kind of API where, in a different
widget of your creation, you hide all of your
opinionated decisions and you expose just a
minimal API for yourself to recreate those buttons later. So we see that that is
what's going on here. All we're passing in
is the label, what text should the button show, is
this button selected, and then what happens when the user
interacts with the button. Very minimal. And below, in the difficulty
button widget itself, we'll see how it
all comes together. So in difficulty
button, first of all, we notice that it begins with
the focusable control builder widget. And this uses a builder
pattern, as the name suggests, and it passes in a state
variable, which has information like, is this hovered? Is this focused? And then it's up to you
to decide, what does that mean for your widget? In the build method or in the
method that the builder runs, we see, again, it quickly
delegates to a stack. Now in here, I want to point
out how much this button does make use of that state object. Look, here we check to see,
is it hovered or focused? We also check, is
the button selected? So this button is
very iteratively built up and very dependently
built up based on the nature of its state. So those are the
difficulty buttons, and if you made it to the end of
this snippet you shouldn't see any linter errors in your
title_screen_ui.dart file, but you will see some errors
in your title_screen.dart file. So switch back to that and
we'll get that in order. Now in title_screen.dart,
remember, we just were adding these
handlers in a difficulty variable to that nested
widget title screen UI. So some thing's got
to store, for example, which difficulty level is,
in fact, currently selected. Well, the place to do this
is to convert title screen to a stateful widget, just
like the codelab says. So we want to convert it
from stateless to stateful, and your IDE can help you
with that if you click on where it says stateless widget. Once you've converted
it, you'll be ready to make the
rest of the changes. To start in title
screen state, you'll want to add all of this
new code, and let's talk about what it does. First of all,
there's new variables for emit color and orb color. Remember, earlier I
said that we were just going to hard code those to
grab the color associated with the casual
difficulty button or the casual
difficulty, but now we're adding the buttons
so we can actually have the color of our
entire scene accurately be painted by what difficulty
the user is choosing? Next we've got some variables to
store the difficulty, and then some handlers to wire
up the behaviors that change when the user clicks on
buttons or hovers over buttons. Scrolling down, it also can be
good to paste the entire build method again because the
orb color variable now has an underscore,
whereas before it didn't because the orb and
emit color variables have been moved up
here and they're now private computed variables. So if you copy that in as well,
you get down to the bottom. If you copy the whole build
method you'll get this. If you make the edits by hand
then be sure at the bottom, for title screen UI you will
need to add in the difficulty, the three difficulty
parameters-- the value itself, the pressed, and
the focused handler. So there's no changes
in lit image again, and this means we're ready
to re-evaluate our app. So again, I'm switching
back to my code and I'm jumping to the next
branch for me, which is right here. Step four, milestone two. And when I rebuild,
we see our difficulty buttons appear in the bottom
left, and they're operable. So as I hover over different
difficulty buttons, we'll see the color of
the entire scene redraw. Now the change here
is instantaneous. It's really not that
pleasant, but in the next step we're going to add a
lot of nice animations that'll breathe some life
and polish into this. OK. Back to the codelab. Moving on we get to the next
step, add the Start button. So we're staying
in title_screen_ui, and if we scroll down into
the bottom of its build method we see the first change
that we need to make, which is to add another child to
the stack in this build method. And the main character here
is this Start button widget. Later it's going to have a
valuable onPressed value that will actually do
something, but for now it's just an empty lambda. But you can copy
the whole section into the bottom of your stack. Then it's time to add
the Start button itself. So when you scroll down,
we'll see the definition for the Start button. Grab the whole thing, just
paste it right in the bottom of title_screen_ui.dart. So what does the
Start button do? Well, it's reusing
focusable control builder. We saw that earlier. And then, just like a lot of
the things in this codelab, it delegates to a
stack to get everything positioned just right. It does include the images for
that angled border that we saw, and then it ultimately
results in a-- makes its way to
a text widget that draws the Start Mission text. So let's now return to our
app and see that running. I'll jump to step forward
milestone three, rebuild, and there it is. And it's got a hover
effect that is subtle, and it's going to get a
lot cooler in the next step when we add animations. Back to the codelab. Oh, we've actually completed
step four, add a UI, and it's time for those
animations right now. All right, add animations. Now in this step, like
I said, a lot of polish is going to come into our UI. But I'm equally
excited about how we're going to add
the animations as I am about the
animations themselves. And the how we're going
to add these animations is by using a package
called a Flutter Animate. If you watched the
announcement video for this at Flutter
Forward back in January, then you probably already know
some of why I'm so excited. But if not, I'm
really looking forward to sharing this with you. The first thing
that we want to do is add a little
developer helper utility from this package in
our main.dart file. So open up main.dart and
import Flutter Animate. Then scroll down,
and that will allow us to add this one
line right here, Animate.restartOnHotReload
equals true. Now if you've written
animations in Flutter before, you may have noticed that
they don't tend to rerun when your app hot reloads. That's kind of frustrating,
but it does make sense, because a hot reload is
different from a hot restart in that the hot reload does not
re-execute your initialization code. If a hot reload did rerun
your initialization code, then it would clobber
all of your state and it would basically
be a hot restart. So, this is relevant
for animations because we tend to
kick off our animations in our initialization code,
often in an init state method of a stateful widget. So what this line does is it
tells Flutter Animate, which is already keeping track
of all the animations that you declare
using the package, to watch for hot reload
events and restart all of those animations
every time it sees one. It's a really nice
developer productivity win. All right. Once you've got
that wired up, you do have to restart your app
because just like I was saying, initialization code doesn't
rerun on hot reload. So to get that hot
reload trick working, we have to hot restart our app. OK. But now we're ready
to really get into it. Back down in title
screen UI, of course, begin by importing
Flutter Animate, and then we can get going. The first widget that we're
going to animate is the title text widget, and I want you
to look in its build method and find this row, and then
notice all the new code at the end of this line,
.animate and .fadeIn. So you can copy these in, and
then do the same for the text widget below it,
.animate and .fadeIn. Now let's talk about
what this is doing. You may never have seen
this .animate method before, especially because it's new and
it's provided by the Flutter Animate package. Well, .animate is an extension
method on all widgets, and it simply wraps that widget
that you originally called it on. So in this case, it's going to
wrap this row with a new widget called animate, and
that animate widget sets up a lot of
the infrastructure to get us going
and ready to call the other very helpful methods
that this package offers. So once we call .animate, we're
ready to call something like fadeIn, and we simply tell it,
wait 800 milliseconds before you start this animation and
complete the animation another 700 milliseconds later. Of course, summing to
one and a half seconds. And then we do the same
thing just below it. Into the Unknown, you
fade in one second after you would have
otherwise rendered and complete another
700 milliseconds later. And merely by incrementing
the delay between these two widgets, we can begin to
orchestrate chained animations, and that is really, really neat. And think about how
tricky all of this would be to write if we were
using our own custom animation controllers. It would really be a
challenge to wire this up. But with just that
tiny declaration, we are ready to
see it in action. So as always, I'll
switch back to my editor, pop to the correct
branch, and rerun. And when I return
to my app, we see Outpost 57, Into the Unknown,
animate in very elegantly off an absolutely minimal
declaration from us. It's a really exciting new way
to write animations in Flutter. OK. Scrolling down, we're going
to keep this train going. We're going to add
this kind of effect to all the other
buttons in the screen. So next up is the
difficulty buttons, and we're going to see
some similar code here. First find the casual button,
and then add these three lines at the bottom. Animate it in, wait 1.3 seconds. We're continuing
the progression. Before we had 0.8,
1.0, and now 1.3. And then a new player has
entered the party as well. We have a new .slide method. Not only will this casual
button fade into place, but it will fade and
slide into place, which I think is an
effect that you're really going to like in a second. Now add the same thing
to the normal button. A couple lines
down here and then scroll down to the
hardcore button, and we'll find a
few more methods. OK. With that, we're ready
to see some new effects. Have you ever written
animations this quickly before? It has never been so easy. All right, and I will refresh. And when I returned
to my app, we now see this whole left side come
in this staggered marching band-like order
one after another. Really, really cool. OK, next we're going to do
the same thing with the Start button. So back to the codelab. I'll scroll down, and we
see fade in the Start button is next. So you can find its
definition, again, at the bottom of your
title_screen_ui.dart file, and what we're really changing
here is just these lines at the bottom. We're going to add two
animation lines right where your stack closes, and
then three more right where the sized box closes. And of course, those
are just coming from the top of the build
method, stack and size box there respectively. But there's another very
interesting new method here, .shimmer, and I'm excited to
show you how cool this one looks. So I'm hopping back
over to my editor, moving to step five,
milestone three, rebuilding. And first of all, the
whole UI now fades in, and you can see there's
Start button sliding up just like the difficulty buttons. But watch the shimmer effect. As we mouse over, it's just
a really, really cool hover effect, in my opinion. Very, very nice. And the next thing
we're going to do is add other hover effects
to the difficulty buttons. So back to the codelab. We scroll down and we're going
to animate the difficulty hover effect. We're staying in
title_screen_ui. That's where all of
our buttons' code is. And in the difficulty
button class, I want you to scroll
down and find, in the stack, the container
that is the first child. Before you've made the change
that we're talking about here, that container is going to
be the first thing you see. And I want you to copy the
entire animated opacity widget and paste it right over
the top of the container. It'll recontain the
container, but this is how you get this update. Once you've done
that, we're actually ready to watch this again. Moving to step five,
milestone four, rebuilding, and let's see how this works. First of all, our buttons
look totally different. Our difficulty buttons
used to all have a border. Now only the selected
button has a border. But the reason for that
is that they were all showing their hover
border all the time, so now this hover
border fades in and out as we hover over
a specific button and it reveals the
active state that was there all along of,
again, the not crosshairs that always make me
think of crosshairs, that kind of bracket, whichever
difficulty button is, in fact, colored. OK. So we've added the hover effect
to our difficulty buttons and we're ready to
do the last thing in the animation step, which
is animating the color change as we actually change
our difficulty. So we've made a lot of
changes in title_screen_ui. Now we're going to pop
back to title_screen.dart. And just like we
were doing elsewhere, first we want to import the
Flutter Animate package. Now scrolling down,
there's a new widget here, AnimatedColors. You can grab its whole
definition and paste it right in the bottom of
title_screen.dart. So animated colors. What does it do? Well, it takes a
couple of parameters. First a builder, and
then also two colors-- orb color and emit color. These are the parameters
that we've seen floating around this code base. And it's kind of an
implicitly animated widget, so it's
going to keep track of the old values of orb
color and emit color, and whenever it
gets new values it's going to linearly interpolate
and call that builder-- that's 60-ish frames a second
depending on your screen-- until it arrives at
the updated values. So down here in the
builder we see it uses-- oh, sorry, in the
build method we see it uses two tween
animation builders. It decides that it's going
to take half a second to do this whole thing, and it's
down here at the very bottom where the magic really happens. This is the final
method that the widget calls where it eventually gets
around to running the builder that we pass in. And here, all of the
intermediate values for orb color and emit color
are visited until the animation is complete. All right. So we've added the
animated colors widget. Step two always,
use the new widget. So in title_screen is where
we make the next few changes. To begin, you want
to copy everything in the animated colors
block here the way down to where it ends. You can, of course, copy
just the whole definition for the class if you'd like. But what's actually
changing is you're going to copy the
animated colors widget and paste it on
top of the stack. Before you make any
change here, you'll see the scaffold give way to
a center widget, which then renders a stack,
and you're going to wrap that stack in the
animated colors widget. Scrolling down, this is the
last milestone in step five. We'll be ready to see our
scene animate from one color scheme to the next. So I've switched
branches, rebuilt. First of all, our scene just
looks the same, of course, but now when I
hover over a button, it gradually shifts
to the next color scheme, which is a much
more natural looking effect. Remember, the orb is
going to be in the middle and it's going to visually be
the source of all this light, so it wouldn't make sense for
it to instantaneously switch colors. All right. We've wrapped up
the animation step and we're ready to move on. So we've done a lot so far. Step six is where we finally
get to add those juicy fragment shaders. To begin, we're going
to return to main art and make a couple modifications. First, import the
provider package. Again, that's been lurking
about in the pubspec.yaml this whole time and you don't
need to reimport anything. Then scroll down
into the main method itself and replace the entire
old definition of runApp with this new way
to call runApp. And we're wrapping that
Next Gen app widget with a provider, which is
going to load these shaders. Also, don't forget to
import assets.dart as well. That's where the load
shaders method comes from. Now, our whole
widget tree will be able to reach up and grab those
shaders whenever they need. OK. The codelab also
reminds us we've made an edit to the main
method in main.dart, so no hot reload will
be sufficient here. That initialization
code is not re-executed. So after you hot restart, your
app will be ready to continue. Now we're back in
title_screen_ui for the first shader. We're going to begin with that
cosmic radiation glitching effect on the text
in the upper left. So there's a handful
of imports to add. Just copy them all,
paste them right in, and then we're ready
to make some changes in title_screen_ui. So the TitleText widget is
going to be the first part that receives some shader action. And if we scroll down
to its build method, what you'll see before you make
any edits is that you're simply returning this column widget. But I don't want you to do that. I want you to instead
capture that column widget in another variable
and call it Content. Once you do that, you'll be
ready to scroll down and add this entire long
return statement. So what does this
long block of code do? Well, it begins with a consumer
widget that reaches up the tree and finds the provider that
we just added to main.dart. Then once all the shaders
are actually in place, it gets things going by
using a ticking builder. Ticking builder is an
interesting widget you may not have ever seen
before, but its idea is to tell the
Flutter framework, I want to draw a new
frame every chance we get. Every time the
operating system says you can draw a frame if
you want, let's do it. Now normally this is a terrible
idea for most of your apps. It would just waste
battery pointlessly. But once your app
has ambient motion, then there's no
way around the fact that you need to draw new
frames at some interval. So the ticking builder
is going to do that and it exposes to its children
how much time has passed. Then we get to this
animated sampler widget, and I'm not going to lie. The animated sampler
widget is a bit of a beast. Its code is all included
in this code base, but you really don't
need to read or worry about it too much. The framework one
day is probably going to offer
something like this so this is just a first draft
of how this API might look. But let's take a peek
at what this does. First, it takes another
widget as its child, and what this animated
sampler widget is going to do is apply a shader to
what some widget would have otherwise rendered. Pretty cool. Keep that in mind. So it takes two parameters. The first is that child widget
that we just talked about, and the other is a function
that itself receives three parameters,
and I'm going to talk about these in reverse order. First is the canvas. Now the canvas is simply
where you draw all the stuff. You might have seen this before
if you've used custom painters or things like that. The last line here
we see we actually use this canvas to draw
something to the screen. Now above that-- well also,
there's the size parameter. This is just how
much space we have. And then lastly, we get
this image parameter first. It's not like a JPEG. It's not that kind of an image. It's a rendered
version of the widget that we passed in to
the child parameter. So for us, that's
the actual text. The shape of the words,
Outpost 57, Into the Unknown. That's coming in in this
handle called Image. So this function sets
up some parameters. It passes in the relevant
data to the actual UI shader. This UI shader is the
one for the title. And notice the last
parameter here. We're telling it, hey, this
is your starting point. This is what you're going
to run your shader logic on, however this other widget
would have rendered itself but for you. So then we finally do-- we do this other setup and
finally call a canvas.drawRect, and that is what will
apply the shader. OK. That was pretty cool and we're
ready to see it in action. So I'll return to my
code, switch to step six, milestone one, and rebuild. And when I do that, once the
app comes back into place, we see the cosmic radiation
is once again bombarding our title, and it's almost
melting under the influence. A really, really cool effect. Any time you see on Twitter
where some otherwise normal Flutter UI is being really
intensely manipulated, it's this trick
that they're doing. Like the counter app
that entirely ripples like the surface of water
after a stone was dropped in or something is using something
like the animated sampler and an appropriate
fragment shader. OK. So continuing on,
we finally arrive at the main character of this
whole saga, the space orb. So we're still in
title_screen_ui for now, and there's just one
change we're going to make. Add this onStartPressed handler
to the title_screen_ui's constructor, add an
attribute to store that, and then scrolling down,
update what we actually pass into the Start button. The Start button,
in a moment, is going to be able to slightly
influence the state of the orb. Once that's in
place, you can return to title_screen
where we're going to make some dramatic changes. First add all of these
imports at the top, and then we're ready to
get into the good stuff. Now the codelab says here we're
modifying title_screen state, and it reads almost
every part of the class is modified in some
way, and that is true. So I recommend copying this
entire class definition, completely pasting over top
of what you've got currently, and then we'll talk
through what it does. So the interesting
parts in here are-- well, first of all,
finalReceiveLightAmt and finalEmitLightAmt have
become the computed properties I promised you they
would one day be, and they take into
account the orb's energy. It's going to be
growing and shrinking under multiple different
influences momentarily, and we want the scene, the walls
and the rocks around the orb, to be reflecting that
light in a realistic way. So orb energy is now
involved in the calculations for how those images
should reflect the light. Scrolling down, we see
an animation controller, pulseEffect. This is going to create
a little heartbeat effect in the orb on its
own, so it'll just kind of beat a little bit
and seem alive even if the user has walked
away from the screen and is not touching anything. That heartbeat, by the way,
has a slightly random interval to it as driven by this
getRndPulseDuration method. Here we see some
code that whenever the animation completes it
gets a new random duration and goes backwards again,
and then when it finishes going backwards it gets
a new random duration and goes forwards again. So a pretty cool
little effect here. The next interesting
method is bumpMinEnergy. This is going to be
called when the user taps any of the buttons on the
screen, the difficulty or the Start button. And this is going to breathe,
again, a little bit of life into the orb in the center. All right. Here we see handleMouseMove. This updates a mouse
position variable. Remember, the orb is
going to be growing and shrinking based on the
location of the cursor. So handleMouseMove is passed to
a new MouseRegion widget which allows that to actually work. Now, not a ton has changed
in the build method itself, but of course, there
is this new entry, the actual orb itself,
and it's realized by this OrbShaderWidget,
which wraps the widget-- or wraps the shader
itself from the provider that we added to
main.dart, and we pass in the variables about
how we think it should look. And then there's
an onUpdate method where it tells us every
time it's changed something on its own. So those are the changes
to title_screen state. But if you copy that in,
you'll see a bunch of errors where your editor is
telling you that LitImage, something is off there. Well, it's received this
new parameter, pulseEffect. So if we scroll
down, the next step is to update the
definition of LitImage, and pulseEffect is that kind of
heartbeat animation controller earlier that we
were talking about. LitImage is now going to
watch that and rerender itself whenever that animation
controller ticks. OK. We are ready to see
the orb once again. It's been off stage for
a minute, but no more. I'll jump to step six,
milestone two, and rebuild. And returning to our UI is
the space anomaly itself, and as our cursor moves out and
in, the orb grows and shrinks. And as we do that, watch
how those side walls get brighter and dimmer. That's the light
amount variables that we've been working
with this whole time. And again, the
whole scene changes as we change the
difficulty level. And then we wired through that
little handler to Start button, so watch how as we
tap the Start button we get this pulsing
effect on the space orb. Pretty neat. OK. That's the end of step
six, add fragment shaders. We're on to the
final active step where we add a particle field. So in here we need one new
file, particle_overlay.dart. And I recommend simply
grabbing all of this code and pasting it in to
that file, and we'll talk about how it works. So particle_overlay, the
widget that we're creating, uses particle_field, another
library written and distributed by-- you guessed it-- gskinner. So particle_overlay,
the widget, immediately delegates to a particle_field
widget from that package. And we supply particle_field
with an image that it should use and a blend mode for how
to recolor that image, and then an all important onTick method. An onTick, as the comment says,
runs every tick of the app, and the contract
for this method is that it passes us a
particle controller. That particle controller
has a .particles attribute, and it's our job in this
method to update that list. That's all we have to do. So to begin, we see that
this method, every tick it adds one new particle
to the particle field, so one more wave will
be wafting off the orb. After it adds that particle
field-- oh, by the way, there's a lot of intimidating
looking trigonometry code in here. This positions all of
the individual particles in a circular shape
around the orb itself. After they're positioned
we see that it loops over the particles and
checks for any that are too old and it kicks them out of
the screen if it finds them. Any that aren't too old
get updated by moving a little further away,
growing slightly, and having their lifespan decremented so
they will one day time out. Now we've created this widget. Step two is always to use it. So back in title_screen.dart,
import particle overlay and scroll down into the build
method of title_screen state. And you'll find basically in
the middle of the build method, just above titleFgBase,
a new entry appears, and it is allowed to take up
as much space as it wants, as indicated by position.filled. But in the middle, we see our
new particle_overlay widget. All right. That's the last change in this
step, so if I return to my code and jump to step seven,
milestone one and only, rebuild, we will see the
final product once again. As we move into
the center, we see the energy field, the particles
that we've just added, grow in size and intensity,
and they dim as well. They reduce their
intensity as the orb dims, so they're tied
into the strength that the orb is emitting. So folks, if you made it this
far, if you followed along, congratulations. We've covered a lot of
stuff in this codelab. We brought together a
lot of different effects, ranging from these highly
detailed custom shaders to a particle field
around that custom shader to animations moving things
ever so slightly around our app, and in the end, I think
we built a screen that pushes the boundaries of what
most people think of when they think of Flutter UIs. I also
hope that you saw that when we say with Flutter you can control
every pixel on your screen, we really mean it. Thanks for watching everyone. Enjoy the rest of Google I/O. [MUSIC PLAYING]
So we're supposed to compose our UIs from static images and shaders now? ;-)
The effects are cool, for sure, but the fact that all classes he's talking about are private classes to the demo screen, says that there's no much to reuse elsewhere. Also, IMHO, writing the code is the easy part. But how to come up with the initial idea, the graphics and animations would be the part I'd struggle with.