第2章
Flutter基础知识
在创建了第一个Flutter程序后,我们还需要继续补充Flutter的基础知识。在后面的章节中讲解组件、布局、动画、装饰等时都需要用到这些基础知识。
本章将围绕以下几个知识点展开:
□入口程序
□Material Design
□Flutter主题
□无状态组件和有状态组件
□使用包资源
□Http请求
2.1 入口程序
每一个Flutter项目的/lib目录下都有一个main.dart文件,打开该文件,里面应该有一个main()函数。Flutter使用Dart语言开发,而在Dart语言中,main()函数是Dart程序的入口,也就是说,Flutter程序在运行的时候,第一个执行的函数就是main()函数。如下面的代码所示:
void main() => runApp(Widget app);
如果你是第一次接触Dart语言,可能会对上面的语法感到陌生,这是Dart语言特有的速写形式,将其展开后,完整代码如下所示:
void main() {
return runApp(Widget app);
}
从上面的代码中可以看到,main()函数中只调用runApp函数,使用runApp函数可以将给定的根组件填满整个屏幕。你可能会有疑问,为什么一定要使用runApp函数?如果不调用runApp函数,项目也可以正常执行,但是屏幕上什么都不会显示。Flutter是Dart语言的移动应用框架,runApp函数就是Flutter框架的入口,如果不调用runApp函数,那你执行的就是一个Dart控制台应用。更多关于Dart语言的细节,会在下面第3章“Dart语言简述”专门讲解。
2.2 Material Design设计风格
每一个.dart文件的第一行几乎都会导入flutter/material.dart包,这个包是Flutter实现Material Design设计风格的基础包,里面有文本输入框(Text)、图标(Icon)、图片(Image)、行排列布局(Row)、列排列布局(Column)、Decoration(装饰器)、动画等组件,大家可以将它们理解为网页中的按钮、标题、选项框等组件库。第一行代码如下所示:
import 'package:flutter/material.dart';
那么Material Design又是什么呢?是谷歌推出的一套视觉设计语言。比如有的App可以换皮肤,而每一套皮肤就是一种设计语言,有古典风、炫酷风、极简风,等等,而Material Design就是谷歌风。Flutter采用的就是Material Design风格。
2.3 Flutter主题
为了在整个应用中使用同一套颜色和字体样式,可以使用“主题”这种方式。定义主题有两种方式:全局主题,或使用Theme来定义应用程序局部的颜色和字体样式。事实上,全局主题只是由应用程序根MaterialApp创建的主题(Theme)。
定义一个主题后,就可以在我们自己的Widget中使用它,Flutter提供的Material Widgets将使用主题为AppBars、Buttons、Checkboxes等设置背景颜色和字体样式。
2.3.1 创建应用主题
创建主题的方法是将ThemeData提供给MaterialApp构造函数,这样就可以在整个应用程序中共享包含颜色和字体样式的主题。ThemeData的主要属性如表2-1所示。
如果没有提供主题,Flutter将创建一个默认主题。主题数据的示例代码如下:
new MaterialApp(
title: title,
theme: new ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.lightBlue[800],
accentColor: Colors.cyan[600],
),
);
2.3.2 局部主题
如果我们想在应用程序的某一部分使用特殊的颜色,那么就需要覆盖全局的主题。有两种方法可以解决这个问题:创建特有的主题数据或扩展父主题。
1.创建特有的主题数据
实例化一个ThemeData并将其传递给Theme对象,代码如下:
new Theme(
//创建一个特有的主题数据
data: new ThemeData(
accentColor: Colors.yellow,
),
child: new FloatingActionButton(
onPressed: () {},
child: new Icon(Icons.add),
),
);
2.扩展父主题
扩展父主题时无须覆盖所有的主题属性,我们可以通过使用copyWith方法来实现,代码如下:
new Theme(
//覆盖accentColor为Colors.yellow
data: Theme.of(context).copyWith(accentColor: Colors.yellow),
child: new FloatingActionButton(
onPressed: null,
child: new Icon(Icons.add),
),
);
2.3.3 使用主题
主题定义好后就可以使用它了。首先,函数Theme.of(context)可以通过上下文来获取主题,方法是查找最近的主题,如果找不到就会找整个应用的主题。
下面来看一个简单的示例,应用的主题颜色定义为绿色,界面中间再加一个带有背景色的文本。
完整的例子代码如下所示:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final appName = '自定义主题';
return new MaterialApp(
title: appName,
theme: new ThemeData(
brightness: Brightness.light,//应用程序整体主题的亮度
primaryColor: Colors.lightGreen[600],//App主要部分的背景色
accentColor: Colors.orange[600],//前景色(文本、按钮等)
),
home: new MyHomePage(
title: appName,
),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
MyHomePage({Key key, @required this.title}) : super(key: key);
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: new Center(
child: new Container(
//获取主题的accentColor
color: Theme.of(context).accentColor,
child: new Text(
'带有背景颜色的文本组件',
style: Theme.of(context).textTheme.title,
),
),
),
floatingActionButton: new Theme(
//使用copyWith的方式获取accentColor
data: Theme.of(context).copyWith(accentColor: Colors.grey),
child: new FloatingActionButton(
onPressed: null,
child: new Icon(Icons.computer),
),
),
);
}
}
自定义主题的效果如图2-1所示。
2.4 无状态组件和有状态组件
无状态组件(StatelessWidget)是不可变的,这意味着它们的属性不能改变,所有的值都是最终的。
有状态组件(StatefulWidget)持有的状态可能在Widget生命周期中发生变化。实现一个StatefulWidget至少需要两个类:
□一个StatefulWidget类。
□一个State类。StatefulWidget类本身是不变的,但是State类在Widget生命周期中始终存在。
Flutter的官方给出一个有状态组件的示例,点击右下角的+号按钮,应用界面中间的数字会加1,如图2-2所示。
这个示例有几个关键的部分,解析如下。
示例代码中MyHomePage必须继承自StatefulWidget类,如下所示:
class MyHomePage extends StatefulWidget
重写createState方法,如下所示:
@override
MyHomePageState createState() => new _MyHomePageState();
状态类必须继承自State类,如下所示:
class _MyHomePageState extends State
定义一个普通变量_counter作为计数器变量,调用setState方法来控制这个变量的值的变化,如下所示:
int _counter = 0;
void _incrementCounter() {
setState(() {
//计数器变量
_counter++;
});
}
完整的示例代码如下所示:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
//MyApp不需要做状态处理,所以此组件继承StatelessWidget即可
class MyApp extends StatelessWidget {
// 这个组件是整个应用的主组件
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
//自定义主题
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
//主页需要继承自StatefulWidget
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
//标题
final String title;
//必须重写createState方法
@override
_MyHomePageState createState() => new _MyHomePageState();
}
//状态类必须继承State类,注意后面需要指定为
class _MyHomePageState extends State {
int _counter = 0;//计数器
void _incrementCounter() {
//调用State类里的setState方法来更改状态值,使得计数器加1
setState(() {
//计数器变量,每次点击让其加1
_counter++;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
//居中布局
body: new Center(
//垂直布局
child: new Column(
//主轴居中对齐
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',//绑定计数器的值
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,//按下+号按钮调用自增函数
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
}
}
2.5 使用包资源
Flutter包类似于Java语言里的jar包,由全球众多开发者共同提供第三方库。例如,网络请求(http)、自定义导航/路由处理(fluro)、集成设备API(如url_launcher&battery) 以及第三方平台SDK(如Firebase)等。这使得开发者可以快速构建应用程序,而无须从头造轮子。
1.包仓库
所有包(package)都会发布到Dart的包仓库里,如图2-3所示,输入你想使用的包后点击搜索即可。
提示:包仓库地址为:https://pub.dartlang.org
2.包使用示例
接下来使用url_launcher这个包来详解讲解第三方包的使用,步骤如下。
步骤1:打开pubspec.yaml文件,在dependencies下添加包的名称及版本,如图2-4箭头指向的内容所示。
步骤2:点击Packages get命令来获取工程配置文件中所添加的引用包,或者打开命令行窗口执行flutter packages get命令,如图2-5所示。
注意:在更新包资源的过程中注意观察控制台消息,可能有版本错误、网络问题,这些都会导致更新失败。
步骤3:打开main.dart文件,导入url_launcher.dart包:
import 'package:url_launcher/url_launcher.dart';
步骤4:这时就可以使用launch方法来打开url地址了:
const url = ' https://www.baidu.com';
launch(url);
完整的main.dart代码如下所示:
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: '使用第三方包示例',
home: new Scaffold(
appBar: new AppBar(
title: new Text('使用第三方包示例'),
),
body: new Center(
child: new RaisedButton(
onPressed: () {
//指定url并发起请求
const url = 'https://www.baidu.com';
launch(url);
},
child: new Text('打开百度'),
),
),
),
);
}
}
步骤5:启动示例,打开界面如图2-6所示。
点击“打开百度”按钮,页面会跳转至百度页面,如图2-7 所示。
2.6 Http请求
HTTP协议通常用于做前后端的数据交互。Flutter请求网络有两种方法,一种是用Http请求,另一种是用HttpClient请求。
1. Http请求方式
在使用Http方式请求网络时,需要导入http包。如下所示:
import 'package:http/http.dart' as http;
请看下面的完整示例代码,示例中发起了一个http的get请求,并将返回的结果信息打印到控制台里:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'http请求示例',
home: new Scaffold(
appBar: new AppBar(
title: new Text('http请求示例'),
),
body: new Center(
child: new RaisedButton(
onPressed: () {
var url = 'http://httpbin.org/';
//向http://httpbin.org/发送get请求
http.get(url).then((response) {
print("状态: ${response.statusCode}");
print("正文: ${response.body}");
});
},
child: new Text('发起http请求'),
),
),
),
);
}
}
请求界面如图2-8所示。
点击“发起http请求”按钮,程序开始请求指定的url,如果服务器正常返回数据,则状态码为200。控制台输出内容如下:
Performing hot reload...
Syncing files to device iPhone X...
Reloaded 1 of 509 libraries in 452ms.
flutter: 状态: 200
flutter: 正文:
注意:服务器返回状态200,同时返回正文。完整的正文远不只这些内容,你可以自己测试此示例,并查看控制台的输出消息。
2. HttpClient请求方式
在使用HttpClient方式请求网络时,需要导入io及convert包,如下所示:
import 'dart:convert';
import 'dart:io';
请看下面的完整示例代码,示例中使用HttpClient请求了一条天气数据,并将返回的结果信息打印到控制台里。具体请求步骤看代码注释即可。
import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:io';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
//获取天气数据
void getWeatherData() async {
try {
//实例化一个HttpClient对象
HttpClient httpClient = new HttpClient();
//发起请求
HttpClientRequest request = await httpClient.getUrl(
Uri.parse("http://t.weather.sojson.com/api/weather/city/101030100"));
//等待服务器返回数据
HttpClientResponse response = await request.close();
//使用utf8.decoder从response里解析数据
var result = await response.transform(utf8.decoder).join();
//输出响应头
print(result);
//httpClient关闭
httpClient.close();
} catch (e) {
print("请求失败:$e");
} finally {
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'httpclient请求',
home: Scaffold(
appBar: AppBar(
title: Text('httpclient请求'),
),
body: Center(
child: RaisedButton(
child: Text("获取天气数据"),
onPressed: getWeatherData,
),
),
),
);
}
}
请求界面如图2-9所示。
点击“获取天气数据”按钮,程序开始请求指定的url,如果服务器正常返回数据,则状态码为200。控制台输出内容如下:
Performing hot reload...
Syncing files to device iPhone X...
Reloaded 1 of 419 libraries in 412ms.
flutter: {"time":"2018-11-30 08:09:00","cityInfo":{"city":"天津市","cityId":
"101030100","parent":"天津","updateTime":"07:56"},"date":"20181130","message":
"Success!", "status":200,"data":{"shidu":"84%","pm25":30.0,"pm10":79.0,"quality":"良","wendu":"1","ganmao":"极少数敏感人群应减少户外活动","yesterday":{"date":"29日星期四","sunrise":"07:08","high":"高温 9.0℃","low":"低温 0.0℃","sunset":"16:50","aqi":86.0,"fx":"东风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"}, "forecast": [{"date":"30日星期五","sunrise":"07:09","high":"高温 10.0℃","low":"低温 1.0℃","sunset":"16:50","aqi":53.0,"fx":"西风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"},{"date":"01日星期六","sunrise":"07:10","high":"高温 10.0℃","low":"低温4.0℃", "sunset":"16:49","aqi":94.0,"fx":"东风","fl":"<3级","type":"阴","notice":"不要被阴云遮挡住好心情"},{"date":"02日星期日","sunrise":"07:11","high":"高温 1<…>
注意:返回的数据是JSON格式,所以后续还需要做JSON处理。另外还需要使用utf8.decoder从response里解析数据。
如果请求里需要带参数,可以在URI里增加查询参数,具体的请求地址和参数要根据实际需要编写,代码如下所示:
Uri uri=Uri(scheme: "https", host: "t.weather.sojson.com", queryParameters: {
"_id": 26,
"city_code": "101030100",//接口需要的city_code
"city_name": "天津"
});