0x02.编写第一个Flutter应用-[Flutter]
Tue, May 8, 2018Flutter官网上Get started里的教程写的还是挺好的,因此本教程基于Flutter官网教程【Write Your First Flutter App】翻译撰写。
本指南将带领你完成第一Flutter应用。阅读本文不需要具备Dart或移动端开发经验,但需要了解变量、循环、分支等基本的编程概念。
- 创建可运行的Flutter应用
- 使用扩展包(external package)
- 添加一个有状态组件(Stateful widget)
- 创建支持无限滚动的列表视图
- 添加交互
- 导航到新的页面
- 使用主题改变UI样式
- 搞定
下面的动画展示了应用完成后的效果。
通过本教程,将能够掌握以下知识
- Flutter应用的基本结构
- 查找、使用包扩展功能
- 使用热重载(hot reload)进行快速开发
- 如何实现一个有状态组件
- 如何创建一个无限的懒加载列表
- 如何创建并导航到另一页面
- 如何使用主题改变应用外观
在开始旅程前,请确保您已正确安装了Flutter SDK(含Dart SDK)、Android SDK及IDE(VS Code、IntelliJ IDEs)。
一、创建Flutter应用
我一般都是用idea,装好插件后,按步骤创建新的Flutter project就可以了,如有疑问可参考官方文档Getting Started with your first Flutter app,文档里详细写了android studio、VS Code和命令行三种方式下新建项目的方法。
好了,下面进入正题。
修改lib/main.dart文件为以下内容
// 导入flutter/material包 import 'package:flutter/material.dart'; // 定义main函数,执行runApp void main() => runApp(new MyApp()); // 定义继承自无状态组件的组件类 class MyApp extends StatelessWidget { // 重载build方法 @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'), ), ), ); } }
点击运行,可以在模拟器/真机中看到下图的效果。
现在我们的第一个Flutter应用已经可以成功运行了。
知识点:
- 创建了一个最基本的Material应用(Material是谷歌推出的一套面向Web、APP的UI设计语言),Flutter框架提供了丰富的Material组件。
- main方法中的【=>】是dart中对于当行函数/方法的简写方式。
- 应用扩展自无状态组件(StatelessWidget),这使得应用成为一个Flutter组件。在Flutter中,所有的东西都被抽象为组件,除了一般UI框架中的按钮、文本框等,对齐(alignment)、外边框(padding)和布局(layout)等也都由组件实现。
- Material库中的Scaffold组件提供了一个基本的页面布局,包括标题栏(appBar)及主视图区域(body)。Flutter通过构造控件树的方式维护页面结构,使用Flutter提供的组件可以快速构建控件树,搭建页面。
- 组件类的build方法返回组件树结构,来描述如何显示。
- 目前主视图区域的组件树由一个居中组件(Center)包含一个文本组件(Text)构成。居中组件(Center)的作用是使其子组件相对其父组件居中显示。
二、使用扩展包
在本步骤中,我们将导入一个开源扩展包english_words,这个包中包含了数千个常用的英文单词及相关的函数。在dart官方包管理网站可以找到更多的关于dart和flutter的开源包(可能需要科学上网。。。)
Flutter通过pubspec文件对项目配置及资源进行管理。修改pubspec.yaml,在其中添加对于包english_words的依赖(3.0或更高版本)。
dependencies: flutter: sdk: flutter cupertino_icons: ^0.1.0 english_words: ^3.1.0
右键点击pubspec.yaml,选择Flutter->Package get,在控制台中将看到类似如下的信息
flutter packages get Running "flutter packages get" in startup_namer... Process finished with exit code 0
在lib/main.dart中导入english_words包
import 'package:flutter/material.dart'; import 'package:english_words/english_words.dart';
使用english_words包生成单词替换”Hello World”
import 'package:flutter/material.dart'; import 'package:english_words/english_words.dart'; void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { final wordPair = new WordPair.random(); 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 Text(wordPair.asPascalCase), // 使用这部分替换 ), ), ); } }
点击IDE中的【】图标,不需要重新部署应用就可以看到刚刚的修改结果。通常情况下,我们再修改代码后都只需要热重载,而不需要重新部署应用。
wordPair.asPascalCase会随机取得英文单词,而组件的build方法会在每次需要刷新页面时被调用,所以每次刷新都会显示不同的单词。
出问题了?请再次仔细检查是否正确修改点,如果仍然不行,可以直接使用下面文件的内容替换工程中的文件。出于学习的目的考虑不建议直接替换文件,更好的方式是比较下面文件的内容与你写的有和差异并进行修改。
三、添加有状态组件(Stateful widget)
无状态组件的不可变性意味着,组件一旦创建,其属性就无法改变。
有状态组件可以在其生命周期中修改状态值。实现一个有状态组件至少需要两个类:
- 一个有状态组件类(继承自StatefulWidget)
- 一个状态类(继承自State)
有状态组件类自身不可变,而是通过自身持有的状态类(State class)修改组件状态。
在本步骤中我们将添加一个有状态组件RandomWords,这个组件将创建一个状态类RandomWordsState。这个状态类将维护组件中显示的建议和喜欢单词对。
添加一个有状态组件类RandomWords到main.dart。可以在类MyApp的作用域外的任何地方添加。在这个示例中,我们将其添加到MyApp类之后。
class RandomWords extends StatefulWidget { @override createState() => new RandomWordsState(); }
添加一个状态类RandomWordsState。包括构建组件、维持状态的主要代码都位于这个类中。这个类将保存随着用户滚动而无限增长的生成的单词对,以及最喜欢的单词对,因为用户通过切换心脏图标来将它们从列表中添加或删除。 我们将逐步构建这个类,首先创建类。
class RandomWordsState extends State<RandomWords> { }
接下来实现build方法。如果使用IDE,会看到IDE的提示。
class RandomWordsState extends State<RandomWords> { @override Widget build(BuildContext context) { final wordPair = new WordPair.random(); return new Text(wordPair.asPascalCase); } }
删除、修改类MyApp中注释部分的内容
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { // final wordPair = new WordPair.random(); // 删除本行 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(wordPair.asPascalCase), // 修改本行内容为下面的内容 child: new RandomWords(), // 新的内容 ), ), ); } }
重启APP,如果热重载应用可能会收到以下警告。
Reloading...
Not all changed program elements ran during view reassembly; consider
restarting.
这可能是误报,但考虑重新启动以确保您的更改反映在应用的用户界面中。
应用程序应该像以前一样运行,每次热重新加载或保存应用程序时都会显示一个单词对。
实际使用过程中确实发现过热重载机制无法将应用刷新到最新状态的情况,如果开发过程中明明修改了却死活不起效,重新部署一下试试吧。(具体什么情况下需要重新部署,我目前还不清楚)
有问题?
四、创建无限滚动列表视图
经过了上面的三步、我们虽然创建了一个可以运行的应用,但是应用的就够和功能上似乎没有太大的变化。在本步骤中,我们将利用列表视图的构造器工厂方法,为应用添加列表视图。通过构造器方法,可以实现列表滚动时的懒加载。
再RandomWordsState类中添加数组变量
_suggestions
来保存建议单词对。这个变量以下划线【_】开头,在Dart语言中通常以此表示一个私有变量。 紧接着添加一个变量biggerFont
来保存文字大小。class RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _biggerFont = const TextStyle(fontSize: 18.0); ... }
在类RandomWordsState中添加一个方法
_buildSuggestions()
。这个方法构造一个列表视图来显示建议单词对。 列表视图类提供了构造属性itemBuilder
,我们需要提供一个匿名函数,作为这个工厂构造方法的回调函数。匿名方法接收两个参数:构造上下文(BuildContext)和行迭代i
。迭代从0开始,每次调用函数自增,每次向列表添加一个单词对,所有单词只生成一次。这个模型可以在用户滚动时生成一个无限增长的列表。 修改代码为如下内容class RandomWordsState extends State<RandomWords> { ... Widget _buildSuggestions() { return new ListView.builder( padding: const EdgeInsets.all(16.0), // 回调函数每次生成一个单词对添加到列表项中 // 偶数行时添加一个单词对,奇数行时添加 // 注意:分隔符的样式在模拟器中看上去可能不同 itemBuilder: (context, i) { // 添加1像素的分隔符 if (i.isOdd) return new Divider(); // 由于奇数时生成分割线,这里用i整除2,将i换算成单词对数组的实际下标 final index = i ~/ 2; // 滚动到底部时,生成新的单词对 if (index >= _suggestions.length) { // 每次生成10个单词 _suggestions.addAll(generateWordPairs().take(10)); } return _buildRow(_suggestions[index]); } ); } }
_buildSuggestions
方法为每个单词对调用_buildRow
,这个方法为每个新的单词对生成列表项。 添加_buildRow
方法class RandomWordsState extends State<RandomWords> { ... Widget _buildRow(WordPair pair) { return new ListTile( title: new Text( pair.asPascalCase, style: _biggerFont, ), ); } }
修改
_buildSuggestions()
方法,将body部分替换为我们新增加的方法_buildSuggestions
class RandomWordsState extends State<RandomWords> { ... @override Widget build(BuildContext context) { // 删除下面两行 // final wordPair = new WordPair.random(); // return new Text(wordPair.asPascalCase); // 修改为下面的内容 return new Scaffold ( appBar: new AppBar( title: new Text('Startup Name Generator'), ), body: _buildSuggestions(), ); } ... }
修改MyApp类的build方法,删除Scaffold和AppBar。这些将放到
RandomWordsState
类中进行监管,在后续步骤中我们将导航到其他页面中,这样的方式,可以让我们更方便的替换标题等的内容。class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Startup Name Generator', home: new RandomWords(), ); } }
重启引用,可以看到下面的效果了。
有问题?
五、添加交互事件
在本步骤中,我们将在每行中添加一个可点击的心形图标,通过点击心形图标可以将选定的单词对添加到收藏夹或从中删除。
添加一个集合(Set)变量
_saved
。使用集合而非列表(List),是应为我们的需求不允许添加重复记录,集合的实现正好满足我们的需求。class RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _saved = new Set<WordPair>(); final _biggerFont = const TextStyle(fontSize: 18.0); ... }
在
_buildRow
方法中添加变量alreadySaved
,用来保存单词对是否已经被添加到收藏列表。Widget _buildRow(WordPair pair) { final alreadySaved = _saved.contains(pair); ... }
修改
_buildRow
方法,在列表项中添加心形图标。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, ), ); }
重启应用,可以看到每行的后面均添加了心形图标,但目前还不能响应交互事件。
现在来实现添加收藏的交互功能。当点击单词对时,如果没有添加到收藏中则添加、如添加到收藏中则删除。 在组件中为onTab方法指定匿名函数,响应用户点击事件,并在其中使用
setState
修改组件属性并重绘页面。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); } }); }, ); }
热重载一下,我们的应用可以响应点击事件了
有问题?
六、导航到新页面
一步中,将添加一个新的页面用来显示收藏的单体对。同时还将添加首页与收藏页之间的导航处理。 在Flutter中,导航(Navigator)管理管理着包含应用路由(routes)的堆栈(stack),屏幕始终显示在栈顶的页面,将新的路由压入栈中后,屏幕显示刚刚入栈的页面,将顶层页面弹出栈后,屏幕返回到上一个页面。 1. 在标题栏中添加一个图标,当用户点击图标是,跳转到收藏页。
class RandomWordsState extends State<RandomWords> {
...
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Startup Name Generator'),
// 添加下面三行代码
actions: <Widget>[
new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved),
],
),
body: _buildSuggestions(),
);
}
...
}
添加
_pushSaved
方法class RandomWordsState extends State<RandomWords> { ... void _pushSaved() { } }
当用户点击标题栏中的图标时,建造一个新的页面,并压入导航堆栈中,屏幕将显示新的页面。 新页面将使用MaterialPageRoute的
builder
属性指定的匿名函数进行创建。 添加对Navigator.push
的调用,将路由压入导航堆栈。void _pushSaved() { Navigator.of(context).push( ); }
下面的代码将使用一些函数时编程的内容。 首先迭代保存收藏单词对的集合,并根据每一项生成列表项,然后将生成项目重新组合成列表项。 接下来调用
ListTile.divideTiles
方法,向列表项中增加分隔符,并通过toList
方法转换为数组。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(); }, ), ); }
builer属性的匿名函数最终为路由返回一个
Scaffold
,其包含一个标题栏和根据收藏单词对生成的列表页。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), ); }, ), ); }
热重载应用,现在可以跳转到收藏页面了。 在收藏页的标题栏中,框架自动添加了返回按钮【<】,并实现了
Navigator.pop
功能。
七、修改应用主题 截止到上一步为止,我们的应用已经实现了教程开始设定的所有功能性目标,这一步中,我们通过使用主题来修改应用的整体观感。 1. 通过下面的代码,我们将主题颜色修改为白色
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(),
);
}
}
- 热重载应用,看看最终效果吧。
有问题?
总结 通过这个教程我们学习到了 - 创建一个Flutter - 编写Dart代码 - 使用第三方扩展包简化开发 - 使用热重载功能加速开发 - 实现一个有状态组件 - 创建一个懒加载的无限循环列表 - 创建新的页面并通过路由-导航机制跳转 - 使用主题修改应用的观感