Codelabs - Your first Flutter app : https://codelabs.developers.google.com/codelabs/flutter-codelab-first
crash course n速成班 notifier /ˈnəʊtɪfaɪə®/ n通告人 foray /ˈfɔːreɪ/ n涉足,突袭
trivial */*ˈtrɪviəl/ a.unimportant elevate /ˈelɪveɪt/ vt提拔,抬高 centerpiece */*ˈsentərpiːs/ n核心部分 envision /ɪnˈvɪʒən/ vt设想 roomy /ˈruːmɪ/ a宽敞的
hand-picked a精选的 opacity /oʊˈpæsəti/ n不透明性 interpolate /ɪnˈtɜːpəˌleɪt/ vt插入vi插值 typeface n字体
Take a closer look at the code in lib/main.dart
, to understand how it works. At the very top of the file, you’ll find the main()
function. In its current form, it only tells Flutter to run the app defined in MyApp
.
The MyApp
class extends StatelessWidget
. Widgets are the elements from which you build every Flutter app. As you can see, even the app itself is a widget. The code in MyApp
sets up the whole app. It creates the app-wide state, names the app, defines the visual theme, and sets the “home” widget–the starting point of your app.
Next, the MyAppState
class defines the app’s state. This is your first foray into Flutter, one of the easiest ways to manage app state is ChangeNotifier
.
MyAppState
defines the data the app needs to function. Right now, it only contains a single variable with the current random word pair. You will add to this later.
The state class extends ChangeNotifier
, which means that it can notify others about its own changes. For example, if the current word pair changes, some widgets in the app need to know.
The state is created and provided to the whole app using a ChangeNotifierProvider
(see code above in MyApp
). This allows any widget in the app to get hold of the state.
Lastly, there’s MyHomePage
, the widget you’ve already modified. Each numbered line below maps to a line-number comment in the code above:
Every widget defines a build()
method taht’s automatically called every time the widget’s circumstances change so that the widget is always up tp date.
‘MyHomePage’ tracks changes to the app’s current state using the watch
method.
Every build
method must return a widget or a nested tree of widgets. In this case, the top-level widget is Scaffold
.
Column
is one of the most basic layout widgets in Flutter.It takes ant number of children and puts them in a column from top to bottom. By default, the column visually places its children at the top. You’ll soon change this so that the column is centered.
The second Text
widget takes appState
, and accesses the only member of that class, current
(which is a WordPair
). WordPair
provides several helpful getters, such as asPascalCase
or asSnakeCase
. Here, we use asLowerCase
but you can change this now if you prefer one of the alternatives.
Notice how Flutter code makes heavy use of trailing commas. This particular comma doesn’t need to be here, because children
is the last and also only member of this particular Column
parameter list. Yet it is generally a good idea to use trailing commas: they make adding more members trivial, and they also serve as a hint for Dart’s auto-formatter to put a newline there. For more information, see Code formatting.
Scroll to MyAppState
and add a getNext
method.
The new getNext()
method reassigns current
with a new random WordPair
. It also calls notifyListeners()
(a method of ChangeNotifier
) that ensures that anyone watching MyAppState
is notified.
All that remains is to call the getNext
method from the button’s callback.
ElevatedButton(
onPressed: () {
appState.getNext(); // ← This instead of print().
},
child: Text('Next'),
),
Not great. The centerpiece of the app—the randomly generated pair of words—should be more visible. It is, after all, the main reason our users are using this app! Also, the app contents are weirdly off-center, and the whole app is boringly black & white.
The line responsible for showing the current word pair looks like this now: Text(appState.current.asLowerCase)
. To change it into something more complex, it’s a good idea to extract this line into a separate widget. Having separate widgets for seperate logical parts of your UI is an important way of managing complexity in Flutter.
Flutter provides a refactoring helper for extracting widgets but before you use it, make sure that the line being extracted only accesses what it needs. Right now, the line accesses appState
, but really only needs to know what the current word pair is.
For that reason, rewrite the MyHomePage
widget as follows:
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current; // ← Add this.
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(pair.asLowerCase), // ← Change to this.
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
], // ← 7
),
);
}
}
Nice. The Text
widget no longer refers to the whole appState
. Now, call up the Refactor menu. In VS Code, you do this in one of two ways:
Right click the piece of code you want to refactor (Text
in this case) and select Refactor from the frop-down menu, or move your cursor to the piece code you want to refactor (Text
, in this case), and press Cmd+.
Now it’s time to make this new widget into the bold piece of UI we envisioned at the beginning of this section.
Find the BigCard
class and the build()
method within it. As before, call up the Refactor menu on the Text
widget. However, this time you aren’t going to extract the widget.
Instead, select Wrap with Padding. This creates a new parent widget around the Text
widget called Padding
. After saving, you’ll see that the random word already has more breathing room.
Flutter uses Composition over Inheritance whenever it can. Here, instead of padding being an attribute of Text, it’s a widget!
This way, widgets can focus on their single responsibility, and you , the developer, have total freedom in how to compose your UI. For example, you can use the Padding widget to pad text, images, buttons, your own custom widgets, or the whole app. The widget doesn’t care what it’s wrapping.
Next, go one level higher. Place your cursor on the Padding
widget, pull up the Refactor menu, and select Wrap with widget. This allows you to specify the parent widget. Type “Card” and press Enter.
This wraps the Padding widget, and therefore also the Text, with a Card widget.
To make the card stand out more, paint it with a richer color. And because it’s always a good idea to keep a consistent color scheme, use the app’s Theme
to choose the color. Make the following changes to BigCard
’s build()
method.
These two lines do a lot of work:
First, the code requests the app’s current theme with Theme.of(context)
.
Then, the code defines the card’s color to be the same as the theme’s colorSheme
property. The color scheme contains many colors, and primary is the most prominent, defining color of the app.
The card is now painted with the app’s primary color. You can change the color, and the color scheme of the whole app, by scrolling up tp MyApp
and changing the seed color for the ColorScheme
there.
Flutter’s
Colors
class gives you a convenient access to a palette of hand-picked colors, such asColors.deepOrange
orColors.red
. But you can, of course, choose any color. To define pure green with full opacity, for example, useColor.fromRGBO(0, 255, 0, 1.0)
. If you’re a fan of hexadecimal numbers, there’s alwaysColor(0xFF00FF00)
.
Notice how the color animates smoothly. This is called an implicit animation. Many Flutter widgets will smoothly interpolate between values so that the UI doesn’t just jump between states.
The card still has a problem: the text is too small and its color is hard to read. To fix this, make the following changes to BigCard
’s build()
method.
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
// ↓ Add this.
var style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Change this line.
child: Text(pair.asLowerCase, style: style),
),
);
}
What’s behind this change:
By using theme.textTheme
, you access the app’s font theme. This class includes members such as bodyMedium
(for standard text of medium size), caption
(for captions of images), or headlineLarge
(for large headlines).
A display typeface is a typeface that is intended for use at large sizes for headings, rather than for extended passages of body text.
typographic /ˌtaɪpəˈɡræfɪk/ a印刷的 theoretically /θiːəˌˈrɛtɪklɪ/ adv理论上地
exclamation /ˌɛkskləˈmeɪʃən/ n感叹 elevation /ˌɛlɪˈveɪʃən/ n海拔,高度,高地
equivalent /ɪˈkwɪvələnt/ a等价的n等价物 myriad /ˈmɪrɪəd/ quantifier大量a大量的 a myriad of
surface n表面vi显现 impair vt损害 flair */*fler/ n天赋 lump v把…归并一起n肿块
axis /ˈæksɪs/ n(中心/坐标)轴 cross axis n横轴 spacious /ˈspeɪʃəs/ a宽敞的
didactic */*daɪˈdæktɪk/ a教学的,说教的 meat n肉,重要的部分
The displayMedium
property is a large style meant for display text. The word “display” is used in the typographic sense here, such as in display typeface. The documentation for displayMedium
says that “display styles are reserved for short, important text”—exactly our use case.
The theme’s displayMedium
property could theoretically be null. Dart, the programming language in which you’re writing this app, is null-safe, so it won’t let you call methods of objects that are potentially null. In this case, though, you can use the ! operator (“bang operator”) to assure Dart you know what you’re doing. (displayMedium
is definitely not null in this case. The reason we know this is beyond the scope of this codelab, though.)
Calling copyWith()
on displayMedium
returns a copy of the text style with the changes you define. In this case. you’re only changing the text’s color.
To get the new color, you once again access the app’s theme. The color scheme’s onPrimary
property defines a color that is a good fit for use on the app’s primary color.
If you feel like it, change the card further. Here are some ideas:
copyWith()
lets you change a lot more about the text style than just the color. To get the full list of properties you can change, put your cursor anywhere inside copyWith()
's parentheses, and hit Cmd+Shift+Space
.
Similarly, you can change more about the Card
widget. For example, you can enlarge the card’s shadow by increasing the elevation
parameter’s value.
Try experimenting with colors. Apart from theme.colorScheme.primary
, there’s also .secondary
, .surface
, and a myriad of others. All of these colors have their onPrimary
equivalents.
Flutter makes apps accessible by default. For example, every Flutter app correctly surfaces all text and interactive elements in the app to screen readers such as TalkBack and VoiceOver.
Sometimes, though, some work is required. In the case of this app, the screen reader might have problems pronouncing some generated word pairs. While humans don’t have problems identifying the two words in cheaphead, a screen reader might pronounce the ph in the middle of the word as f.
A simple solution is to replace pair.asLowerCase
with pair.asPascalCase
. Pascal case (also known as “upper camel case”), means that each word in the string, including the first one, begins with an uppercase letter. So, “uppercamelcase” becomes “UpperCamelCase”. Using upper camel case helps screen readers identify the individual words in the compound word and provides a better experience to visually impaired users.
However, you might want to keep the visual simplicity of pair.asLowerCase
. Use Text
’s semanticsLabel
property to override the visual content of the text widget with a semantic content that is more appropriate for screen readers. Now, screen readers correctly pronounce each generated word pair, yet the UI stays the same. Try this in action by using a screen reader on your device.
Flutter has a variety of tools for accessibility, including automated tests and the
Semantics
widget. Learn more on Flutter documentation’s Accessibility page.
First, remember that BigCard
is part of a Column
. By default, columns lump their children to the top, but we can easily override this. Go to MyHomePage
’s build()
method, and make the following change.
This centers the children inside the Column
along its main (vertical) axis.
The children are already centered along the column’s cross axis (in other words, they are already centered horizontally). But the Column
itself isn’t centered inside the Scaffold
. We can verify this by using the Widget Inspector.
You can just center the column itself. Put your cursor onto Column
, call up the Refactor menu (with Ctrl+.
or Cmd+.
), and select Wrap with Center.
If you want, you can tweak this some more.
Text
widget above BigCard
. It could be argued that the descriptive text (“A random idea:”) isn’t needed anymore since the UI makes sense even without it. And it’s cleaner that way.SizedBox(height: 10)
widget between BigCard
and ElevatedButton
. This way, there’s a bit more separation between the two widgets. The SizedBox
widget just takes space and doesn’t render anything by itself. It’s commonly used to create visual “gaps”.With the optional changes, MyHomePage
contains this code:
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
),
);
}
}
In the next section, you’ll add the ability to favorite (or ‘like’) generated words.
You add a new property to MyAppState
called favorites
. This property is initialized with an empty list: []
.
You also specified that the list can only ever contain word pairs: <WordPair>[]
, using generics. This helps make your app more robust—Dart refuses to even run your app if you try to add anything other than WordPair
to it. In turn, you can use the favorites
list knowing that there can never be any unwanted objects (like null
) hiding in there.
Dart has collection types other than
List
(expressed with[]
). You could argue that aSet
(expressed with{}
) would make more sense for a collection of favorites. To make this codelab as straightforward as possible, we’re sticking with a list. But if you want, you can use aSet
instead. The code wouldn’t change much.
toggleFavorite()
, which either removes the current word pair from the list of favorites (if it’s already there), or adds it (if it isn’t there yet). In either case, the code calls notifyListeners();
afterwards.First, wrap the existing button in a Row
. Go to MyHomePage
’s build()
method, put your cursor on the ElevatedButton
, call up the Refactor menu with Ctrl+.
or Cmd+.
, and select Wrap with Row.
When you save, you’ll notice that Row
acts similarly to Column—by default, it lumps its children to the left. (
Columnlumped its children to the top.) To fix this, you could use the same approach as before, but with
mainAxisAlignment. However, for didactic (learning) purposes, use
mainAxisSize. This tells
Row` not to take all available horizontal space.
Here’s one way to add the second button to MyHomePage
. This time, use the ElevatedButton.icon()
constructor to create a button with an icon. And at the top of the build
method, choose the appropriate icon depending on whether the current word pair is already in favorites. Also, note the use of SizedBox
again, to keep the two buttons a bit apart.
Most apps can’t fit everything into a single screen. This particular app probably could, but for didactic purposes, you are going to create a separate screen for the user’s favorites. To switch between the two screens, you are going to implement your first StatefulWidget
.
To get to the meat of this step as soon as possible, split MyHomePage
into 2 separate widgets.
Select all of MyHomePage
, delete it, and replace with the following code:
First, notice that the entire contents of MyHomePage
is extracted into a new widget, GeneratorPage
. The only part of the old MyHomePage
widget that didn’t get extracted is Scaffold
.
The new MyHomePage
contains a Row
with two children. The first widget is SafeArea
, and the second is an Expanded
wdiget.
notch /nɒtʃ/ n等级,圆形凹口;vt赢得(比赛) obscure /əbˈskjʊə/ a难懂的,难处理的vt遮挡 wrap around vt围住,包含 from within 从…内部 yet adv还(yet another 再另一位) vervatim /vɜːˈbeɪtɪm/ adj/adv一字不差的 handy /ˈhændɪ/ a有用的,手边的 adventurous /ædˈventʃərəs/ a勇于冒险的 tile /taɪl/ n瓷砖,瓦片 nail /neɪl/ n指甲,钉子vt钉住,搞定 spoiler /ˈspɔɪlə/ n捣乱者,剧透
The SafeArea
ensures that its child is not obscured by a hardware notch or a status bar. In this app, the widget wraps around NavigationRail
to prevent the navigation buttons from being obscured by a mobile status bar, for example.
You can change the extended: false
line in NavigationRail to true
. This shows the labels next to the icons. In a future step, you will learn how to do this automatically when the app has enough horizontal space.
The NavigationRail
has two destinations (Home and Favorites), with their respective icons and labels. It also defines the current selectedIndex
. A selected index of zero selects the first destination, a selected index of on selects the second destination, and so on. For now, it’s hard coded to zero.
The navigation rail also defines what happens when the user selects one of the destinations with onDestinationSelected
. Right now, the app merely outputs the requested index value with print()
.
The second child of the Row
is the Expanded
widget. Expanded widgets are extremely useful in rows and columns–they let you express layouts where some children take only as much space as they need(NavigationRail
, in this case) and other widgets should take as much of the remaining room as possible(Expanded
, in this case). One way to think about Expanded
wdigets is that they are “greedy”. If you want to get a better feel of the role of this widget, try wrapping the NavigationRail
widget with another Expanded
. The resulting layout looks like this: Two Expanded
widgets split all the available horizontal space between themselves, even though the navigations rail only really needed a little slice on the left.
Inside the Expanded
widget, there’s a colored Container
, and inside the container, the GeneratorPage
.
Stateless versus stateful widgets
Until now, MyAppState
covered all your state needs. That’s why all the widgets you have written so far are stateless. They don’t contain any mutable state of their own. None of the widgets can change itself—they must go through MyAppState
.
This is about to change.
You need some way to hold the value of the navigation rail’s selectedIndex
. You also want to be able to change this value from within the onDestinationSelected
callback.
You could add selectedIndex
as yet another property of MyAppState
. And it would work. But you can imagine that the app state would quickly grow beyond reason if every widget stored its values in it.
Some state is only relevant to a single widget, so it should stay with that widget.
Enter the StatefulWidget
, a type of widget that has State
. First, convert MyHomePage
to a stateful widget.
Place your cursor on the first line of MyHomePage
(the one that starts with class MyHomePage...
), and call up the Refactor menu using Ctrl+.
or Cmd+.
. Then, select Convert to StatefulWidget.
The IDE creates a new class for you, _MyHomePageState
. This class extends State
, and can therefore manage its own values. (It can change itself.) Also notice that the build
method from the old, stateless widget has moved to the _MyHomePageState
(instead of staying in the widget). It was moved verbatim—nothing inside the build
method changed. It now merely lives somewhere else.
The underscore (
_
) at the start of_MyHomePageState
makes that class private and is enforced by the compiler. If you want to know more about privacy in Dart, and other topics, read the Language Tour.
Examine the changes:
selectedIndex
, and initialize it to 0
.NavigationRail
definition instead of the hard-coded 0
that was there until now.onDestinationSelected
callback is called, instead of merely printing the new value to console, you assign it to selectedIndex
inside a setState()
call. This call is similar to the notifyListeners()
method used previously—it makes sure that the UI updates.The navigation rail now responds to user interaction. But the expanded area on the right stays the same. That’s because the code isn’t using selectedIndex
to determine what screen displays.
Use selectedIndex
Place the following code at the top of _MyHomePageState
’s build
method, just before return Scaffold
:
Examine this piece of code:
page
, of the type Widget
.page
, according to the current value in selectedIndex
.FavoritesPage
yet, use Placeholder
; a handy widget that draws a crossed rectangle wherever you place it, marking that part of the UI as unfinished.selectedIndex
is neither 0 or 1. This helps prevent bugs down the line. If you ever add a new destination to the navigation rail and forget to update this code, the program crashes in development (as opposed to letting you guess why things don’t work, or letting you publish a buggy code into production).Now that page
contains the widget you want to show on the right, you can probably guess what other change is needed.
Here’s _MyHomePageState
after that single remaining change:
Responsiveness
Next, make the navigation rail responsive. That is to say, make it automatically show the labels (using extended: true
) when there’s enough room for them.
Flutter provides several widgets that help you make your apps automatically responsive. For example, Wrap
is a widget similar to Row
or Column
that automatically wraps children to the next “line” (called “run”) when there isn’t enough vertical or horizontal space. There’s FittedBox
, a widget that automatically fits its child into available space according to your specifications.
But NavigationRail
doesn’t automatically show labels when there’s enough space because it can’t know what is enough space in every context. It’s up to you, the developer, to make that call.
Say you decide to show labels only if MyHomePage
is at least 600 pixels wide.
Note: Flutter works with logical pixels as a unit of length. They are also sometimes called device-independent pixels. A padding of 8 pixels is visually the same regardless of whether the app is running on an old low-res phone or a newer ‘retina’ device. There are roughly 38 logical pixels per centimeter, or about 96 logical pixels per inch, of the physical display.
The widget to use, in this case, is LayoutBuilder
. It lets you change your widget tree depending on how much available space you have.
Once again, use Flutter’s Refactor menu in VS Code to make the required changes. This time, though, it’s a little more complicated:
_MyHomePageState
’s build
method, put your cursor on Scaffold
.Ctrl+.
(Windows/Linux) or Cmd+.
(Mac).Builder
to LayoutBuilder
.(context)
to (context, constraints)
.LayoutBuilder
’s builder
callback is called every time the constraints change. This happens when, for example:
MyHomePage
grows in size, making MyHomePage
’s constraints smallerNow your code can decide whether to show the label by querying the current constraints
. Make the following single-line change to _MyHomePageState
’s build
method:
Now, your app responds to its environment, such as screen size, orientation, and platform! In other words, it’s responsive!.
The only work that remains is to replace that Placeholder
with an actual Favorites screen. That’s covered in the next section.
Remember the Placeholder
widget we used instead of the Favorites page?
If you feel adventurous, try to do this step by yourself. Your goal is to show the list of favorites
in a new stateless widget, FavoritesPage
, and then show that widget instead of the Placeholder
.
Here are a few pointers:
Column
that scrolls, use the ListView
widget.MyAppState
instance from any widget using context.watch<MyAppState>()
.ListTile
has properties like title
(generally for text), leading
(for icons or avatars) and onTap
(for interactions). However, you can achieve similar effects with the widgets you already know.for
loops inside collection literals. For example, if messages
contains a list of strings, you can have code like the following:On the other hand, if you’re more familiar with functional programming, Dart also lets you write code like messages.map((m) => Text(m)).toList()
. And, of course, you can always create a list of widgets and imperatively add to it inside the build
method.
The advantage of adding the Favorites page yourself is that you learn more by making your own decisions. The disadvantage is that you might run into trouble that you aren’t yet able to solve by yourself. Remember: failing is okay, and is one of the most important elements of learning. Nobody expects you to nail Flutter development in your first hour, and neither should you.
Here’s what the widget does:
ListTile
widget for each one.All that remains now is to replace the Placeholder
widget with a FavoritesPage
. And voilá!
You can get the final code of this app in the codelab repo on GitHub.
Look at the code of this advanced version of the same app, to see how you can add animated lists, gradients, cross-fades, and more.
Follow your learning journey by going to flutter.dev/learn.