以下内容基本翻译自Write Your First Flutter App,翻译的并不完全!作为自己学习的笔记,加入了自己的理解,可能有疏漏错误,欢迎指正!
步骤
1.创建一个Flutter app
2.使用扩展包(external package)
3.添加一个Stateful组件
4.创建一个无限滚动的(且懒加载的)ListView
5.添加interactivity
6.引导(Navigate)至一个新的界面(Screen)
7.使用主题(Themes)改变界面(UI)
要创建的App目标
如下图:
1)创建一个列表ListView,无限懒加载一些英文名称
2)用户可以选择或取消选择,保存自己的最爱
3)代码每次创建10个条目,随着滑动,创建新的一批条目
4)点击右上角按钮,进入一个新的界面,ListView中只包含自己的最爱(lists only the favorited names)
学习目标(U CAN LEARN)
1.熟悉Flutter App基本构造(structure)
2.寻找和使用扩展包扩展功能
3.使用热重载(reload),以实现更快的开发周期(development cycle)
4.如何实现Stateful组件
5.如何实现无限懒加载的ListView
6.创建并引导至(Navigate)一个新的界面(Screen)
7.如何使用主题改变app的外观
工具
Flutter SDK,Android Studio IDE,Flutter and Dart plugins for Android Studio IDE
参考Hello Flutter
第一步 创建App
创建过程很简单就省略了,可以参考Hello Flutter和官网
1.替换lib/main.dart
,
lib/main.dart
的位置:如果不是Project
切换
我们要编辑的就是蓝色框里的
main.dart
import 'package:flutter/material.dart';//导入包
void main() => runApp(new MyApp());//入口
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
),
body: new Center(
child: new Text('Hello World'),
),
),
);
}
}
2.运行效果
小结
- 这个例子创建了一个
Material
App。 Material 是一种手机和网页的视觉设计(visual design)语言标准。Flutter
提供了丰富的Material
组件。 -
main
方法指定了用于一行函数或方法的肥箭头=>
(fat arrow)标记,哈哈,肥箭头! - 这个App扩展了
StatelessWidget
,App本身也是一个组件。(The app extends StatelessWidget which makes the app itself a widget)。在Flutter
中,几乎所有东西都是组件,包括alignment
,padding
,layout
. - 来自于
Material
库的支架(Scaffold)组件提供了默认的app bar
,title
,body
属性,它用来保存/容纳
(holds)主屏幕的组件树(widget tree).它的子树(subtree)可能非常复杂。 - 组件的主要功能就是提供描述如何展示其他组件或低等级(lower level)组件的
build()
方法。
翻译不好原文如下
A widget’s main job is to provide a build() method that describes how to display the widget in terms of other, lower level widgets.
ps 简单说 组件提供build()方法,描述如何展示其他组件或者子组件(lower level widget翻译为子组件如何?)
- 这个例子的组件树包括
Center
组件,Center
中包含了一个子组件Text
。Center
对齐(align)它的子组件树到屏幕中心
第二步 使用扩展包
在这一步,我们使用名为english_words的开源包,它包含了几千个常用的英语单词加上一些使用的功能。
你可以在pub.dartlang.org找到这个包,也能找到其他许多开源包。
打开网页搜索english word
即可找到
1.找到pubspec.yaml文件,添加依赖
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.0
english_words: ^3.1.0
2.点击右上角的Packages get,将Package加载到工程中
在控制台(console)可以看到如下信息
flutter packages get
Running "flutter packages get" in startup_namer...
Process finished with exit code 0
3.引入包 在lib/main.dart
中添加import
import 'package:english_words/english_words.dart';
添加后,import语句是灰色的,因为还没有使用
4.使用English word包生成文本,替换屏幕中间的Hello World
添加2句代码,注掉HelloWorld一行
final wordPair = new WordPair.random();
child: new Text(wordPair.asPascalCase),//PascalCase即驼峰效果,例如iloveyou会变成ILoveYou,即每个单词的首字母大写
5.使用热加载(Hot reload)
()按钮升级正在运行的app,每次点击按钮或者保存工程可以看到生成一个新的单词
第三步 添加一个Stateful插件
Stateless组件是不可改变的(immutable),即他们的属性不可更改,所有值都是最终的。
Stateless widgets are immutable, meaning that their properties can’t change—all values are final.
此处需要深入理解一下,Text
组件也是继承自StatelessWidget
,其文本data,样式TextStyle都是可以自己代码设置的,与Stateful的区别在于没有一个State来管理内部状态,App运行时不可更改
Stateful组件维护的状态可能在生命周期里改变。实现一个Stateful组件要求至少2个类:一个StatefulWidget类创建一个State类实例。StatefulWidget本身是不可改变的,但State类维护StatefulWidget
的生命。
这一步,我们添加一个StatefulWidget-RandomWords,它创建自己的State类-RandomWordsState.State类将最终维护组件中创建和喜爱的词组(The State class will eventually maintain the proposed and favorite word pairs for the widget);
1.在main.dart
中添加RandomWords,可以在文件的任何位置,但是要在MyApp
类的外面。我们把它放到文件的最底部。这个类什么也没有做,除了创建它的State类。
class RandomWords extends StatefulWidget {
@override
createState() => new RandomWordsState();
}
2.添加RandomWordsState类,大部分app代码驻留(resides)在这个维护RandomWords的状态的类中。这个类将保存生成的词组,词组随着我们的滑动无限增长,也保存最爱词组(Favorite word pairs),最爱词组随着用户点击型图片添加和删除。
开始创建类,实现build()
方法
class RandomWordsState extends State {
@override
Widget build(BuildContext context) {
// TODO: implement build
}
}
3.移动MyApp
中生成随机词组的代码到RandomWordsState类中,并修改MyApp代码
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
),
body: new Center(
// child: new Text('Hello World'),
child:new RandomWords(),
),
),
);
}
}
...
class RandomWordsState extends State {
@override
Widget build(BuildContext context) {
// TODO: implement build
final wordPair = new WordPair.random();
return new Text(wordPair.asPascalCase);
}
}
重新运行App,界面没有变化,不贴图了
第四步 添加一个无限滚动的ListView
在这一步,我们来扩展RandomWordsState,生成和展示单词列表。当滑动时,这个列表展示在一个ListView
组件中,无限增长。ListView
的builder()
构造方法允许你创建一个按需懒加载的列表视图。
1)创建一个_suggestions
变量保存建议词组(suggested word pairings)。这个变量以_
开始,在Dart中,_
标记的强制私有(enforces privacy)。
2)再添加一个biggerFont变量来标记更大的字体。
3)升级build()
方法,返回一个Scaffold
组件
class RandomWordsState extends State {
final _suggestions = [];
final _biggerFont = const TextStyle(fontSize: 20.0);
@override
Widget build(BuildContext context) {
return new Scaffold (
appBar: new AppBar(
title: new Text('Startup Name Generator'),
),
body: _buildSuggestions(),
);
}
}
可以看到,上面的代码中调用了_buildSuggestions
,我们来实现,返回个ListView
Widget _buildSuggestions() {
return new ListView.builder(
padding: const EdgeInsets.all(16.0),//padding
//itemBuilder 当每次生成新的suggested word 被回调
// 并放置每个词组到ListTitle行
// 偶数行(even rows),添加单词词组
// 奇数行(odd rows),添加个分割线
// 小屏幕设备,分割线可能很难看到
itemBuilder: (context, i) {//item
//添加一个1像素高度的分割线
if (i.isOdd) return new Divider();
//i~/2 意为 i除以2,返回int型整数
//例如i分别为1,2,3,4,5,结果分别为 0,1,1,2,2
//以此来计算ListView中词组的真实数量,减掉了分割线的数量
final index = i ~/ 2;
if (index >= _suggestions.length) {//如果已经达到数组的末尾
_suggestions.addAll(generateWordPairs().take(20));//每次创建20个词组,添加进去
}
return _buildRow(_suggestions[index]);
}
);
}
上面的代码中,每个词组调用一次_buildRow
方法,这个方法,在ListTitle中展示每个新的词组,使你在下一步中使每一行更具有吸引力
Widget _buildRow(WordPair pair){
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
)
);
}
然后来升级MyApp
类中的代码,删除原有的Scaffold
和AppBar
,这些已经被RandomWordsState
管理了,下一步,当用户从一个界面跳转到另一个界面时更容易改变app bar
的名字
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
home: new RandomWords(),
);
}
}
重新(hot reload)运行app,可以看到一列词组,滑动后可以看到新增的词组
第五步 添加互动(interactivity)
这一步,我们给每一行添加可点击的心型图标,当用户点击的时候,切换favorited状态,这个词组添加到保存的favorites集合或从集合中删除。
1.在RandomWordsState中新增一个_saved
集合(Set)。这个集合保存用户已favorited的词组。Set比List更优,因为正确执行的Set不允许重复。
class RandomWordsState extends State {
final _suggestions = [];
final _biggerFont = const TextStyle(fontSize: 18.0);
final _saved = new Set();
...
}
2.在_buildRow
方法中,添加一个alreadySaved
检查,以确认这个词组(word pairing)未曾添加进favorites。
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
...
}
3.同样,在_buildRow
方法中,添加heart-shaped图标给ListTitles。然后,添加交互功能。
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
trailing: new Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
);
}
4.重启app,可以看到每一行有个灰边的心型图标,但它们都还不能交互
5.在_buildRow
方法中使建议词组(the name suggestion titles)
可交互。如果一条词组已添加到favorites,点击后从favorties移除。当一个条目被点击,这个方法调用setState()
方法来通知框架framework 状态的改变。
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
trailing: new Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
onTap: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
},
);
}
ps:在Flutter的react style framework
(反应式框架),调用setState()
触发(trigger)对象的build()
方法,从而更新用户界面。
重载app,现在可以点击条目了!而且会有一个ink splash animation(泼墨动画?其实就是原生的水波纹)
从点击的地方扩展开来!
第六步 导航(Navigate)到新的界面
在这一步,添加一个新的界面(Flutter中称为route-航线)展示favorites。你将学习如何在home route和new route之间航行(navigate)。
其实就是两个界面如何切换,Google非要搞航线(route)航行(navigate)
在Flutter中,领航员-Navigator管理app的routes栈the Navigator manages a stack containing the app’s routes
。压(Pushing)一个route到栈内,更新界面到那个route,出栈(Poping)则返回前一个route。
1.在RandomWordsState 的build()
方法中给AppBar添加一个列表图标,当用户点击图标,新的包含favorites的route入栈,展示图标。
ps:一些组件的属性(Properties)接受一个子组件,而其他的属性,如action,接受一组子组件(an array of widdgets),用方括号[]表示
class RandomWordsState extends State {
...
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Startup Name Generator'),
actions: [
new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved),
],
),
body: _buildSuggestions(),
);
}
...
}
2.在RandomWordsState添加一个_pushSaved()
功能
class RandomWordsState extends State {
...
void _pushSaved() {
}
}
此时重启app,能看到列表图标了,但是点击什么也不会发生,因为_pushSaved
还是个空方法。
3.当用户点击列表图标,新建一个route并压入导航员的栈内(push to the Navigtor's stack)。这个动作使屏幕展示新的route。
新页面的内容由MaterialPageRoute’s的builder
属性建立,使用匿名方法(an anonymous function)。
调用Navigator.push
压栈
void _pushSaved() { Navigator.of(context).push( ); }
4.添加MaterialPageRoute和它的builder。现在,添加生成ListTile
行的代码。ListTile.divideTiles()
方法在每个ListTile中添加横向空间,divided
变量持有最终rows
,调用toList()
,方便的转为list
```
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
final tiles = _saved.map(
(pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
},
),
);
}
```
5.builder属性返回一个Scaffold
组件给新的route,包含AppBar
,命名为Saved Suggestions
。新route的body持有一个ListView,ListView包含ListTiles行,每一行用divider分割。
```
void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
final tiles = _saved.map(
(pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
return new Scaffold(
appBar: new AppBar(
title: new Text('Saved Suggestions'),
),
body: new ListView(children: divided),
);
},
),
);
}
```
6.重启app,点击几个条目,点击AppBar上的列表图标,新的route显示了favorites。Navigator会在AppBar添加一个返回Back按钮。你不必明确实现Navigator.pop
,点击返回Back按钮,返回Home Route。
第七步 使用主题改变UI界面
最后一步,我们来玩玩主题,Theme控制app的外观和触控感受。你可以使用依赖物理设备或模拟器的默认主题,也可以自定义主题反应你的模型。
1.通过设置ThemeData类,你可以很容易的改变App的主题。当前你的app使用的是默认的主题,现在,改变app的主色primary color
为白色。
```
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Startup Name Generator',
theme: new ThemeData(
primaryColor: Colors.white,
),
home: new RandomWords(),
);
}
}
```
2.重载App。可以看到现在背景色是白的,甚至app bar。
3.作为读者的练习,使用 ThemeData 改变UI的其他方面(aspect)。在Material库的 Colors类提供了很多颜色,玩玩吧!