[MUSIC PLAYING] ANDREW BROGDON:
Hello, everybody. Welcome to Building
for iOS with Flutter. I'm Andrew Brogdon
from the Flutter team. BRETT MORGAN: And
I'm Brett Morgan, also from the Flutter team. ANDREW BROGDON: And
today, we're going to tell you all about how
you can build great iOS apps using Flutter. Let's take a second, though,
and do a little audience demographics. How many of you aren't that
familiar with Flutter yet? Maybe you haven't used it. We got some hands. OK. How many of you are
familiar with Flutter? Seasoned coders-- wow. OK. So you guys and gals
might be thinking this. Why are we even
having this talk? Building for iOS with Flutter-- there's a command for that-- flutter build ios. Job done. Shortest I/O talk ever, right? No. In reality, there's a lot more
to building a great app for iOS than just compiling your code. iOS users are a demanding
bunch, and rightfully so. They're used to world-class
apps, polished experiences. They're used to apps that
respect certain design guidelines and follow
particular paradigms. And if you don't respect
those expectations, it's very easy to end up with
a user who looks like this. Unhappy, confused. Why doesn't this look
like all my other apps? Why doesn't this thing move to
the other thing when I do this? That's not what you want. You want users who
look like this-- happy and at ease. The apps feel natural to them. They know how to use them just
by looking at the interface. Now to be fair, that's not
always easy to pull off, but Flutter can help. One of the ways it can help
is by doing some things automatically. This is a list view widget. And it's running on
Android on the left and on iOS on the right. And as you can see on Android,
it has that nice Material glow. And on iOS, it has a bounce. It's updating its scroll
physics automatically to match the platform
that it's running on. And the reason Flutter can do
this is because the people who build the SDK can be
confident that people want this in almost every app. If you're building
for iOS, there's a 99% chance you want
a list that bounces, and so that's
built into the SDK, and it happens automatically
with no code from you. But there are
decisions that Flutter can't make for you, that you
need to be in control of. And that's because different
apps take different paths toward adapting to a platform. One team, for example,
they might say, we need our app to be
as native as possible. The fonts, the animations,
the way it navigates, the chrome that goes around
the app, we want all of that to look very native
to the platform. We just have a few
things in the middle. Maybe those yellow
widgets there, we have our own design for those. Another team might say,
we have the content down. We have our own design
language for that. But we like the
navigation paradigm. We want to keep that. Another team might
say, shoot the works. We have our own brand. We have our own very
customized look. We'll do our own thing for
everything except alerts. That's the only little
bit that we care about. That's what we want. And so you end up
with three apps that take very
different approaches to matching the platform, but
can still look and feel great. Now the bad news
is, again, Flutter can't make these
decisions for you. You need to be aware of and in
control of how you make them. But the good news
is that Flutter is designed to power
your apps no matter where they fall
on this spectrum, and we're going to spend the
rest of this talk showing you how. BRETT MORGAN: So I can
start coding now, right? ANDREW BROGDON: Almost. Just a couple things
to cover quickly. Number one, we're going to
start with a little data here. This talk is about adapting
the user experience to the platform. We're not really talking
about state management or architecture. We do have a model that's
going to store some data. If you're interested
in those topics, there is another
talk on this stage tomorrow by Matt Sullivan
and Filip Hracek, so check that out. Another thing-- we're going
to start with a few widgets, and we're going
to talk about how to take the widgets that
have our design in them and adapt them to iOS. And last, we're not going
to be using Material on iOS, and that absolutely
is an option. If you saw the
talk yesterday, you know that Material's very
adaptable, very expressive and customizable, but
this is just a choice that we made for this talk. And here is the app that
we're going to be hacking on. It has one screen that
presents a list of fruits and vegetables. It has another
screen where you can record that you ate
something and say, I had this many servings. So if I had a kiwi for
breakfast this morning, you can record that. And it has a third
screen that'll keep track of a log
of what you've eaten, what you've gotten in terms
of calories and some vitamins. So that's the app we're
going to be working on. And to build it, we're going
to use the Cupertino package. This is the Cupertino
widget gallery, which you can find on flutter.dev. And it has screenshots,
images, sample code, all sorts of fun stuff
for each of the widgets that's in the package. And originally,
we were just sort of going to go give a tour
of the website as a way to introduce you all
to the widgets that are in the package. And then we remembered,
Flutter has hot reload. It can change
things very quickly. Why don't we just run it
and we'll see how that goes? So thus was born Cupertino
widget lightning round. So we're just going to
show you some widgets here. BRETT MORGAN: Switch to
the demo computer, please. There we go. My very clean desktop here. Here, we have the
traditional Hello World with one slight difference from
the ones you've seen before-- is we're importing
the Cupertino package. So let's get started. First step, we have
a Cupertino button. It's a great little
button, but it seems to be lost in
this sea of white. So let's spruce things up,
give it a bit of color. We can now see we have
a button we can tap on. It'd be nice if it
actually did something. So, of course, we can fill
in our onPressed handler, and we can throw up a
Cupertino alert dialog. So when we do this,
we click on it, and of course today is all
about eating our vegetables, so do we like desserts? Sadly, no. Next, we're going to have a
look at Cupertino switches. We can see it's
actually quite quick. Little guy here, and it
looks all nice and native. We have that paragon of iOS
design, the segmented control. We have a slider of course,
which we'll be seeing later. ANDREW BROGDON: Yeah, we'll
be seeing that once again in a minute. BRETT MORGAN: Yep. We can turn around
and have a picker. Sorry, this time
'round we're going to-- god, I love my auto-typing. We have a nice little picker
we can slide through here. If we can do that, we
might as well have a date. And of course, we
can make a guess when I/O will be on next year. Apparently, it's going
to be early next year. Oh, we have activity indicator. Now at the top, it looks
a little bit blank. I wonder what we
can do about that? We can use a Cupertino
page scaffold and put a navigation
bar in there. And, of course, the piece de
resistance, all sorts of-- ooh, it's a little bit
slow at typing this. It's getting tired. ANDREW BROGDON: We'll be
using this widget again in just a second-- CupertinoTabScaffold. It presents the
tabs at the bottom and also handles building
the contents that go into the tab views. BRETT MORGAN: So now we
have three separate activity indicators. Back to the slides. ANDREW BROGDON:
Back to the slides. There we go. All right, so let's
talk about one decision that you will probably come
to in building any app, and that's navigation. There are a lot of ways
to handle navigation. This is one, for example. This is hierarchical
navigation, where you start at a root location,
and you have some choices. You pick one, then
you get new choices. You pick one, you sort
of make a path that way. The web works this way. You dive deep and then
you hit the Back button. Android apps use
this very frequently, but iOS has another common
pattern called flat navigation, where you have multiple routes
that you access with tabs, and each one has its
own navigation stack. So you can dive deep
on one of them, then still get to the
others using the tabs. And when you come
back, the stack is right where you left it. Let's see if we can put
this to work on our app. So we'll have two roots-- one that's a list of fruits and
veggies, and one that's our log of what we've eaten. And then we'll have a
pop-up from the list that's a form where we can
record some data, record a new log entry
of what we've eaten. How can we get that working? BRETT MORGAN: Oh, back
to the demo computer. ANDREW BROGDON:
It's so easy when you're doing all the work here. This is great. BRETT MORGAN: OK. Here we are, back on our
little iOS simulator. We have an app. We've added a bit more
imports because we have a bit of a data model here
with vegetables and logging what we've eaten. We have the application. Most importantly, we
have a CupertinoApp here instead of a MaterialApp, which
you're probably used to by now. We have some theme data. I don't know if you can
see it at the back there, but this little log icon is
in red because that's red. If we go down to
the main screen, we have a tab bar, which
I showed you before, a pair of navigation
items, and well, we're not building anything yet. So let's fill in
the tab builder. Oops. Hehe. Now I'm showing away the magic. There we go. OK. So we've got a pair
of CupertinoTabViews. ANDREW BROGDON: And
CupertinoTabView is the root view that creates
those separate navigation stacks. So it's going to make a separate
navigator for each page that goes inside this tab view. BRETT MORGAN: OK. So we are actually showing
views when we click on these, but there's nothing there yet. So let's fill in our
stubs for our Log screen. So now we have some
text, and we can see when we click on the buttons
in the tab bar, we get changes. But we've also got
an Add to Log screen. And well, how are we going
to bring this up to screen? Let's use our good old
friend GestureDetector to give ourselves
an onTap handler so that when we
click on this, we get that iOS style animation
of bringing something in. ANDREW BROGDON: And here you
see the CupertinoPageRoute there is the class that encapsulates
that animation that you see. The new screen slides in,
just like you'd expect, and that's the widget
that does that. BRETT MORGAN: So looking
at our Log screen, it'd be nice if we had
something at the top here to make it feel a bit
more at higher for an iOS user. And how we stop doing that
is a CupertinoPageScaffold. The PageScaffold
doesn't actually put anything on screen
itself, but gives us the capability to put a
navigation bar at the top. ANDREW BROGDON: There we go. BRETT MORGAN: Next, we'll
turn around and do the same with our list view and our Add
to Log page, giving us-- there you go. We got the veggie list. And when we click
on our handler, we get same up here, and
also the themed back chevron, so iOS users start
feeling all at home. And back to slides. ANDREW BROGDON: Back
to slides, please. All right. So let's take a look
at what we just did and the decisions that we made. We started with my app. It's a widget. In Flutter, everything
is a widget. And that's what we made
our first decision. We decided we want to use the
Cupertino package in this app, and we created a widget for it-- the CupertinoApp widget. That encapsulates that first
decision that says, I need-- I'm going to need a
Cupertino theme later, and I'm going to need
the navigator set up properly, and so on. That's the first
decision we made. And then from there, we
made our main screen, and we made another decision. We said, I want to
use flat navigation. That's another
design decision we made about adapting
to the platform, so we created the
CupertinoTabScaffold and the CupertinoTabView. One presents the tabs
and has that tab builder, and one creates that
navigator that we need. So that's another place where
we encapsulated a decision into a widget. Now after that, we have
our Add to Log screen. And there, we had
another decision. We said, I want that
navigation bar across the top. I want to have that back
button on my pop-up. I want my title up there
the way the users expect. So that's another
decision we made. It helps to put
it on the screen. [LAUGHS] So these
decisions-- they're all encapsulated in widgets. And so we end up with four
places in our widget hierarchy here, which I've
sort of abbreviated, where we've taken
each decision and made a widget do the work for us. The one thing we don't
have right now is content. So let's take a
look at that and how we can put widgets to work
on one of these screens. The Cupertino package
has a number of widgets, from navigation stuff all the
way down to input controls, as you just saw. And let's take a
look at our app, and we're going to focus
just on the middle screen here, just on this form. We've got the segmented
control there. We've got a slider. We've got a button that
looked very platform-specific. How can we put these
to work in our app? BRETT MORGAN: Back
to the demo computer. Here we go. We're on our Add to
Log screen, and it's time to add some content. Oop, wrong button. There we go. First thing we do
is we add a model. This is where we get
our content from. So on iOS, typography
is very important. We have a class that
turns around and adds a whole bunch of capability
around theming of text. We won't show
that, but we'll put that in the open source project
which is coming up shortly. We're also splitting out
our Add to Log screen into a separate widget
and stateful form. ANDREW BROGDON: Yes. So one of the questions we get
a lot from Flutter developers is how do I separate business
logic from presentation logic? And that's something that
we've just done here. We have a screen widget
that will manage interacting with our model for us. It'll actually read data from
it, save data back to it. And then we have
a separate widget that's going to be in charge
of how the form looks and is presented. So it just takes
a veggie record, and it takes a callback to use
when somebody hits the button. Otherwise, it's
fairly ignorant of how the business logic works. BRETT MORGAN: OK. Next up, we're going to start
adding some state to our form. So we've got a
servingsTextController, and this is going to be
where our user enters how many servings of the apple
or whatever vegetable or fruit they're eating. Now we're going to start
putting things on the screen. We're going to
have a stack here, because we're going to have
a list that has our form, and then overlaid
over the top of that at the bottom of the page
is going to be a summary. Now we're creating a
row across the top. Don't know if you can
see it, but there's a little hint of a box here. That's our flat card. Probably help if I actually
put some content in it so you can actually see it. There we go. We've got some apples. Now it'd be nice to actually
put next to that a description. So what we're going to
do is across our row, we've got a column
which we're going to add a pair of text
items here that are themed using our adaptive theme-- text theming. So now we've got
apples and fruit. And underneath this, we're
going to start bringing in our Cupertino widgets. ANDREW BROGDON: Right. We BRETT MORGANe-- So we have-- ANDREW BROGDON: Sorry. So here's
CupertinoSegmentedControl. This is our first Cupertino
widget in this form. As you can see, the
image above in the text, they could feel at
home in an app running on any number of platforms. But this is something
specific to iOS, CupertinoSegmentedControl, where
we give it the list of children that it's going to
display, we tell it which value to have
highlighted at the moment, and we give it a
callback to invoke when the user taps on it. BRETT MORGAN: Next up, we're
going to put a text field down. It's going to go
right about here, giving us the ability to
enter the number of servings. But it's looking a bit-- oh, yeah. We need a callback handler
so that we can turn around and properly deal with input. I've hidden my keyboard
on the iOS simulator. And next, I want to
make it a bit larger and put something
beside it describing what that number
actually represents. So as you saw, it
grew up in size. Now we know what the
number actually means. Next, we'd actually like
to come down the page and start putting in a slider. Now for the slider to
work, you kind of need to put some labels
on the ends of it, and then put the
CupertinoSlider itself in. ANDREW BROGDON: So here we
have our third Cupertino widget, CupertinoSlider. It takes the value that
it should be displaying, a minimum and a
maximum range, a value for the number of divisions or
stops to show along the way, and another onChanged
callback that's invoking setState with the
new value when it's moved. BRETT MORGAN: OK,
on with the show. Put a nice little
quote there, so we know what apples are all about. And now we're going to start-- sorry, that went a
little bit quickly. What we're doing is
we're putting a summary at the bottom of
the page, and it's important to insert the list
that this is in so this doesn't interact with it too badly. And now, what are we
going to put on it? We're going to put on a
summary of the total calories and the vitamin A and vitamin
C. So let's start painting it. So to do that, on iOS, I have
this wonderful frosted glass effect. And we're implementing
that using image filter. So let's get it on the screen. We're positioning it at the
bottom here so it won't move. There we go. Sorry. If I really pull this, you
can actually see it sort of sliding in behind there. ANDREW BROGDON:
Yeah, so that blurred background widget is doing
that frosted effect that you see at the bottom there. And so that's not
something that's provided by the
Cupertino package, but you can still create
something like that and reuse it in exactly the
same way throughout your app. BRETT MORGAN: So we've
got this wonderful form. Wouldn't it be great if
we had some sort of call to action at the bottom of
the page, some sort of button to actually-- so let's do that. Let's turn around and
add a CupertinoButton. There we go. Add to Log. It's seeming kind of bland
and not really findable. ANDREW BROGDON: Yeah, it's
hard to spot down there. BRETT MORGAN: Let's make
a really small change. I don't know if you
saw it, but this became a CupertinoFilledButton,
and there we go. We've got this nice, big
call to action for our users to turn around and remember that
they've eaten apples at lunch. Back to slides, please. ANDREW BROGDON: Back to slides. All right. So there's our
Add to Log screen. Now imagine you built
that out using Flutter, you built out the other
two screens using Flutter. That's right about the time
somebody's going to come to you and say, hey, that
iOS app you built? It looks great. How quickly can you get it
running on Android, right? Which brings us to our next
topic, multi-platform design. Can we, without turning our code
into a giant bowl of spaghetti, achieve something like this? Update the apps so that
these little details are specific to the platform,
the way navigation works is slightly different to
adapt to the platform. How can we do that? Again, this is not something
that's very easy to pull off normally, but Flutter can help. We still have the SDK
working on our behalf, adapting many of its
widgets automatically, with no code changes, to the
platform that it's running on. Also good news, because we
encapsulated these decisions about which aspects
of the platform we wanted to use
in our app, we only have a few particular
points that we need to revisit
within our code base. A lot of our code
doesn't particularly care which platform
it's running on. There's specific ones
that we need to revisit. And so we can do that and
make decisions there as well. We can check the platform and
say, am I running on Android, or am I running on iOS? Well, if I'm on iOS, I'll
stick with the CupertinoApp as the root. If I'm running on Android,
I'll use the MaterialApp. Further down in our widget
hierarchy, we can say, I want that navigation
bar across the top. I want the Cupertino one on
iOS, and I want a Material one on Android. And so I can make that
decision and still encapsulate it in a widget. And then at the very end
of our widget hierarchy, the leaf nodes that
we're using for input, we can make the same
kind of decision. Right there we can say. If I'm on iOS, I want to
use a CupertinoSlider. If I'm on Android, I'll
use a Material slider. And we can encapsulate that
decision into a widget, as well. So time to code? BRETT MORGAN: Almost. ANDREW BROGDON:
And fair warning, you're about to hear us use
the word adaptive quite a bit when we're making widgets. That just means
it's a widget that's going to make a decision
about how to compose itself based on the platform. BRETT MORGAN: OK. My computer's finally caught up. Off to the demo
computer, please. Ah, there we go. So as you may have
noticed, there's been a slight change in scenery. One, our software team
have been busy writing code and we now have implemented
our list of vegetables. But we're also on
the Android device, and well, as much as it's
great that our code works, it does look a little
weird to have this iOS feel on an Android device. So let's turn around and
start bringing this back and making it feel correct
for our Android users. So the first thing
we've done is, of course, we
pulled in Material, and we've given
ourselves an ability to actually figure out which
platform we're running on. So the first thing we need
to think about changing is our friend CupertinoApp. What we're going to do is,
depending on which platform we're on, whether on iOS
or a Material device, we pull in CupertinoApp
or MaterialApp. Next thing we're going
to do is we're going to-- of course, as Andrew
mentioned, we're going to use the
word adaptive a lot-- and so it's time to actually
adapt our main screen. So to implement, as
Andrew was talking about, the difference between the
flat hierarchy and the-- what did you call it? ANDREW BROGDON: Flat navigation
and hierarchical navigation. BRETT MORGAN: Thank you. What we're using
is on iOS, we're using the TabScaffold
to give ourselves the buttons across the bottom-- these guys. And on Material, we're
just showing the log screen as the root of our
application, and I'll show you how we gave our
hierarchical navigation in a second. So the first point
we're starting with is AdaptivePageScaffold. That takes the title of
the scaffold and the child. And when we're on iOS, we
just, as we were before, use the CupertinoPageScaffold. And on Material, we use
the material Scaffold, and we introduce a
drawer on the side. So let's go ahead
and wire that in. And as you can see,
we suddenly have this Material-looking screen. We don't have the buttons
across the bottom. And when I click correctly,
we can navigate into our list. And ooh, that looks
a little wonky. ANDREW BROGDON: We got some
text styles to work on. BRETT MORGAN: We
should fix that. ANDREW BROGDON:
One thing to note-- the widget that we created
to make that decision for us isn't very big, and we're still
able to be very declarative and not too imperative. We can make one
decision and then say, this is how I'm built this way. This is how I'm
built another way. BRETT MORGAN: OK. Next step. Let's wire that all
in so that we're using the AdaptivePageScaffold
everywhere, and suddenly, everything
looks a bit better. We still have our
CupertinoSlider and our SegmentedControl, which
we need to deal with next. ANDREW BROGDON: Yes. BRETT MORGAN: Oops. Now you know how I do it. ANDREW BROGDON: And it works
if you rotate the device. BRETT MORGAN: OK. So next thing we're going
to do is introduce a layer above our CupertinoSlider
so we can correctly figure out which one we're
using depending on platform. We've passing the
value, the min, the max, the changed on callback handler. And on iOS, we just go
out to CupertinoSlider, and on Material, we go
out to Material Slider. Why is that not working? Click it once more. ANDREW BROGDON: We have some
I/O gremlins in our demo. BRETT MORGAN: We do. OK. ANDREW BROGDON: One of the
reasons that we're making these widgets specifically-- because
it might look like we're just taking some properties and
drilling them down into other widgets-- is that they allow us to
make the decision ourselves. When we replaced that
CupertinoSegmentedControl-- BRETT MORGAN: You
know what I did wrong? I didn't wire it in. It wasn't the system. It was me. ANDREW BROGDON: There you go. You do still have to
wire in the widget and actually deal with it. BRETT MORGAN: There we go. ANDREW BROGDON: There we are. BRETT MORGAN: We
got Material Slider. Next step, we're going
to adapt our text. So this is still
looking a bit Cupertino. It's a lot of typing, I know. ANDREW BROGDON: It's doing
a lot of work for us. BRETT MORGAN: If only I
could get this in my day job. The next thing we're going to
do is adapt the text button down at the bottom. We're going to selectively
use either a CupertinoButton or a RaisedButton. Let's go ahead and wire them in. And as you can see, this is
now a Material text field and the raised button. And back to slides. ANDREW BROGDON: So there's one
topic we haven't covered yet. We've been focusing
mostly on how to adapt your user
experience and your UI to the various platforms. But there's more to an
app than just its UI. There are native APIs that we
might want to interact with. How do I get the user's
location in a convenient way on both iOS and Android? How do I interact with other
aspects of the platform? The good news here
is that Flutter offers a technology
called Platform Channels. Every Flutter app has a
little bit of Dart code and a little bit of
native code running in it, and they can talk to each other. The Dart code, for example,
can ask the native side, hey, would you run a method for me? And the native side
will say, no problem. I'll catch that
message, run the method. Here's the return value. And this works the
other way, as well. Native code can call
into Dart and ask it to do something for them. You can also use
what's called an event channel to expose
a stream of data coming from the native side. So if you wanted to know
what the air pressure was with your device,
you can expose that as a stream of values
that are continually getting updated and exposed
as a native Dart Stream. And so you can use
this technology to add a little
bit of native code to your apps to do whatever
it is you need to do. A little bit of
good news on that, though, is that for most of
the common use cases, packages and plugins have
already been built. This is a screenshot of Pub. It's the package manager
for Dart and Flutter, where you can find thousands
of open source plugins and packages for both
Dart and Flutter. And we're going to be talking
about one called path_provider. This is one that's actually
maintained by the Flutter team. There's a group of engineers
within the team that's just dedicated to the
package ecosystem. And what this does
is it figures out where documents should be
stored on a particular platform. So Dart can totally save files,
knows how to read things back. No problem there. But the temporary
directory on iOS is different than the location
of the temporary directory on Android. So the package exposes an
interface in Dart to say, hey, where should I put a file
in the temporary directory? Can you find that
directory for me? And the Dart code
will invoke a method, using Platform
Channels, and the plugin includes a little bit of
native code for both platforms that listens for
those calls and says, oh, you want the
temporary directory? No problem. I'll run off and
invoke my method to figure out where
that is, and then I'll return that to you as a string. And the Platform
Channels handles marshaling the
data back and forth and automatically
converts types for you. I was a slide behind. All right, so let's
look at how we can put that to work in our app. We're going to take a look at
our model for the first time. BRETT MORGAN: Oh, yes. ANDREW BROGDON:
Oh, over to code. BRETT MORGAN: Like magic. OK, as you can see,
we're back here on iOS. We have all of our
wonderful application, and we can turn around
and eat like 10 apples, add that to our log. We go over to our log. We've got this wonderful thing. Now, I'm just going
to motivate what we're about to
build by showing you something that
obviously you guys will have hit on the odd occasion. If I turn around and do a
full restart, I lose my state. So wouldn't it be really great
if we turned round and actually saved what the user was doing
to disk so we don't wind up with this disappearing
state problem? So what we're going to do is
we're here in the models file, and I'm going to utilize
the package that Andrew just spoke about, which
is our path_provider. So what we're going
to do is down here in the AppState object, which
is the state of the application, as it were, we've
been using it up 'til now-- it has all the vegetables
that we have, including fruit. Naming things is hard. And we have our log entries. So what we're going
to do is we going to create a pair of
methods here that give us the ability to
write entries to disk and read them back in. So let's use our
path_provider to find out where the application documents
directory is, create a file-- oh, name a file,
entries.json in there, and then write our entries
out using the power of JSON. Next, let's do the same
with reading entries in. And we can again get the
application documents directory, use the
same file name, deal with the obvious case
of there is no file here, and finally, actually read
it in and decoded it as JSON. Next thing we need to
do is when we actually click that Add to Log button,
actually write it out to disk. And on the flip side, when the
AppState object is created, read entries in from our
storage and log all the entries, or add them all to the log
so they're actually visible. So now that we've done
that, assuming I've gone all the way to
the end, I should now be able to go over here, again
eat 10 apples for breakfast, add them to our log. ANDREW BROGDON: 1,300 calories? Good lord. BRETT MORGAN: That
was a lot, wasn't it? Why are we eating apples? And now I should, all things
willing, do a full restart and we still have state. ANDREW BROGDON: There we go. BRETT MORGAN: Back to
the slides, please. ANDREW BROGDON: And so
that's our talk for you guys. And everybody that's
here and everybody that's watching on
YouTube, as well, hopefully you've seen
how we can build out an app that has a
first-class interface, still feels natural
to people on iOS, and if we need to go
to multi-platform, we can adapt it
quickly and easily while keeping our code clean. Hopefully you have
everything you need to end up with users
that look like this-- happy with your app. Again, I've been Andrew
Brogdon from the Flutter team. This is Brett Morgan, also
from the Flutter team. We have some resources
for you here on the slide. flutter.dev is the website. You can find samples, including
a published app that uses the Cupertino package, at
github.com/flutter/samples. We're headed straight from
this to the Office Hours tent where we would love to
answer every question every single one of you has. Thank you so much. [APPLAUSE] [MUSIC PLAYING]