1. 前言
原文发布在语雀:
Flutter入门总结 · 语雀 www.yuque.com
本文主要涉及内容如下:
文章写得比较仓促,欢迎指正。
示例代码在这里。
2. 环境搭建
- 安装flutter sdk
- 配置 iOS Android 开发环境
- 配置编辑器(VSCode、 Android Studio)
- 启动
3. Widget
Everything’s a Widget
3.1 组件介绍
3.1.1 Text
创建带格式的文本widget。
可控制字体、字号、粗细、对齐、颜色、溢出隐藏等属性。
Text(
'Hello Flutter! Hello Flutter! Hello Flutter!',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
3.1.2 Image
可显示asset图片、网络图片、本地图片、内存中的图片。
- new Image.asset, for obtaining an image from an AssetBundle using a key.
- new Image.network, for obtaining an image from a URL.
- new Image.file, for obtaining an image from a File.
- new Image.memory, for obtaining an image from a Uint8List.
Image.network('https://i.loli.net/2019/03/07/5c80d019a6663.jpg', fit: BoxFit.fitWidth)
3.1.3 Container
一个拥有更多UI装饰能力的widget, 类似div view。
支持 padding margin 背景图片 border 3d变换等。通常作为其他widget的容器。
Container(
margin: EdgeInsets.all(10.0),
color: Colors.blue,
width: 100.0,
height: 100.0,
);
3.1.4 Row、Column
其设计应当是借鉴前端flex布局,继承Flex widget。属性及能力与前端flex基本一致。
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Container(
width: 80,
height: 100,
color: Colors.blueGrey,
child: Center(
child: Text('block 1'),
),
),
Container(
width: 100,
height: 180,
color: Colors.pink,
child: Center(
child: Text('block 2'),
),
),
Container(
width: 80,
height: 140,
color: Colors.teal,
child: Center(
child: Text('block 3'),
),
),
],
);
主要属性:
- mainAxisAlignment → MainAxisAlignment 主轴对齐方式:
- center
- end
- spaceAround
- spaceBetween
- spaceEvenly
- start
其中spaceAround、spaceBetween以及spaceEvenly的区别在于距离首尾widget到边界的距离分别是空白区域的1/2、0、1。
- crossAxisAlignment → CrossAxisAlignment 交叉轴对齐方式:
- baseline
- center
- start
- end
- stretch
3.1.5 Stack
允许子元素堆叠,类似前端绝对定位。
子widget分为两种:
- Positioned,相当于前端中设置 position: absolute, 此时可以设置 top left等属性进行定位
- unPositioned, 普通widget,可以被Positioned widget 遮挡
Container(
width: double.infinity, // 在宽度上强制撑满 height: 300,
child: Stack(
children: <Widget>[
Container(
color: Colors.blueGrey,
width: 100,
height: 100,
alignment: Alignment.center,
child: Text('block 1 unPositioned', style: TextStyle(color: Colors.white)),
),
Positioned(
top: 80,
left: 80,
child: Container(
color: Colors.pink,
width: 100,
height: 100,
alignment: Alignment.center,
child: Text('block 2 positioned', style: TextStyle(color: Colors.white)),
),
),
Positioned(
right: 150,
bottom: 60,
child: Container(
color: Colors.teal,
width: 100,
height: 100,
alignment: Alignment.center,
child: Text('block 3 positioned', style: TextStyle(color: Colors.white),),
),
),
],
),
);
3.2 StatelessWidget 与 StatefulWidget
StatelessWidget 无状态widget,StatefulWidget 为持有内部状态的Widget。
在Flutter中,所有widget都是不可变的(immutable),那么StatefulWidget如何持有内部状态呢。
StatefulWidget由以下两部分组成:
- StatefulWidget 类
- State类
StatefulWidget始终是不变的,但State具有自己的生命周期,可以保持、修改状态,必须使用setState()修改状态。
以下为 flutter create 生成demo中的示例:
// StatefulWidget 类 class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
// State 类 class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
实现效果为,点击按钮增加 _counter,并在页面上显示。
3.3 State生命周期
| 生命周期 | 描述 | | — | — | | initState | Called when this object is inserted into the tree
当widget插入渲染树时调用 | | build | Describes the part of the user interface represented by this widget
描述此窗口小部件表示的用户界面部分
构建widget时调用 | | didChangeDependencies | Called when a dependency of this State object changes
当State依赖的InheritedWidget改变时调用 | | didUpdateWidget | Called whenever the widget configuration changes
当组件状态发生改变时调用 | | deactivate | Called when this object is removed from the tree
从渲染树中移除时调用 | | dispose | Called when this object is removed from the tree permanently
当widget销毁时调用 |
盗闲鱼的一张图:
3.4 widget 间通信
Flutter与React Vue类似,都是「状态流向下,事件流向上」,即单向数据流。
先看下示例效果:
三个组件,Parent为父组件,还有两个子组件Child1, Child2。
3.4.1 状态流向下传递
Child1接收Parent组件的state,并显示。
// child1.dart
class Child1 extends StatelessWidget {
final String child1Data;
Child1({
this.child1Data,
});
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.all(20),
elevation: 4,
child: Column(
children: <Widget>[
ListTile(
title: Text('Child1', style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20)),
subtitle: Text('接收父组件data'),
),
Divider(),
Container(
padding: EdgeInsets.all(20),
child: Text('child1 Data 的值: $child1Data', style: TextStyle(fontSize: 18)),
),
],
),
);
}
}
// parent.dart
class Parent extends StatefulWidget {
@override
State<StatefulWidget> createState() => _ParentState();
}
class _ParentState extends State<Parent> {
String parentData;
void _onTextFieldChanged(value) {
_updateParentData(value);
}
void _updateParentData(value) {
setState(() {
parentData = value;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Card(
margin: EdgeInsets.all(10),
elevation: 4,
child: Column(
children: <Widget>[
ListTile(
title: Text('Parent', style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20)),
),
Divider(),
Container(
padding: EdgeInsets.all(20),
child: Text('Parent Data 的值: $parentData', style: TextStyle(fontSize: 18)),
),
TextField(
decoration: InputDecoration(fillColor: Colors.blue.shade100, filled: true, labelText: 'Parent 输入框'),
onChanged: _onTextFieldChanged,
),
Divider(),
Child1(child1Data: parentData),
],
),
)
],
);
}
}
在parent widget中我们主要做了两件事:
- 监听TextField输入框变化并将值存储在state parentData中
- 将parentData 传递给child1 widget
在child1 widget中我们接收这个参数并将其显示在Text widget中
3.4.2 事件流向上传递
child2 widget收集自身输入框的数据并将输入传递给parent widget并显示。
同react一样,parent widget将修改state的方法传递给child2 widget,child2调用方法时携带需要传递的数据作为参数即可实现state由下往上传递。
// parent.dart
class Parent extends StatefulWidget {
@override
State<StatefulWidget> createState() => _ParentState();
}
class _ParentState extends State<Parent> {
String parentData;
void _onTextFieldChanged(value) {
_updateParentData(value);
}
void _updateParentData(value) {
setState(() {
parentData = value;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Card(
margin: EdgeInsets.all(10),
elevation: 4,
child: Column(
children: <Widget>[
ListTile(
title: Text('Parent', style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20)),
),
Divider(),
Container(
padding: EdgeInsets.all(20),
child: Text('Parent Data 的值: $parentData', style: TextStyle(fontSize: 18)),
),
TextField(
decoration: InputDecoration(fillColor: Colors.blue.shade100, filled: true, labelText: 'Parent 输入框'),
onChanged: _onTextFieldChanged,
),
Divider(),
Child1(child1Data: parentData),
Child2(updateParentData: _updateParentData),
],
),
)
],
);
}
}
// parent2.dart
class Child2 extends StatefulWidget {
final Function updateParentData;
Child2({
this.updateParentData,
});
@override
State<StatefulWidget> createState() => _Child2State();
}
class _Child2State extends State<Child2> {
String child2Data;
void _onTextFieldChanged(value) {
setState(() {
child2Data = value;
widget.updateParentData(value);
});
}
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.all(20),
elevation: 4,
child: Column(
children: <Widget>[
ListTile(
title: Text('Child2', style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20)),
subtitle: Text('通过回调传递data到父组件'),
),
Divider(),
Container(
padding: EdgeInsets.all(20),
child: Text('child2 Data 的值: $child2Data', style: TextStyle(fontSize: 18)),
),
Divider(),
TextField(
decoration: InputDecoration(fillColor: Colors.blue.shade100, filled: true, labelText: 'Child2 输入框'),
onChanged: _onTextFieldChanged,
),
],
),
);
}
}
在parent中将修改state的方法传递给child2。
child2中,监听TextField变化,并调用parent传递的方法,将输入作为参数。
3.4.3 InheritedWidget
类似React context,允许跨级传输数据,子组件可订阅祖先节点state。
4. 路由和导航
管理多个页面时有两个核心概念和类:
Route和
Navigator。 一个route是一个屏幕或页面的抽象,Navigator是管理route的Widget。Navigator可以通过route入栈和出栈来实现页面之间的跳转。
常用API:
- Navigator.push()
- Navigator.pop()
4.1 基础使用
// Navigator.push
onPressed: (BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen(param1: 'xxx')),
);
},
// Navigator.pop
onPressed: (BuildContext context) {
Navigator.pop(context);
}
4.2 具名路由
在main方法中声明具名路由:
// main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Home(), // 这里声明的页面会被默认为 '/' 路径指向的页面 routes: <String, WidgetBuilder>{
'/widgetExample': (BuildContext context) => WidgetExample(),
'/dataTransfer': (BuildContext context) => DataTransfer(),
},
);
}
}
也可以使用下面的写法:
// main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
initialRoute: '/', // 指定默认路由 routes: <String, WidgetBuilder>{
'/': (BuildContext context) => Home(),
'/widgetExample': (BuildContext context) => WidgetExample(),
'/dataTransfer': (BuildContext context) => DataTransfer(),
},
);
}
}
然后就可以像下面这样调用:
Navigator.of(context).pushNamed('/widgetExample');
注意:
上面这种方式并不支持动态传参。
4.3 定制路由
可以定制路由过渡样式:
下面的示例实现的是页面旋转淡出的效果:
// home.dart
_navCustomRoute(BuildContext context) {
Navigator.push(
context,
PageRouteBuilder(
// opaque: false, transitionDuration: Duration(seconds: 1),
pageBuilder: (context, _, __) => CustomRoute(),
transitionsBuilder: (___, Animation<double> animation, ____, Widget child) {
return FadeTransition(
opacity: animation,
child: RotationTransition(
turns: Tween<double>(begin: 0.5, end: 1.0).animate(animation),
child: child,
),
);
},
),
);
}
效果如图:
5. 网络请求与JSON解析
5.1 网络请求
5.1.1 原生
import 'dart:io';
var httpClient = new HttpClient();
get() async {
var httpClient = new HttpClient();
var uri = new Uri.http(
'example.com', '/path1/path2', {'param1': '42', 'param2': 'foo'});
var request = await httpClient.getUrl(uri);
var response = await request.close();
var responseBody = await response.transform(UTF8.decoder).join();
}
5.1.2 http库
import 'package:http/http.dart' as http;
void get() async {
var client = http.Client();
http.Response response = await client.get(url_2);
_content = response.body;
}
5.1.3 dio库
dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时等…
void get() async {
Dio dio = new Dio();
Response response = await dio.get(url_3);
}
5.2 JSON解析
使用Dio进行网络请求设置返回格式为json时,我们会得到Map,使用自带的 convert 方法解码JSON也是得到Map,键值对的Map在使用中并不方便,需要进一步解析成Model实体。
常用解析策略有两种:
- 手动序列化和反序列化
- 通过代码生成自动序列化和反序列化
依据项目复杂度与应用场景灵活选择。
假定JSON结构如下,我们来看一下如何解析:
// lib/mock/mock.json
{
"id": "487349",
"name": "Hayden",
"score": 1000
}
5.2.1 手动反序列化
手动JSON序列化是指使使用 dart:convert 中内置的 JSON 解码器。它将原始 JSON字 符串传递给JSON.decode() 方法,然后在返回的 Map 中查找所需的值。 它没有外部依赖或其它的设置,对于小项目很方便。
创建model:
// lib/model/student_model.dart
class Student {
String studentId;
String studentName;
int studentScores;
Student({
this.studentId,
this.studentName,
this.studentScores,
});
factory Student.fromJson(Map<String, dynamic> parsedJson) {
return Student(
studentId: parsedJson['id'],
studentName: parsedJson['name'],
studentScores: parsedJson['score'],
);
}
}
工厂函数fromJson主要功能是键名的映射,fromJson的参数parsedJson类型定义为Map是因为键名总是String,但键的类型并不确定。
接下来我们在页面中使用该Model解析上文的Json数据:
// lib/pages/json_decode.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import '../model/student_model.dart';
class JsonDecode extends StatelessWidget {
Future<String> _loadJsonFile() async {
return await rootBundle.loadString('lib/mock/mock.json');
}
Future loadStudent() async {
// 加载JSON文件 String jsonString = await _loadJsonFile();
// 解码 final jsonRes = json.decode(jsonString);
// 解析 Student student = new Student.fromJson(jsonRes);
print(student.studentName);
print(student);
print(jsonString);
print(jsonRes);
print(jsonRes['name']);
}
@override
Widget build(BuildContext context) {
loadStudent();
return Scaffold(
appBar: AppBar(
title: Text('JSON 解析'),
),
body: Text('请在控制台中查看打印'),
);
}
}
loadStudent方法中:
第一步,引入JSON文件,得到json字符串
第二部,使用自带convert 解码第一步得到的json字符串,生成对应Map
第三步,使用Studen Model解析Map,得到Student类的实例
控制台打印如下:
5.2.2 自动生成反序列化代码
在这里我们使用json_serializable实现自动反序列化
**
5.2.2.1 一、添加依赖包
在pubspec.yaml中添加我们需要的依赖包:
dependencies: json_annotation: ^2.0.0 dev_dependencies: build_runner: ^1.0.0 json_serializable: ^2.0.0
这里我们添加了三个依赖包:json_annotation build_runner json_serializable,保存修改后vscode会自动安装依赖包,也可以手动安装(cmd + shift + p 命令面板中选择 Flutter: Get Packages)
5.2.2.2 二、创建Studen实体类
// lib/model/studen_model_auto.dart
class StudentAuto {
String studentId;
String studentName;
int studentScores;
StudentAuto({
this.studentId,
this.studentName,
this.studentScores,
});
}
5.2.2.3 三、关联
// lib/model/studen_model_auto.dart
import 'package:json_annotation/json_annotation.dart';
part 'student_model_auto.g.dart';
@JsonSerializable()
class StudentAuto {
@JsonKey(name: 'id')
String studentId;
@JsonKey(name: 'name')
String studentName;
@JsonKey(name: 'scores')
int studentScores;
StudentAuto({
this.studentId,
this.studentName,
this.studentScores,
});
factory StudentAuto.fromJson(Map<String, dynamic> json) => _$StudentAutoFromJson(json);
Map<String, dynamic> toJson() => _$StudentAutoToJson(this);
}
在@JsonKey中我们做了键名映射。
此时flutter会报错,因为我们还没有生成 student_model_auto.g.dart 文件
5.2.2.4 生成JSON解析文件
我们已经在StudentAuto实体类前加了注解 @JsonSerializable()
在项目根目录下执行 flutter packages pub run build_runner build
完成后会在 lib/model 目录自动生成 student_model_auto.g.dart 文件
来看下这个文件的内容,其实与手动解析是一样的:
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'student_model_auto.dart';
// ************************************************************************** // JsonSerializableGenerator // **************************************************************************
StudentAuto _$StudentAutoFromJson(Map<String, dynamic> json) {
return StudentAuto(
studentId: json['id'] as String,
studentName: json['name'] as String,
studentScores: json['scores'] as int);
}
Map<String, dynamic> _$StudentAutoToJson(StudentAuto instance) =>
<String, dynamic>{
'id': instance.studentId,
'name': instance.studentName,
'scores': instance.studentScores
};
5.2.2.5 使用model反序列化json
// lib/pages/json_decode.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import '../model/student_model_auto.dart';
class JsonDecode extends StatelessWidget {
Future<String> _loadJsonFile() async {
return await rootBundle.loadString('lib/mock/mock.json');
}
Future loadStudentAuto() async {
String jsonString = await _loadJsonFile();
final jsonRes = json.decode(jsonString);
StudentAuto student = StudentAuto.fromJson(jsonRes);
print(student.studentName);
print(student);
print(jsonString);
print(jsonRes);
print(jsonRes['name']);
}
@override
Widget build(BuildContext context) {
loadStudentAuto();
return Scaffold(
appBar: AppBar(
title: Text('JSON 解析'),
),
body: Text('请在控制台中查看打印'),
);
}
}
控制台打印如图: