本文由 简悦 SimpRead 转码, 原文地址www.raywenderlich.com
在本章中,您将从头开始构建您的第一个Flutter应用程序,并掌握基本原理.
现在你已经对Flutter有了基本的认识,那么你已经准备好开始你的Flutter学习生涯了。你的第一个任务是从头开始建立一个基本的应用程序,让您能够掌握基本工具的使用和Flutter应用的基本结构。你会动手修改这个应用并了解如何使用一些常用的组件,如ListView和Slider。
创建一个简单的应用程序将让你看到用Flutter构建跨平台的应用程序是多么的快速和容易--你很快就能掌握它。
在本章结束时,你将建立一个轻量级的菜谱应用程序。由于你刚刚开始学习Flutter,这个应用程序将提供一个写死的食谱列表数据,你需要使用滑块根据份数重新设置总数。
下面是你将要完成的应用程序最终的样子:
开始这一章之前,你所需要的是将Flutter环境设置好。如果flutter doctor的结果没有错误,你就可以开始了。否则,请回到上一章,重新配置flutter环境。
创建一个新的应用程序 有两种简单的方法来启动一个新的Flutter应用程序。在上一章中,你通过IDE创建了一个新的应用程序项目。你也可以用flutter命令创建一个应用程序。这里你将使用第二个选项。
打开一个终端窗口,然后进入到你想要创建工程的地方。例如,你可以在本课程资料的文件夹下创建工程,跳转到flta-materials/02-hello-flutter/projects/starter/。
创建一个新项目是很简单。在终端上运行
flutter create recipes
这个命令在一个新的文件夹中创建一个新的应用程序,文件夹和应用程序的名字都叫recipes。项目中有基本的演示demo,支持在iOS和Android上运行,正如你在前一章看到的那样。
使用你的IDE,通过open an existing project选项打开recipes文件夹。
构建并运行,你会看到与第一章 "入门 "中相同的示例应用程序。
点击 "+"按钮可以增加计数器。
现在开始学习编写自己的程序吧
这个示例程序很不错 ,因为flutter create命令为它创建了完成的模板代码,使您能够启动和运行。但这并不是你自己的应用程序。正如你在main.dart的顶部看到的那样,它就是MyApp。
class MyApp extends StatelessWidget {
这定义了一个名为MyApp的Dart类,它继承(extends)或者说拓展(inherits)自StatelessWidget。在Flutter中,用户界面上几乎所有的东西都是Widget。一个StatelessWidget组件在你构建它之后就不会改变。在下一节中,你会学到更多关于Widget和State的知识。现在,就把MyApp看作是应用程序的容器。
由于你正在建立一个食谱应用程序,你的主类命名为MyApp就不合适了,把它命名为RecipeApp吧。
你可以手动改变类的名字,但通过使用IDE的重命名操作,可以避免在复制和粘贴的过程中出错,这个命令在会同时修改所有调用这个类的地方。
在Android Studio中,你可以在Refactor ▸ Rename菜单项下找到这个功能,或者在MyApp类中右击MyApp...并导航到Refactor ▸ Rename。把MyApp重命名为RecipeApp。结果会是这样的。
void main() {
runApp(RecipeApp());
}
class RecipeApp extends StatelessWidget {
main()是应用程序启动时的代码入口。 runApp()告诉Flutter哪个是应用程序的顶级部件。
热重载对main函数不起作用,所以要想看到UI有没有变化需要重新构建和运行。 :]
注意:正如第1章 "入门 "中提到的,当你保存你的修改时,热重载会自动运行并更新UI。如果不起作用,检查你的IDE中的Flutter设置,确认保存时热重载这个选项是启用状态。
如果你更新了改变应用程序样式的代码,你只要进行热重启就可以了。但如果你做了重大改变,你必须停止并重启应用程序并重新构建。
改变应用程序的样子
为了继续把它变成一个新的应用程序,接下来你将定制你的小部件的UI。将RecipeApp的build()改为。
// 1
@override
Widget build(BuildContext context) {
// 2
return MaterialApp(
// 3
title: 'Recipe Calculator',
theme:ThemeData(
// 4
primaryColor: Colors.white,
accentColor: Colors.black
),
// 5
home:MyHomePage(title: 'Recipe Calculator');
);
}
这段代码改变了应用程序的样子:
- 一个widget的build()方法可以将多个其他Widget组合在一起形成一个新的组件
- MaterialApp是一个使用Material Design的应用程序的顶级widget。
- 该应用的标题是设备用来区分各个app的。UI界面上不会展示。
- 主题决定了app的主体样式和颜色等等。在这里,主色(the primary color)是Colors.white,重点色(the accent color)是Colors.black。
- 这仍然使用与之前相同的MyHomePage部件,但现在,你已经更新了标题,设备上app的名称已经改了。
当你现在重新启动应用程序时,你会看到同样的小工具,但它们的风格更加复杂。
通过定制 "MaterialApp "主体,你已经迈出了使应用程序成为你自己的第一步。你将在下一节中完成对原有示例应用程序的清理。
清理应用程序
你已经设计了应用程序的主题,但它仍然显示着计数器的演示。清理屏幕是你的下一个步骤。首先,将_MyHomePageState类替换为。
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
// 1
return Scaffold(
// 2
appBar: AppBar(
title:Text(widget.title),
),
// 3
body: SafeArea(
// 4
child:Container();
),
);
}
}
快速看一下这显示了什么。
Scaffold为一个应用程序提供了整体结构,在这里,我们使用了它的两个属性。AppBar通过title字段设置了一个 "Text "组件的标题属性,该组件引用了上一步 "homeMyHomePage(title: 'Recipe Calculator') "中传入的 "title"属性。body有SafeArea,它使应用程序不会和一些系统组件冲突,如系统状态栏。SafeArea有一个child小组件,这是一个空的Container小组件。
一个热重载后,你就会留下一个干净的应用程序。
建立一个recipe list
一个空荡荡的recipe应用没什么意义。它应该有一个漂亮的食谱列表供用户滚动浏览。但是在你展示列表之前,你需要数据来填充用户界面。
添加数据模型
你将使用Recipe作为这个应用程序中食谱的主要数据结构。
在lib文件夹中创建一个新的Dart文件,名为recipe.dart。
在该文件中添加以下类。
class Recipe {
String label;
String imageUrl;
Recipe(this.label, this.imageUrl);
}
这是一个带有标签和图片链接的数据模型。
你还需要为应用程序提供一些数据来显示。在一个正常的应用程序中,你一般从本地数据库或网络中获取这些数据。然而,为了简单起见,当你开始使用Flutter时,你将在本章中使用写死(hard-coded)的数据。
给Recipe添加以下代码。
static List<Recipe> samples = [
Recipe('Spaghetti and Meatballs',
'assets/2126711929_ef763de2b3_w.jpg'),
Recipe('Tomato Soup',
'assets/27729023535_a57606c1be.jpg'),
Recipe('Grilled Cheese',
'assets/3187380632_5056654a19_b.jpg'),
Recipe('Chocolate Chip Cookies',
'assets/15992102771_b92f4cc00a_b.jpg'),
Recipe('Taco Salad',
'assets/8533381643_a31a99e8a6_c.jpg'),
Recipe('Hawaiian Pizza',
'assets/15452035777_294cefced5_c.jpg'),
];
这个static List<Recipe>是写死的。你以后会添加更多属性,但现在,它只是一个名字和图片的List。
注意:
List是一个有序的数据集合;在一些编程语言中,它被称为数组。`List'的索引从0开始。
你已经创建了一个带有图片的List,但是你的项目中还没有任何图片。要添加它们,需要在Finder中把你项目资料中的02-hello-flutter下的assets文件夹复制到你的项目的根目录中。当你完成以后,assets应该与lib文件夹处于同一级别。这样,当你运行应用程序时,它就能找到这些图片。
你会注意到,在Finder中复制粘贴之后,文件夹和图片会自动显示在Android Studio项目列表中。
但是仅仅把资产添加到项目中并不能在应用程序中显示它们。要让应用程序找到这些图片,请打开Recipes项目根文件夹中的pubspec.yaml。
在 "# To add assets to your application,.... "下添加以下几行。
assets:
- assets/
这些行指定**assets/**是一个资产文件夹,必须包含在应用程序中。确保这里的第一行与上面的uses-material-design: true行对齐。
显示列表
在数据准备好后,你的下一步是为数据创建一个展示的地方。
在main.dart中,你需要导入数据文件,这样main.dart中的代码才能找到它。在文件的顶部,在其他导入行下添加以下内容。
import 'recipe.dart';
接下来,在_MyHomePageState SafeArea的子程序中,用以下代码替换掉child:Container():
// 4
child:ListView.builder(
// 5
itemCount: Recipe.samples.length,
// 6
itemBuilder:(BuildContext context, int index) {
// 7
return Text(Recipe.samples[index].label);
},
),
这段代码做了以下工作。
- 使用ListView建立一个列表。
- itemCount决定了列表的行数。在本例中,length是Recipe.samples列表中对象的数量。 itemBuilder为每一行建立widget树。
- 一个文本部件显示配方的名称。
- 现在执行热重载,你会看到下面的列表。
将列表放入卡片
你现在显示的是真实的数据,这很好,但这几乎不是一个应用程序。为了使事情变得更有趣,你需要添加图片来配合标题。
为了做到这一点,你将使用一个Card组件。在Material Design中,Card组件是用户界面的一个区域,在这个区域中,你可以展示一个特定实体的相关信息。例如,在一个音乐应用中的卡片可能会有专辑名称、艺术家和发行日期的标签,以及专辑封面的图片,也许还有一个用星星来评分的控件。
你的菜谱卡片将有菜谱的标签和图片。卡片的widget树有以下结构。
在main.dart中,在_MyHomePageState的底部,添加以下内容,使用buildRecipeCard()创建一个自定义的widget。
Widget buildRecipeCard(Recipe recipe) {
// 1
return Card(
// 2
child: Column(
// 3
children: <Widget>[
// 4
Image(image: AssetImage(recipe.imageUrl)),
// 5
Text(recipe.label),
],
),
);
}
这里是你如何定义你的新的自定义卡片部件:
- 你从buildRecipeCard()返回一个Card。
- Card的child是一个Column。Column是一个定义垂直布局的widget。
- Column有两个child。
- 第一个child是一个图像部件。AssetImage说明图片是从pubspec.yaml中定义的本地资产包中获取的。
- 第二个child是一个文本小组件。它将包含recipe.label的值。
要使用该Card,请到_MyHomePageState,并将ListView itemBuilder的返回语句更新为这样。
return buildRecipeCard(Recipe.samples[index])。
这就指示itemBuilder为样本列表中的每个配方使用这个自定义卡片部件。
hot restart应用程序以看到图片和文本卡片。
请注意,卡片的底部默认并不是一个直角。Material Design提供了一个标准的圆角和阴影。
观察小部件树
现在是思考整个应用的widget树的好时机。你还记得它是从main()的RecipeApp开始的吗?
RecipeApp建立了一个MaterialApp,它又使用MyHomePage作为它的主页。这构建了一个带有AppBar和ListView的Scaffold。然后你更新了ListView的构建器,为每个items做了一个Card。
思考widget树有助于理解应用程序,因为布局变得越来越复杂,而且你会增加互动性。幸运的是,你不需要每次都手绘图表。
在Android Studio中,当你的应用程序运行时,从View ▸ Tool Windows ▸ Flutter Inspector菜单中打开Flutter Inspector。这将打开一个强大的UI调试工具。
这个视图向你显示屏幕上的所有小部件以及它们的组成方式。当你滚动时,你可以刷新树状图。你可能会注意到卡片的数量在变化。这是因为列表并没有把每一个项目都同时保留在内存中以提高性能。你会在后面的章节中详细介绍这一点。
让它看起来更漂亮
默认的卡片看起来还不错,但它们并不像它们可以做到的那样漂亮。有了一些额外的东西,你可以使卡片变得更漂亮。这包括用布局部件(如Padding)包裹部件,或指定额外的样式参数。
开始时,用buildRecipeCard()代替。
Widget buildRecipeCard(Recipe recipe) {
return Card(
// 1
elevation: 2.0,
// 2
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0)),
// 3
child: Padding(
padding: const EdgeInsets.all(16.0),
// 4
child: Column(
children: <Widget>[
Image(image: AssetImage(recipe.imageUrl)),
// 5
const SizedBox(
height: 14.0,
),
// 6
Text(
recipe.label,
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w700,
fontFamily: 'Palatino',
),
)
],
),
),
);
}
这有几个更新要看。
- 卡片的高度决定了卡片在屏幕上的高度,影响它的阴影。
- shape处理卡片的形状。这段代码定义了一个角半径为10.0的圆角。
- Padding通过指定的padding值嵌套其子节点的内容。
- padding的子节点仍然是Column。
- 在图像和文本之间是一个SizedBox。这是一个有固定尺寸的空白视图。
- 你可以用一个样式对象来定制文本部件。在这个例子中,你已经指定了一个Palatino字体,大小为20.0,字体粗细度为w700。
Hot reload,你会看到一个更有风格的列表。
你可以修改这些属性值,使列表看起来 "恰到好处"。通过热重载,你可以很容易地做出改变,并立即看到它们对运行中的应用程序的影响。
使用 Widget inspector,你会看到添加的Padding和SizedBox小部件。当你选择一个部件,比如SizedBox,它会在一个单独的窗格中显示它的所有实时属性,其中包括你明确设置的属性和那些被继承的或默认设置的属性。
选择一个小组件也会突出显示它的源码。
注意:你可能需要点击刷新树按钮来重新加载检查器中的小部件结构。详情见第4章,"了解小组件"。
添加一个配方的详细页面
你现在有一个漂亮的列表,但这个应用程序无法和用户互动。让它变得更好的是,当用户点击卡片时,向他们展示关于食谱的细节。你将通过让卡片对点击作出反应来开始实现这个功能。
添加点击事件
在_MyHomePageState中,找到ListView.builder()。将itemBuilder的返回语句替换为以下内容。
// 7
return GestureDetector(
// 8
onTap: () {
// 9
Navigator.push(context,
MaterialPageRoute(
builder: (context) {
// 10
return Text('Detail page');
},
),
);
},
// 11
child: buildRecipeCard(Recipe.samples[index]),
);
这引入了一些新的Widget和概念。一个一个地看这几行。
- 引入了一个GestureDetector部件,顾名思义,它可以检测手势。
- 实现了一个onTap函数,当小组件被点击时,它被调用回调。
- Navigator小组件管理着一个页面栈。用MaterialPageRoute调用push()会把一个新的Material Page推到栈中。第三节,"Navigating Between Screens",将更详细地介绍Navigator。
- builder创建目标页面。
- GestureDetector的child定义了手势的活动区域。
Hot reload加载应用程序,现在每个卡片都可以被点击了。
创建一个实际的目标页面
点击产生的的页面显然只是一个占位符。它不仅难看,而且因为它没有所有正常的页面特征,用户现在被困在这里,特别在没有后退按钮的iOS设备上。不过别担心,你可以解决这个问题
在lib中,创建一个名为recipe_detail.dart的新Dart文件。
现在,将这段代码添加到该文件中。
import 'package:flutter/material.dart';
import 'recipe.dart';
class RecipeDetail extends StatefulWidget {
final Recipe recipe;
const RecipeDetail({Key? key, required this.recipe}) : super(key: key);
@override
_RecipeDetailState createState() {
return _RecipeDetailState();
}
}
这将创建一个新的StatefulWidget,它有一个初始化器,接收要显示的Recipe细节。这是一个StatefulWidget,因为你以后会在这个页面上添加一些交互式状态。
你需要_RecipeDetailState来构建这个小部件,所以接下来添加这个。
class _RecipeDetailState extends State<RecipeDetail> {
@override
Widget build(BuildContext context) {
// 1
return Scaffold(
appBar: AppBar(
title: Text(widget.recipe.label),
),
// 2
body: SafeArea(
// 3
child: Column(
children: <Widget>[
// 4
SizedBox(
height: 300,
width: double.infinity,
child: Image(image:
AssetImage(widget.recipe.imageUrl)),
),
// 5
const SizedBox(
height: 4,
),
// 6
Text(
widget.recipe.label,
style: const TextStyle(fontSize: 18),
),
],
),
),
);
}
}
小部件的主体与你已经看到的一样。这里有几件事需要注意。
- Scaffold定义了页面的整体结构。
- 在body中,有一个SafeArea,包含一个Column,Column里面有一个包含两个SizedBox和一个Text的 children。
- SafeArea使应用程序不至于离操作系统组件太近,例如大多数iPhone的凹槽或互动区。
- 这里有一个新的Widget是包裹图像的SizedBox,它定义了图像的尺寸。在这里,高度固定为300,宽度会调整以保证长宽比不变。Flutter中的单位是逻辑像素(不是物理像素)。
- 有一个间隔的SizedBox。
- 标签的文本与Card中的文本的Style不一样,以显示你有多少可定制性。 接下来,回到main.dart,在文件的顶部添加以下一行。
import 'recipe_detail.dart';
然后,在_MyHomePageState里面,找到GestureDetector的onTap参数,把MaterialPageRoute的返回语句替换成。
return RecipeDetail(recipe: Recipe.samples[index]);
从菜单中选择 Run ▸ Flutter Hot Restart来执行热重启,将应用状态设置为原始列表。点击一个配方卡现在会显示RecipeDetail页面。
注意:你需要在这里使用热重启,因为热重载在你更新状态后不会更新UI。
因为您现在有一个带有appBar的支架,Flutter会自动包含一个返回按钮,让用户回到主列表。
添加成分
为了完成详细页面,你需要向配方类添加额外的细节。在这之前,你必须给菜谱添加一个配料表。
打开recipe.dart,添加以下类。
class Ingredient {
double quantity;
String measure;
String name;
Ingredient(this.quantity, this.measure, this.name);
}
这是一个成分的简单数据容器。它有一个名称、一个计量单位--如 "杯 "或 "汤匙"--和一个数量。
在Recipe类的顶部,添加以下内容。
int servings;
List<Ingredient> ingredients;
这就增加了一些属性,指定该菜可供多少人实用,而配料是一个简单的列表。
为了使用这些新的属性,在Recipe类中进入你的样本列表,将Recipe的构造函数从。
Recipe(this.label, this.imageUrl);
改为:
Recipe(this.label, this.imageUrl, this.servings, this.ingredients);
你会看到你的部分代码下有红色的斜线,因为servings和 ingredients的值还没有被设置。接下来你会解决这个问题。
为了包括新的servings和 ingredients属性,用下面的代码替换现有的samples代码。
static List<Recipe> samples = [
Recipe(
'Spaghetti and Meatballs',
'assets/2126711929_ef763de2b3_w.jpg',
4,
[
Ingredient(1, 'box', 'Spaghetti'),
Ingredient(4, '', 'Frozen Meatballs'),
Ingredient(0.5, 'jar', 'sauce'),
],
),
Recipe(
'Tomato Soup',
'assets/27729023535_a57606c1be.jpg',
2,
[
Ingredient(1, 'can', 'Tomato Soup'),
],
),
Recipe(
'Grilled Cheese',
'assets/3187380632_5056654a19_b.jpg',
1,
[
Ingredient(2, 'slices', 'Cheese'),
Ingredient(2, 'slices', 'Bread'),
],
),
Recipe(
'Chocolate Chip Cookies',
'assets/15992102771_b92f4cc00a_b.jpg',
24,
[
Ingredient(4, 'cups', 'flour'),
Ingredient(2, 'cups', 'sugar'),
Ingredient(0.5, 'cups', 'chocolate chips'),
],
),
Recipe(
'Taco Salad',
'assets/8533381643_a31a99e8a6_c.jpg',
1,
[
Ingredient(4, 'oz', 'nachos'),
Ingredient(3, 'oz', 'taco meat'),
Ingredient(0.5, 'cup', 'cheese'),
Ingredient(0.25, 'cup', 'chopped tomatoes'),
],
),
Recipe(
'Hawaiian Pizza',
'assets/15452035777_294cefced5_c.jpg',
4,
[
Ingredient(1, 'item', 'pizza'),
Ingredient(1, 'cup', 'pineapple'),
Ingredient(8, 'oz', 'ham'),
],
),
];
这就为这些项目填写了一个配料表。请不要在家里做这些,这些只是例子。]
现在热重新加载应用程序。没有任何变化是可见的,但它应该能成功构建。
显示成分
一份食谱如果没有配料,就没有什么用处。现在,你准备添加一个小部件来显示它们。
在recipe_detail.dart中,在最后一个文本后的栏目中添加以下内容。
// 7
Expanded(
// 8
child: ListView.builder(
padding: const EdgeInsets.all(7.0),
itemCount: widget.recipe.ingredients.length,
itemBuilder: (BuildContext context, int index) {
final ingredient = widget.recipe.ingredients[index];
// 9
return Text(
'${ingredient.quantity} ${ingredient.measure} ${ingredient.name}');
},
),
),
这段代码增加了。
- 一个Expand小组件,它可以填补剩下的所有空间。这样一来,配料表就会占据其他部件没有填充的空间。
- 一个ListView,每个原料有一行。
- 一个Text,使用string interpolation,用运行时的值填充一个字符串。你在字符串字面内使用${expression}的语法来表示这些。 通过选择 Run ▸ Flutter Hot Restart,并导航到一个详细的页面来查看成分。
干得好,现在屏幕上显示了配方名称和成分。接下来,你将添加一个功能,让用户可以互动。
添加一个服务滑块
你现在显示的是建议食用的成分。如果你能改变所需的数量,并让配料的数量自动更新,那不是很好吗?
你将通过添加一个Slider组件来实现这一点,允许用户调整份量。
首先,在_RecipeDetailState的顶部创建一个实例变量来存储滑块的值。
int _sliderVal = 1;
在Column内部,Expand部件之后,添加以下Slider部件。
Slider(
// 10
min: 1,
max: 10,
divisions: 10,
// 11
label: '${_sliderVal * widget.recipe.servings} servings',
// 12
value: _sliderVal.toDouble(),
// 13
onChanged: (newValue) {
setState(() {
_sliderVal = newValue.round();
});
},
// 14
activeColor: Colors.green,
inactiveColor: Colors.black,
),
Slider呈现一个圆形的拇指,可以沿着轨道拖动来改变一个值。下面是它的工作原理。
- 你用min、max和divisions来定义滑块的移动方式。在这个例子中,它在1和10的值之间移动,有10个不连续的停止点。也就是说,它只能有1、2、3、4、5、6、7、8、9或10的值。
- 标签随着_sliderVal的变化而更新,并显示一个扩大展示serving数量。
- 滑块默认用double工作,所以这就转换成int变量。
- 反之,当滑块发生变化时,这里使用round()将double型的滑块值转换为int,然后将其保存在_sliderVal中。
- 这将滑块的颜色设置为更突出的颜色。activeColor是最小值和游标之间的部分,而inactiveColor代表其余部分。
Hot reload 加载应用程序,调整滑块并观察反映在指标上的值。
更新配方
看到改变后的值反映在滑块上是很好的,但现在,它并不影响配方本身。
要做到这一点,你只需要改变扩展成分itemBuilder的返回语句,将_sliderVal的当前值作为每个成分的一个因素。
return Text('${ingredient.quantity * _sliderVal} '
'${ingredient.measure} '
'${ingredient.name}');
hot reload后,你会看到,当你移动滑块时,食谱的成分会发生变化。
就这样。你现在已经建立了一个很酷的、交互式的Flutter应用程序,并且它在在iOS和Android上的工作方式是一样的。
在接下来的几节中,您将继续探索部件和状态如何工作。你还会了解到一些重要的功能,如网络。
关键点
- 用flutter创建一个新的应用程序。
- 使用widget,用控件和布局组成一个屏幕。
- 使用widget参数进行造型。
- 一个MaterialApp部件指定了应用程序,而Scaffold指定了一个特定屏幕的高级结构。 状态允许交互式部件。
- 当状态改变时,你通常需要热重启应用程序,而不是热重载。在某些情况下,你可能还需要完全重建和重启应用程序。
接下来该怎么做?
恭喜你,你已经写出了你的第一个应用程序
为了了解所有可用的widget选项,api.flutter.dev/ 的文档应该是你的起点。特别是,Material库api.flutter.dev/flutter/mat… 和Widget库api.flutter.dev/flutter/wid… 它们将涵盖大部分你可以放在屏幕上的东西。这些页面列出了所有的参数,并且经常有浏览器内的互动部分,你可以进行实验。
第3章,"基本部件",是关于使用部件的所有内容,第4章,"理解部件",更详细地介绍了部件背后的理论。未来的章节将更深入地讨论本章中简要介绍的其他概念。