本文从 Flutter 的运行环境搭建开始介绍如何使用 Flutter,之后以相关知识点的说明、代码演示以及实际结果展示来介绍基本控件的使用方法。
使用 Flutter 需要首先了解 Dart 的语法,如果你还不清楚如何使用 Dart,可以先去看看「 Dart 」一文了解 Dart 基础知识。
搭建运行环境
-
安装并配置 JDK
-
下载安装 Android Studio
下载网址:Download Android Studio and SDK tools
安装到自定义目录
-
下载配置 Flutter SDK
下载网址:Windows install | Flutter
安装到自定义目录后,把 Flutter 安装目录的 bin 目录配置到 path 环境变量
命令行输入
flutter -v
检查是否配置成功 -
配置 Flutter 国内镜像
方法是将配置下面两条环境变量:
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn PUB_HOSTED_URL: https://pub.flutter-io.cn
-
运行
flutter doctor
命令检测环境是否配置成功可能会遇到的问题:cmdline-tools component is missing
运行
flutter doctor --android-licenses
后全部选择y
再次运行
flutter doctor
显示如下信息则表示配置成功:Doctor summary (to see all details, run flutter doctor -v): [√] Flutter (Channel stable, 2.5.3, on Microsoft Windows [Version 10.0.19042.1348], locale zh-CN) [√] Android toolchain - develop for Android devices (Android SDK version 31.0.0) [√] Chrome - develop for the web [√] Android Studio (version 2020.3) [√] IntelliJ IDEA Ultimate Edition (version 2021.1) [√] Connected device (2 available) • No issues found!
-
在 Android Studio 配置 Flutter 插件
打开 Android Studio,选择 Plugins -> Maketplace,搜索并安装 Flutter 插件
-
创建 Flutter 项目
New Flutter Project -> Flutter -> 设置 Flutter SDK -> 设置项目信息 -> Finish
-
在 Android Studio 中导入运行 Flutter 项目
导入项目文件夹中的
android
文件夹,等待下载 gradle 构建工具 -
在手机上调试
用数据线连接手机和电脑,开启手机的调试模式并允许 USB 安装
在 Android Studio 中选择 run -> run 'app' ,会在手机上安装应用
打开应用(下图是默认的应用样式):
在 VSCode 中开发
在 VSCode 中安装 Flutter 和 Flutter Widget Snippets 插件。
打开 Flutter 项目的文件夹,在终端中使用flutter run
命令来运行项目。
在终端中按下下列按键可实现相应效果:
r 键:点击后热加载,即重新加载。
R 键:点击后重新启动。
p 键:显示网格,让开发者掌握布局情况。
o 键:切换 android 和 iOS 等预览模式。
q 键:退出调试预览模式。
Hello Flutter
目录结构
下面是生成的项目文件中我们主要用到的文件:
android:android 平台相关代码
ios:iOS 平台相关代码
lib:flutter 相关代码,是我们主要编写的代码
test:测试代码
pubspec.yaml:配置文件,一般存放一些第三方库的依赖
入口文件
Flutter 的入口文件是 lib/main.dart
:
void main(){
runApp(MyApp());
}
main
方法是 Dart 的入口方法,runApp
是 Flutter 的入口方法, MyApp
是自定义的控件,应用程序显示的是 MyApp
控件中的内容。
基本控件
这一节介绍 Flutter 常用的基本控件,包括控件的作用、属性和使用样例。看完本节你可以了解到这些控件的使用场景和使用方法。
Center 控件
在代码中使用 Flutter ,你首先需要导入 Flutter 包,可以输入快捷指令 fimpmat
:
import 'package:flutter/material.dart';
Center 控件将其子 widget 居中显示在自身内部的 widget:
import 'package:flutter/material.dart';
void main() {
runApp(
Center(
child: Text(
'Hello Flutter',
textDirection: TextDirection.ltr,
),
),
);
}
在 Center 控件中使用了 Text 子控件。
**Ctrl
+ 鼠标左键
点击控件名查看控件的源码,了解传递的参数。 **
抽离控件
由于直接在 runApp 中写控件内容使得代码结构不够清晰,我们需要将 runApp 中的内容抽离出来单独作为一个控件。
Flutter 中自定义控件是一个类,它们必须继承 StatelessWidget 或 StatefulWidget 这两个抽象类。
StatelessWidget 是无状态控件,状态不可变
StatefulWidget 是有状态控件,状态可能在 widget 生命周期中改变
我们需要实现抽象类中的抽象方法,点击快速修复可以看到我们需要实现 build
方法,它需要返回一个 widget 。我们将原来 runApp 中的参数写到 build 方法中的返回参数中,并在 runApp 中使用自定义控件。
import 'package:flutter/material.dart';
void main() {
runApp(MyAPP());
}
class MyAPP extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Text('Hello Flutter'),
);
}
}
Text 控件
我们可以给对 Text 控件中的内容进行修改。
class MyAPP extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Text(
'Hello Flutter',
// 文字方向
textDirection: TextDirection.ltr,
// 文字样式
style: TextStyle(
fontSize: 40.0,
color: Colors.yellow,
),
),
);
}
}
使用 Material Design
MaterialApp 控件
MaterialApp 封装了应用程序实现 Material Design 所需要的一些 widget。一般作为顶层 widget 使用。
常用属性:home 主页、title 标题、color 颜色、theme 主题、routes 路由等。
Scaffold 控件
Scaffold 是 Material Design 布局结构的基本实现,Scaffold 即脚手架,提供了用于显示 drawer、snackbar 和底部 sheet 的 API。
常用属性:
appBar - 显示在界面顶部的一个 AppBar
body - 当前界面所显示的主要内容 Widget
drawer - 抽屉菜单控件
使用这两个控件:
import 'package:flutter/material.dart';
void main() {
runApp(MyAPP());
}
class MyAPP extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// 主页
home: Scaffold(
// 导航栏
appBar: AppBar(
// 标题
title: Text('Flutter Demo'),
),
// 页面主体
body: HomeContent(),
),
// 主题
theme: ThemeData(
// 主题颜色
primarySwatch: Colors.yellow,
),
);
}
}
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Text(
'Hello Flutter',
style: TextStyle(
fontSize: 40.0,
color: Colors.yellow,
),
),
);
}
}
现在,我们的页面有了大致框架。
Container 控件
Container
widget 可以用来创建一个可见的矩形元素。 Container 可以使用 BoxDecoration
来进行装饰,如背景、边框、阴影等。 Container
还可以设置外边距、内边距和尺寸的约束条件等。另外,Container
可以使用矩阵在三维空间进行转换。
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
child: Text('Hello Flutter'),
alignment: Alignment.topCenter,
height: 300.0,
width: 300.0,
decoration: BoxDecoration(
color: Colors.yellow,
border: Border.all(
color: Colors.blue,
width: 2.0,
),
),
),
);
}
}
Image 控件
图片控件是显示图像的控件,常用 Image.asset
导入本地图片、Image.network
导入网络图片。
引入网络图片
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
color: Colors.yellow,
),
child: Image.network(
"https://flutter.cn/static/4ea7d7f5f72649f0bcec.png",
alignment: Alignment.topLeft,
// 背景颜色
// color: Colors.blue,
// 图片混合模式
// colorBlendMode: BlendMode.luminosity,
// 图片适配方式
fit: BoxFit.contain,
repeat: ImageRepeat.repeat,
),
),
);
}
}
实现图片的圆角
- 使用 BoxDecoration 实现
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
color: Colors.yellow,
// 圆角半径
borderRadius: BorderRadius.circular(150),
// 修饰图片
image: DecorationImage(
// 提供图片
image: NetworkImage(
"https://flutter.cn/static/4ea7d7f5f72649f0bcec.png"),
fit: BoxFit.contain,
repeat: ImageRepeat.repeat,
),
),
),
);
}
}
- 使用 ClipOval 实现
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
child: ClipOval(
child: Image.network(
"https://flutter.cn/static/4ea7d7f5f72649f0bcec.png",
height: 200,
width: 200,
fit: BoxFit.cover,
),
),
),
);
}
}
引入本地图片
-
新建两个目录
项目根目录\images\(2.0x/3.0x)
,image
目录相当于1.0x
-
将图片放入
images
文件夹和两个子文件夹中 -
配置
pubspec.yaml
文件在 flutter 下配置 assets:
flutter:
uses-material-design: true
assets:
- images/demo.jpg
- images/2.0x/demo.jpg
- images/3.0x/demo.jpg
-
在代码中使用
使用方法类似
Images.network
:
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
child: ClipOval(
child: Image.asset(
"images/demo.jpg",
height: 200,
width: 200,
fit: BoxFit.cover,
),
),
),
);
}
}
ListView 控件
垂直列表
ListView 默认为垂直列表:
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
padding: EdgeInsets.all(10),
children: <Widget>[
// 数组里可以放其他 Widget
ListTile(
// 设置前置图标
leading: Icon(
Icons.settings,
// 改变图标样式
color: Colors.yellow,
size: 30,
),
title: Text(
'点亮你的Vue技术栈,万字Nuxt.js实践笔记来了',
// 设置字体
style: TextStyle(
fontSize: 16,
),
),
subtitle: Text('作为一位 Vuer(vue开发者),如果还不会这个框架,那么你的 Vue 技术栈还没被点亮'),
// 设置后置图标
trailing: Icon(Icons.sentiment_satisfied_sharp),
),
ListTile(
title: Text('如何用 docker 打造前端开发环境'),
subtitle: Text('如何使用 docker 打造前端开发环境 docker 的用法很多,除了可以用来部署项目,还可'),
),
ListTile(
title: Text('如何做前端Code Review'),
subtitle: Text('向互联网大厂学习,从代码格式、代码错误、代码习惯、代码优化四个角度进行前端Co'),
),
],
);
}
}
水平列表
配置 scrollDirection
属性为 Axis.horizontal
,将排列方向改为水平排列,得到水平列表:
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 240,
child: ListView(
// 配置方向,默认为垂直列表
scrollDirection: Axis.horizontal,
children: <Widget>[
Container(
width: 180,
color: Colors.yellow,
),
Container(
width: 180,
color: Colors.orange,
// 列表嵌套
child: ListView(
children: <Widget>[
Image.network(
"https://flutter.cn/static/4ea7d7f5f72649f0bcec.png",
),
Image.network(
"https://flutter.cn/static/4ea7d7f5f72649f0bcec.png",
),
],
),
),
Container(
width: 180,
color: Colors.red,
),
],
),
);
}
}
动态列表
动态列表可以动态循环数据。
class HomeContent extends StatelessWidget {
// 自定义私有方法
List<Widget> _getData() {
List<Widget> list = [];
for (var i = 0; i < 20; i++) {
list.add(ListTile(
title: Text('this is list $i'),
));
}
return list;
}
@override
Widget build(BuildContext context) {
return ListView(
children: this._getData(),
);
}
}
使用外部数据:
// lib/res/listData.dart
List listData = [
{
"title": "4 年经验裸辞 2 个月,40 场面试、一路的心态变化及经验总结",
"author": "天明夜尽",
"avatar":
"https://p3-passport.byteacctimg.com/img/user-avatar/aa2fe44ba824fc1b5e21a73b565ef0db~300x300.image",
"imgUrl":
"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/78e7025d384846e9b0314a53a2adab36~tplv-k3u1fbpfcp-zoom-crop-mark:1304:1304:1304:734.awebp?",
},
{
"title": "[万字总结]我还在正确的道路上么?2021年一个前端新人的半年学习工作总结",
"author": "速冻鱼",
"avatar":
"https://p9-passport.byteacctimg.com/img/user-avatar/1a85c9561d83fc5e8a441432767677fb~300x300.image",
"imgUrl":
"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e4a813587ec84152b243c3d54323d01e~tplv-k3u1fbpfcp-zoom-crop-mark:1304:1304:1304:734.awebp?",
}
];
// lib/main.dart
import 'res/listData.dart';
...
class HomeContent extends StatelessWidget {
// 自定义私有方法
List<Widget> _getData() {
var tempList = listData.map((value) {
return ListTile(
leading: Image.network(value["imgUrl"]),
title: Text(value["title"]),
subtitle: Text(value["author"]),
);
});
return tempList.toList();
}
@override
Widget build(BuildContext context) {
return ListView(
children: this._getData(),
);
}
}
使用 ListView.builder:
ListView的标准构造函数适用于数目比较少的场景,如果数目比较多的话,最好使用ListView.builder
。因为 ListView 的标准构造函数会将所有 item 一次性创建,而 ListView.builder 会创建滚动到屏幕上显示的 item。
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: listData.length,
itemBuilder: (context, index) {
return ListTile(
leading: Image.network(listData[index]["imgUrl"]),
title: Text(listData[index]["title"]),
subtitle: Text(listData[index]["author"]),
);
},
);
}
}
还可以这么写:
class HomeContent extends StatelessWidget {
List<Widget> _getData() {
var tempList = listData.map((value) {
return ListTile(
leading: Image.network(value["imgUrl"]),
title: Text(value["title"]),
subtitle: Text(value["author"]),
);
});
return tempList.toList();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: this._getData().length,
itemBuilder: (context, index) {
return this._getData()[index];
},
);
}
}
GridView 控件
创建网格布局主要有两种方式,GridView.count
和 GridView.builder
。
GridView.count
:
import 'res/listData.dart';
...
class HomeContent extends StatelessWidget {
List<Widget> _getListData() {
var tmpList = listData.map((value) {
return Container(
child: Column(
children: <Widget>[
Image.network(value["imgUrl"]),
// Image 和 SizeBox 之间的距离
SizedBox(height: 10),
Text(value["title"]),
],
),
);
});
return tmpList.toList();
}
@override
Widget build(BuildContext context) {
return GridView.count(
// 一行的 widget 数量
crossAxisCount: 2,
// 两个 Widget 的水平距离
crossAxisSpacing: 20,
// 两个 Widget 的垂直距离
mainAxisSpacing: 20,
// 整个 GridView 的内边距
padding: EdgeInsets.all(10),
// 每个 Widget 的宽高比例
// childAspectRatio: 1.7,
children: this._getListData(),
);
}
}
GridView.bulider
:
class HomeContent extends StatelessWidget {
Widget _getListData(context, index) {
return Container(
child: Column(
children: <Widget>[
Image.network(listData[index]["imgUrl"]),
// Image 和 SizeBox 之间的距离
SizedBox(height: 10),
Text(listData[index]["title"]),
],
),
);
}
@override
Widget build(BuildContext context) {
return GridView.builder(
// 循环数据的数量
itemCount: listData.length,
itemBuilder: this._getListData,
// 设置主要布局
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 一行的 widget 数量
crossAxisCount: 2,
// 两个 Widget 的水平距离
crossAxisSpacing: 20,
// 两个 Widget 的垂直距离
mainAxisSpacing: 20,
),
);
}
}
页面布局控件
Padding 控件
Padding 控件可以给子控件添加内边距:
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.fromLTRB(0, 0, 10, 0),
child: GridView.count(
crossAxisCount: 2,
childAspectRatio: 1.7,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(10, 10, 0, 0),
child: Image.network(
"https://i0.hdslb.com/bfs/feed-admin/e03e0d22e0c361f1f0ec544dcd718e306b371980.jpg@880w_388h_1c_95q",
fit: BoxFit.cover,
),
),
Padding(...
],
),
);
}
}
Row 控件
Row
控件即行控件:
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 600,
child: Row(
// 主轴显示方式
mainAxisAlignment: MainAxisAlignment.center,
// 次轴显示方式
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
IconContainer(Icons.home),
IconContainer(Icons.search, color: Colors.yellow),
IconContainer(Icons.mail, color: Colors.orange),
],
),
);
}
}
// 自定义按钮控件
class IconContainer extends StatelessWidget {
double size;
Color color;
IconData icon;
IconContainer(this.icon, {this.color = Colors.red, this.size = 32});
@override
Widget build(BuildContext context) {
return Container(
height: 100,
width: 100,
color: color,
child: Center(
child: Icon(
icon,
size: size,
color: Colors.white,
),
),
);
}
}
Column 控件
Column 控件即列控件,使用方法类似 Row
控件:
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 600,
width: 200,
child: Column(
// 主轴显示方式
mainAxisAlignment: MainAxisAlignment.center,
// 次轴显示方式
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
IconContainer(Icons.home),
IconContainer(Icons.search, color: Colors.yellow),
IconContainer(Icons.mail, color: Colors.orange),
],
),
);
}
}
Expanded 控件
Expanded 控件类似于 Web 开发中的 flex 布局。
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
// 类似 web 中的 flex 属性,此时 width 属性失效
flex: 1,
child: IconContainer(Icons.home),
),
Expanded(
flex: 2,
child: IconContainer(Icons.search, color: Colors.yellow),
),
],
);
}
}
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
// 下面的代码使得左侧固定宽度,右侧自适应
Expanded(
child: IconContainer(Icons.home),
),
Expanded(
flex: 2,
child: IconContainer(Icons.search, color: Colors.yellow),
)
],
);
}
}
Stack 控件
相当于 css 里的绝对定位。
Stack 控件将其中的子控件堆叠在一起,定位所有的子控件只需要使用 Stack
控件:
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
// 将 Stack 中的元素堆到一起
child: Stack(
// 表示让其中所有的元素居中
// alignment: Alignment.center,
// 自定义方位,参数为 x 和 y,值域为 -1 到 1
alignment: Alignment(0, -0.1),
// 数组中的内容按照先后次序堆叠
children: <Widget>[
Container(
height: 400,
width: 300,
color: Colors.red,
),
Text(
"this is text1",
style: TextStyle(fontSize: 30),
)
],
),
);
}
}
与 Align 控件一起使用
如果想对 Stack 控件中的每个子控件分别定位,我们可以结合 Align 控件一起使用:
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
height: 400,
width: 300,
color: Colors.red,
child: Stack(
children: <Widget>[
Align(
alignment: Alignment.topLeft,
child: Icon(
Icons.home,
size: 40,
color: Colors.white,
),
),
Align(
alignment: Alignment.topCenter,
child: Icon(
Icons.search,
size: 40,
color: Colors.white,
),
),
Align(
alignment: Alignment.topRight,
child: Icon(
Icons.mail,
size: 40,
color: Colors.white,
),
),
],
),
),
);
}
}
与 Position 控件一起使用
相较于Align 控件的 alignment 属性只能定义大致方位,Position 通过 top、 bottom、 left、 right 四个参数控制控件的方位,类似 CSS的绝对定位。
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
height: 400,
width: 300,
color: Colors.red,
child: Stack(
children: <Widget>[
Positioned(
left: 0,
top: 0,
child: Icon(
Icons.home,
size: 40,
color: Colors.white,
),
),
Positioned(
left: 50,
top: 50,
child: Icon(
Icons.search,
size: 40,
color: Colors.white,
),
),
Positioned(
left: 100,
top: 100,
child: Icon(
Icons.mail,
size: 40,
color: Colors.white,
),
),
],
),
),
);
}
}
AspectRatio 控件
可以调整子元素的宽高比。
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 300,
child: AspectRatio(
// 设置子元素的宽高比
aspectRatio: 2.0 / 1.0,
child: Container(
color: Colors.pink,
),
),
);
}
}
Card 控件
实现卡片效果。
import 'res/listData.dart';
...
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: listData.map((value) {
return Card(
// 外边距
margin: EdgeInsets.all(10),
child: Column(
children: <Widget>[
AspectRatio(
aspectRatio: 16 / 9,
child: Image.network(
value["imgUrl"],
fit: BoxFit.cover,
),
),
ListTile(
// 添加头像
leading: CircleAvatar(
backgroundImage: NetworkImage(value["avatar"]),
),
title: Text(
value["title"],
// 最大行数
maxLines: 2,
// 文字溢出
overflow: TextOverflow.ellipsis,
),
subtitle: Text(value["author"]),
)
],
),
);
}).toList(),
);
}
}
Wrap 控件
单行的 Wrap 控件与 Row 控件几乎一致,当子控件所需的 mainAxis 主轴位置大于一行时, Wrap 控件会扩展到 crossAxis 交叉轴上显示。
首先定义一个 MyButton 控件,ElevatedButton 控件的详细用法在下文有介绍。
class MyButton extends StatelessWidget {
final String? text;
const MyButton(this.text);
@override
Widget build(BuildContext context) {
return ElevatedButton(
// 按下触发事件
onPressed: () {},
child: Text(text!),
);
}
}
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Wrap(
// 主轴方向,默认水平
direction: Axis.horizontal,
// 主轴对齐方式
alignment: WrapAlignment.start,
// 主轴子控件的间距
spacing: 10,
// 文本方向
textDirection: TextDirection.ltr,
// 交叉轴方向,默认从上到下
verticalDirection: VerticalDirection.down,
// 交叉轴对齐方式
runAlignment: WrapAlignment.start,
// 交叉轴子控件间距
runSpacing: 2,
children: <Widget>[
MyButton("第 1 集"),
MyButton("第 2 集"),
MyButton("第 3 集"),
MyButton("第 4 集"),
MyButton("第 5 集"),
MyButton("第 6 集"),
MyButton("第 7 集"),
],
);
}
}
StatefulWidget 有状态控件
前面提到的控件都是 StatelessWidget
无状态控件,下面我们来讲 StatefulWidget
有状态控件。
StatefulWidget
持有的状态可能在 widget
生命周期内发生改变。如果想改变页面中的数据就需要用到 StatefulWidget
。
在 VSCode 中安装 Awesome Flutter Snippets 插件可用于自动生成自定义控件架构。
在无状态控件中实现点击按钮改变控件中数据的功能是不可行的:
class HomeContent extends StatelessWidget {
int count = 0;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: <Widget>[
SizedBox(height: 100),
Text(
"You have pressed the button $count time(s).",
style: TextStyle(fontSize: 20),
),
SizedBox(height: 100),
ElevatedButton(
onPressed: () {
this.count++; // count 不发生变化
},
child: Text(
"Press here!",
style: TextStyle(fontSize: 20),
),
)
],
),
);
}
}
使用有状态控件:
// 有状态控件
class HomeContent extends StatefulWidget {
// 构造函数
HomeContent({Key? key}) : super(key: key);
@override
_HomeContentState createState() => _HomeContentState(/* 可在此传参 */);
}
class _HomeContentState extends State<HomeContent> {
int count = 0;
// 可通过构造函数接收参数
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: <Widget>[
SizedBox(height: 100),
Text(
"You have pressed the button $count time(s).",
style: TextStyle(fontSize: 20),
),
SizedBox(height: 100),
ElevatedButton(
onPressed: () {
// 使用 setState 可以重新渲染
setState(() {
count++;
});
},
child: Text(
"Press Here!",
style: TextStyle(fontSize: 20),
),
),
],
),
);
}
}
BottomNavigationBar 控件
用于配置底部的导航栏。
class MyAPP extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Tabs(),
theme: ThemeData(
primarySwatch: Colors.yellow,
),
);
}
}
class Tabs extends StatefulWidget {
Tabs({Key? key}) : super(key: key);
@override
_TabsState createState() => _TabsState();
}
class _TabsState extends State<Tabs> {
// 定义私有变量,表示选中的选项序号
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Demo'),
),
body: Text("TabBar"),
bottomNavigationBar: BottomNavigationBar(
// 默认选中的选项(从 0 开始)
currentIndex: _currentIndex,
// 点击回调函数,参数为点击的选项序号
onTap: (int index) {
setState(() {
_currentIndex = index;
});
},
// 设置图标大小
// iconSize: 48.0,
// 选中的选项颜色
fixedColor: Colors.red,
// 底部导航栏选中动画样式,默认 fixed,可选 shifting
// type: BottomNavigationBarType.shifting,
// 底部的选项卡
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: "Home",
),
BottomNavigationBarItem(
icon: Icon(Icons.category),
label: "Category",
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: "Settings",
),
],
),
);
}
}
现在,我们可以通过点击导航栏的选项来改变导航栏中的图标效果。为了实现完整的功能,即点击选项跳转到相应的页面,我们还需要准备这些页面。
// lib/pages/tabs/Home.dart
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
HomePage({Key? key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Text("This is Home page.");
}
}
// Category 和 Settings 页面同理
// lib/pages/Tab.dart
import 'package:flutter/material.dart';
import './tabs/Category.dart';
import './tabs/Home.dart';
import './tabs/Settings.dart';
class Tabs extends StatefulWidget {
...
}
class _TabsState extends State<Tabs> {
int _currentIndex = 0;
// 存放相应的页面
final List _pageList = [
HomePage(),
CategoryPage(),
SettingsPage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Demo'),
),
// 实现切换页面的功能
body: _pageList[_currentIndex],
bottomNavigationBar: BottomNavigationBar(...
);
}
}
Navigator 路由控件
Flutter 通过 Navigator 控件以栈的形式管理路由并提供了相应的方法:Navigator.push
和 Navigator.pop
。
Flutter 提供了两种配置路由跳转的方式:基本路由和命名路由。
基本路由
实现界面的跳转和传值。
// 定义 Search 页面和 Form 页面
// lib/pages/Form.dart
import 'package:flutter/material.dart';
class FormPage extends StatelessWidget {
String content;
FormPage({this.content = "default content"});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("From Page"),
),
body: Text(content),
// 除了自带的返回按钮,我们也可以自己定义一个按钮实现相同的功能
floatingActionButton: FloatingActionButton(
child: Text("Back"),
onPressed: () {
// 路由跳转,出栈
Navigator.of(context).pop();
},
),
);
}
}
// lib/pages/tabs/Home.dart
import 'package:flutter/material.dart';
import '../Search.dart';
import '../Form.dart';
class HomePage extends StatefulWidget {
...
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
// 路由跳转 入栈
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SearchPage(),
),
);
},
child: Text("点击跳转到搜索页面"),
),
ElevatedButton(
onPressed: () {
// 路由跳转 入栈
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
FormPage(content: 'content provided by home page'),
),
);
},
child: Text("点击跳转到表单页面并传值"),
),
],
),
);
}
}
命名路由
首先在根控件的 routes 参数中以 Map 的数据类型定义命名路由。
通过 Navigator.pushNamed(context, '/routeName')
跳转。
// main.dart 配置命名路由
import 'package:flutter/material.dart';
import 'pages/Tabs.dart';
import 'pages/Form.dart';
import 'pages/Search.dart';
void main() {
runApp(MyAPP());
}
class MyAPP extends StatelessWidget {
// 定义路由变量
final routes = {
'/form': (context, {arguments}) => FormPage(arguments: arguments),
'/search': (context) => SearchPage(),
};
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Tabs(),
theme: ThemeData(
primarySwatch: Colors.yellow,
),
// routes: routes, 命名路由不传参时可以这样写
// 命名路由传参时
onGenerateRoute: (RouteSettings settings) {
// 统一处理
final String? name = settings.name;
final Function? pageContentBuilder = routes[name];
if (pageContentBuilder != null) {
if (settings.arguments != null) {
final Route route = MaterialPageRoute(
builder: (context) =>
pageContentBuilder(context, arguments: settings.arguments));
return route;
} else {
final Route route = MaterialPageRoute(
builder: (context) => pageContentBuilder(context));
return route;
}
}
},
);
}
}
// Home.dart 使用命名路由跳转
...
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
// 路由跳转 入栈
Navigator.pushNamed(context, '/search');
},
child: Text("点击跳转到搜索页面"),
),
ElevatedButton(
onPressed: () {
// 路由跳转 入栈
Navigator.pushNamed(context, '/form', arguments: {
"content": 'content provided by home page',
});
},
child: Text("点击跳转到表单页面并传值"),
),
],
),
);
}
}
// Form.dart 被跳转的路由
class FormPage extends StatelessWidget {
Map? arguments;
FormPage({this.arguments});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("From Page"),
),
body: Text(arguments != null ? arguments!["content"] : 'default content'),
floatingActionButton: FloatingActionButton(
child: Text("Back"),
onPressed: () {
// 自定义返回按钮
// 路由跳转,出栈
Navigator.of(context).pop();
},
),
);
}
}
分离路由代码
由于路由部分的代码较为复杂,我们可以将路由部分的代码分离成单独的文件:
// lib/routes/Route.dart
import 'package:flutter/material.dart';
import '../pages/Form.dart';
import '../pages/Search.dart';
import '../pages/Tabs.dart';
final routes = {
'/': (context) => Tabs(),
'/form': (context, {arguments}) => FormPage(arguments: arguments),
'/search': (context) => SearchPage(),
};
// 命名路由传参
Route? onGenerateRoute(RouteSettings settings) {
// 统一处理
final String? name = settings.name;
final Function? pageContentBuilder = routes[name];
if (pageContentBuilder != null) {
if (settings.arguments != null) {
final Route route = MaterialPageRoute(
builder: (context) =>
pageContentBuilder(context, arguments: settings.arguments)
);
return route;
} else {
final Route route = MaterialPageRoute(
builder: (context) => pageContentBuilder(context)
);
return route;
}
}
}
// lib/main.dart
import 'package:flutter/material.dart';
import 'routes/Routes.dart';
void main() {
runApp(MyAPP());
}
class MyAPP extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// home: Tabs(),
// 初始化时加载的路由
initialRoute: '/',
theme: ThemeData(
primarySwatch: Colors.yellow,
),
onGenerateRoute: onGenerateRoute,
);
}
}
路由替换
- 返回到上一级
Navigator.of(context).pop();
- 替换路由
替换是指将当前页面路由出栈,把替换的路由入栈。
Navigator.of(context).pushReplacementNamed('/Search');
- 返回到根路由
Navigator.of(context).pushAndRemoveUntil(
new MaterialPageRoute(
// 跳转到下标为 1 的导航栏选项页
builder: (context) => new Tabs(index: 1)
),
// 将路由栈置为空
(route) => route == null,
);
// tab 页面有改动,可以接收导航栏选项下标,这里省略
AppBar 控件
AppBar 控件用来修改页面顶部内容。
class AppBarDemoPage extends StatelessWidget {
const AppBarDemoPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("AppBarDemoPage"),
// 标题是否居中显示
centerTitle: true,
// 改变导航栏的背景颜色
backgroundColor: Colors.red,
// 修改左侧的按钮图标,默认为返回上一级
// leading: Icon(Icons.menu),
// 不同于图标控件,图标按钮控件有点击事件
leading: IconButton(
onPressed: () {
print("this is menu icon");
},
icon: Icon(Icons.menu),
),
// 在导航栏尾部放图标
actions: <Widget>[
IconButton(
onPressed: () {
print("this is search icon");
},
icon: Icon(Icons.search),
),
IconButton(
onPressed: () {
print("this is settings icon");
},
icon: Icon(Icons.settings),
),
],
),
body: Text("AppBar Demo"),
);
}
}
// 去掉 debug 标签,在 main.dart 中
class MyAPP extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// 去掉 debug 图标
debugShowCheckedModeBanner: false,
...
);
}
}
在 AppBar 中实现 Tabs 切换
class AppBarDemoPage extends StatelessWidget {
const AppBarDemoPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text("顶部导航栏 Demo"),
// TabBar 选项,在顶部 AppBar 的底部
bottom: TabBar(
tabs: <Widget>[
Tab(text: "热门"),
Tab(text: "推荐"),
],
),
),
// TabBar 内容
body: TabBarView(
children: <Widget>[
Text("这是热门内容"),
Text("这是推荐内容"),
],
),
),
);
}
}
这样写导航栏会同时生成一个导航栏标题和导航栏 Tabs 选项。如果是在已有 AppBar 的页面中,会与原来的导航栏标题一起显示,出现两个导航栏标题。
去掉冗余的标题
为了解决这一问题,我们可以删去内部导航栏的标题,把 TabBar 控件放在 title 选项的里面。
class _CategoryPageState extends State<CategoryPage> {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
// 背景颜色
backgroundColor: Colors.white,
title: TabBar(
// 指示器颜色
indicatorColor: Colors.blue,
// 是否可滑动,默认 false
isScrollable: true,
tabs: <Widget>[
Tab(text: "热门"),
Tab(text: "推荐"),
],
),
),
// TabBar 内容
body: TabBarView(
children: <Widget>[
Text("这是热门内容"),
Text("这是推荐内容"),
],
),
),
);
}
}
使用 TabController
使用 TabController 可以监听页面的变化并做出相应动作,可用于刷新页面等操作。
class TabBarControllerPage extends StatefulWidget {
...
}
// 混入 SingleTickerProviderStateMixin 类
class _TabBarControllerPageState extends State<TabBarControllerPage>
with SingleTickerProviderStateMixin {
// 定义 _tabController
late TabController _tabController;
// 实例化 TabController
@override
// 声明周期函数
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: 2);
// 监听页面变化
_tabController.addListener(() {
print(_tabController.index);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("TabController Demo"),
bottom: TabBar(
// 定义 controller 参数
controller: _tabController,
tabs: <Widget>[
Tab(text: "推荐"),
Tab(text: "热榜"),
],
),
),
body: TabBarView(
// 定义 controller 参数
controller: _tabController,
children: <Widget>[
Text("这是热门内容"),
Text("这是推荐内容"),
],
),
);
}
}
Drawer 侧边栏控件
Drawer 控件实现抽屉功能,可以拉出侧边栏,同时给 AppBar 增加相应按钮。
class _TabsState extends State<Tabs> {
int _currentIndex = 0;
// 存放相应的页面
final List _pageList = [...
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(...
// 实现切换页面的功能
body: _pageList[_currentIndex],
bottomNavigationBar: BottomNavigationBar(...
// 左侧侧边栏
drawer: Drawer(
child: Column(
children: <Widget>[
ListTile(
leading: CircleAvatar(
child: Icon(Icons.home),
),
title: Text("我的空间"),
),
// 添加分割线
Divider(),
ListTile(
leading: CircleAvatar(
child: Icon(Icons.people),
),
title: Text("用户中心"),
),
],
),
),
// 右侧侧边栏
endDrawer: Drawer(
child: Text('hello'),
),
);
}
}
DrawHeader
定义侧边栏头部:
class _TabsState extends State<Tabs> {
int _currentIndex = 0;
// 存放相应的页面
final List _pageList = [...
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(...
// 实现切换页面的功能
body: _pageList[_currentIndex],
bottomNavigationBar: BottomNavigationBar(...
// 左侧侧边栏
drawer: Drawer(
child: Column(
children: <Widget>[
Row(
children: [
Expanded(
// 定义侧边栏头部
child: DrawerHeader(
child: Text("Hello Flutter"),
decoration: BoxDecoration(
// 背景颜色
color: Colors.blue[300],
// 背景图片
image: DecorationImage(
image: NetworkImage(
"https://i1.hdslb.com/bfs/face/4e5d0a51273fe3f8fabc700b6a71bb8a38c9e21e.jpg@240w_240h_1c_1s.webp"),
fit: BoxFit.contain),
),
),
),
],
),
ListTile(...
],
),
),
);
}
}
UserAccoutsDrawerHeader
可以更方便地显示用户信息:
child: UserAccountsDrawerHeader(
accountName: Text("罗翔说刑法"),
accountEmail: Text("luoxiang@163.com"),
// 头像
currentAccountPicture: CircleAvatar(
backgroundImage: NetworkImage(
"https://i1.hdslb.com/bfs/face/4e5d0a51273fe3f8fabc700b6a71bb8a38c9e21e.jpg@240w_240h_1c_1s.webp",
),
),
// 背景
decoration: BoxDecoration(
color: Colors.white,
image: DecorationImage(
image: NetworkImage(
"https://i0.hdslb.com/bfs/space/cb1c3ef50e22b6096fde67febe863494caefebad.png@2560w_400h_100q_1o.webp",
),
fit: BoxFit.cover),
),
// 其他图片,被放在右上角
// otherAccountsPictures: <Widget>[],
),
侧边栏的路由跳转
...
ListTile(
leading: CircleAvatar(
child: Icon(Icons.home),
),
title: Text("我的空间"),
onTap: () {
// 返回时默认侧边栏是展开状态,打开侧边栏也是入栈,下一行代码弹出侧边栏状态,用于隐藏侧边栏
Navigator.of(context).pop();
Navigator.pushNamed(context, '/form');
},
),
...
按钮控件
ElevatedButton
下面介绍按钮控件,包括普通按钮、带图标的按钮、圆形按钮、圆角按钮:
class ButtonPage extends StatelessWidget {
const ButtonPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Button Demo"),
),
body: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// 在按钮外层加 Container 用来加宽度和高度,如果用 Expanded 包裹,则宽度与屏幕一致
Container(
height: 40,
width: 160,
child: ElevatedButton(
// 按下事件
onPressed: () {
print("ElevatedButton");
},
child: Text("ElevatedButton"),
style: ButtonStyle(
// 阴影效果
elevation: MaterialStateProperty.all(2),
// 按钮颜色
backgroundColor: MaterialStateProperty.all(Colors.orange),
// 字体颜色
foregroundColor: MaterialStateProperty.all(Colors.black),
),
),
),
// 带图标的按钮
ElevatedButton.icon(
onPressed: () {
print("ElevatedButton with Icon");
},
icon: Icon(Icons.search),
label: Text("ElevatedButton with Icon"),
)
],
),
Row(
children: <Widget>[
// 圆形按钮
ElevatedButton(
onPressed: () {
print("Circle Button");
},
child: Text("Circle Button"),
style: ButtonStyle(
shape: MaterialStateProperty.all(
CircleBorder(
side: BorderSide(
// 边框样式,默认 solid,若为 none 后两个参数不生效
style: BorderStyle.solid,
// 边框颜色
color: Colors.orange,
// 边框宽度
width: 1,
),
),
),
),
),
// 圆角按钮
ElevatedButton(
onPressed: () {
print("Round ButtonR");
},
child: Text("Round Button"),
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
// 还可以使用 StadiumBorder
// StadiumBorder(
// side: BorderSide(
// // 边框样式
// style: BorderStyle.none,
// ),
// ),
),
),
),
],
),
],
),
);
}
}
TextButton、OutlinedButton、IconButton、ButtonBar
简单介绍一下文本按钮、轮廓按钮、图标按钮、按钮组的使用方法:
Row(
children: <Widget>[
// 文本按钮
TextButton(
onPressed: () {
print("TextButton");
},
child: Text("TextButton"),
),
// 轮廓按钮
OutlinedButton(
onPressed: () {
print("OutlinedButton");
},
style: ButtonStyle(
// 配置边框
side: MaterialStateProperty.all(
BorderSide(
width: 1,
color: Colors.grey,
),
),
),
child: Text("OutlinedButton"),
),
// 图标按钮
IconButton(
onPressed: () {
print("IconButton");
},
icon: Icon(Icons.search),
),
],
),
Row(
children: <Widget>[
// 按钮组,可以放多个按钮
ButtonBar(
children: <Widget>[
ElevatedButton(
onPressed: () {},
child: Text("ButtonBar 1"),
),
ElevatedButton(
onPressed: () {},
child: Text("ButtonBar 2"),
),
],
)
],
)
自定义按钮控件
自定义一个简单的按钮控件:
// 自定义按钮控件
class MyButton extends StatelessWidget {
final String text;
final pressed;
const MyButton({this.text = "custom button", this.pressed, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: pressed,
child: Text(text),
);
}
}
FloatingActionButton 控件
class ButtonPage extends StatelessWidget {
const ButtonPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(...
// 浮动按钮
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add, color: Colors.black, size: 30),
onPressed: () {
print("floatingActionButton");
},
// 配置样式
backgroundColor: Colors.yellow,
),
// 浮动按钮位置
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
body: Column(...
);
}
}
与底部导航栏结合
浮动按钮常常与底部导航栏结合:
class _TabsState extends State<Tabs> {
int _currentIndex = 0;
// 存放相应的页面
final List _pageList = [...
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(...
// 外层加 Container 便于调整大小
floatingActionButton: Container(
height: 64,
width: 64,
padding: EdgeInsets.all(8),
// 调整按钮位置
margin: EdgeInsets.only(top: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32),
color: Colors.white,
),
// 浮动按钮
child: FloatingActionButton(
child: Icon(Icons.add, color: Colors.black, size: 30),
onPressed: () {
print("floatingActionButton");
// 跳转到相应页面
setState(() {
_currentIndex = 1;
});
},
elevation: 0,
// 配置样式,选中时改变颜色
backgroundColor:
_currentIndex == 1 ? Colors.yellow[600] : Colors.yellow,
),
),
...
);
}
}
表单
TextField
下面的代码可以提供默认值、获取输入框里的值:
class _TextFieldPageState extends State<TextFieldPage> {
// 初始化时给表单赋值时需要实例化,否则直接 var _username;
var _username = TextEditingController();
var _password;
// 赋初始值
void initState() {
super.initState();
_username.text = "罗翔";
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Demo"),
),
body: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
// 输入文本框
TextField(
decoration: InputDecoration(
// 类似 placeholder
hintText: "输入框的提示信息",
// 默认一条下划线,改变为边框
border: OutlineInputBorder(),
// 可以添加标签名效果
labelText: "用户名",
// 添加图标
// icon: Icon(Icons.people),
),
// 默认值
controller: _username,
// 显示的行数,默认为 1
// maxLines: 4,
// 值为 true 时改为密码框,此时行数须为 1
// obscureText: true,
// 这里自动双向绑定输入框的 value 和 _username.text,所以不需要 onChange
),
SizedBox(
height: 20,
),
TextField(
decoration: InputDecoration(
hintText: "输入密码",
border: OutlineInputBorder(),
labelText: "密码",
),
obscureText: true,
// 保存密码值
onChanged: (value) {
_password = value;
},
),
SizedBox(
height: 20,
),
Container(
// 与外层同宽
width: double.infinity,
child: ElevatedButton(
onPressed: () {
print(_username.text);
print(_password);
},
child: Text("登录")),
)
],
),
),
);
}
}
Checkbox 多选框
class _TextFieldPageState extends State<TextFieldPage> {
bool? flag = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Demo"),
),
body: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
Checkbox(
// 是否选中
value: flag,
// 点击后触发事件
onChanged: (value) {
setState(() {
flag = value;
});
},
),
],
),
),
);
}
}
CheckboxListTail
CheckboxListTile(
// 是否选中
value: flag,
// 点击后触发事件
onChanged: (value) {
setState(() {
flag = value;
});
},
title: Text('标题'),
subtitle: Text('二级标题'),
// secondary 用于添加单选框对面的内容,比如图标或者图片等
secondary: Icon(Icons.home),
),
Radio
class _TextFieldPageState extends State<TextFieldPage> {
Object? sex = 1;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Demo"),
),
body: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
// 单选按钮
Radio(
// 按钮的值
value: 1,
// 按钮组的值,如果按钮组的值等于按钮的值,则选中该项
groupValue: sex,
onChanged: (value) {
setState(() {
sex = value;
});
},
),
Text("男"),
Radio(
value: 2,
// 按钮组的序号
groupValue: sex,
onChanged: (value) {
setState(() {
sex = value;
});
},
),
Text("女"),
Row(
children: [
Text(sex.toString()),
Text(int.parse(sex.toString()) == 1 ? "男" : "女"),
],
)
],
),
),
);
}
}
RadioListTail
class _TextFieldPageState extends State<TextFieldPage> {
Object? sex = 1;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Demo"),
),
body: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
RadioListTile(
value: 1,
groupValue: sex,
onChanged: (value) {
setState(() {
sex = value;
});
},
// 相较于 Radio 新增
title: Text("标题"),
subtitle: Text("副标题"),
// secondary 用于添加单选框对面的内容,比如图标或者图片等
// 设置选中的高亮状态
// selected: true,
),
RadioListTile(
value: 2,
groupValue: sex,
onChanged: (value) {
setState(() {
sex = value;
});
},
title: Text("标题"),
subtitle: Text("副标题"),
),
],
),
),
);
}
}
Switch
class _TextFieldPageState extends State<TextFieldPage> {
bool flag = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Demo"),
),
body: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
Switch(
value: this.flag,
onChanged: (value) {
setState(() {
flag = value;
});
},
),
],
),
),
);
}
}
日期控件
时间和时间戳转换
- 时间换时间戳
var now = new DateTime.now();
print(now.millisecondsSinceEpoch);//单位毫秒,13 位时间戳
- 时间戳换时间
var now = new DateTime.now();
var a=now.millisecondsSinceEpoch; //时间戳
print(DateTime.fromMillisecondsSinceEpoch(a));
引入第三方库 pub.dev/packages/da… 改变日期输出格式
自带日期控件
class _DatePageState extends State<DatePage> {
var _now = DateTime.now(); //2021-12-07 22:46:48.717857
_showDatePicker() async {
// showDatePicker 类似于 js 的 promise 函数
var result = await showDatePicker(
context: context,
// 初始化日期
initialDate: _now,
// 开始日期
firstDate: DateTime(2010),
// 截止日期
lastDate: DateTime(2030),
);
print(result);
setState(() {
_now = result!;
});
}
@override
void initState() {
super.initState();
var a = _now.millisecondsSinceEpoch; //1638888408717
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Demo"),
),
body: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
// 有触发波纹效果的控件
InkWell(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("${formatDate(_now, [yyyy, "-", mm, "-", dd])}"),
Icon(Icons.arrow_drop_down),
],
),
onTap: _showDatePicker,
),
],
),
),
);
}
}
自带时间控件
class _DatePageState extends State<DatePage> {
TimeOfDay _nowTime = TimeOfDay(hour: 12, minute: 20);
_showTimePicker() async {
TimeOfDay? result = await showTimePicker(
context: context,
// 初始化时间
initialTime: _nowTime,
);
setState(() {
_nowTime = result!;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Demo"),
),
body: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
InkWell(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("${_nowTime.format(context)}"),
Icon(Icons.arrow_drop_down),
],
),
onTap: _showTimePicker,
),
],
),
),
);
}
}
设置语言为中文
- 配置依赖
flutter_localizations:
sdk: flutter
- 在 main.dart 中导包
import 'package:flutter_localizations/flutter_localizations.dart';
- 在 main.dart 中设置
class MyAPP extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
...
// 配置
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('zh', 'CH'),
const Locale('en', 'US'),
],
);
}
}
第三方日期控件库
flutter_datetime_picker | Flutter Package (pub.dev)
Dialog
AlertDialog
class _DialogPageState extends State<DialogPage> {
_alertDialog() async {
var result = await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("提示信息"),
content: Text("内容"),
actions: <Widget>[
TextButton(
onPressed: () {
// 按下按钮后关闭页面,并将 "cancel" 传递给 result
Navigator.pop(context, 'cancel');
},
child: Text("取消"),
),
TextButton(
onPressed: () {
Navigator.pop(context, 'confirm');
},
child: Text("确定"),
),
],
);
},
);
print(result);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Demo"),
),
body: Container(
child: Column(
children: [
ElevatedButton(
onPressed: _alertDialog,
child: Text("AlertDialog"),
)
],
),
),
);
}
}
SimpleDialog
class _DialogPageState extends State<DialogPage> {
_simpleDialog() async {
var result = await showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: Text("选择内容"),
children: [
SimpleDialogOption(
child: Text("选项 1"),
onPressed: () {
Navigator.pop(context, '选项 1');
},
),
SimpleDialogOption(
child: Text("选项 2"),
onPressed: () {
Navigator.pop(context, '选项 2');
},
),
],
);
},
);
print(result);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Demo"),
),
body: Container(
child: Column(
children: [
ElevatedButton(
onPressed: _simpleDialog,
child: Text("SimpleDialog"),
)
],
),
),
);
}
}
showModalBottomSheet
class _DialogPageState extends State<DialogPage> {
_showModalBottomSheet() async {
var result = await showModalBottomSheet(
context: context,
builder: (context) {
return Container(
// 设置高度
height: 200,
child: Column(
children: [
ListTile(
title: Text("点赞"),
onTap: () {
Navigator.pop(context, 'like');
},
),
ListTile(
title: Text("分享"),
onTap: () {
Navigator.pop(context, 'share');
},
),
ListTile(
title: Text("转发"),
onTap: () {
Navigator.pop(context, 'forward');
},
),
],
),
);
},
);
print(result);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Demo"),
),
body: Container(
child: Column(
children: [
ElevatedButton(
onPressed: _showModalBottomSheet,
child: Text("showModalBottomSheet"),
)
],
),
),
);
}
}
fluttertoast 第三方库
fluttertoast | Flutter Package (pub.dev)
自定义 Dialog
- 定义 MyDialog 控件
// lib/components/MyDialog.dart
import 'dart:async';
import 'package:flutter/material.dart';
class MyDialog extends Dialog {
String? title;
String? content;
MyDialog(this.title, this.content);
// 3 秒后关闭弹窗
_showTimer(context) {
var timer;
timer = Timer.periodic(
Duration(milliseconds: 1000),
// 定时器结束触发回调函数
(t) {
Navigator.pop(context);
t.cancel();
// 或者使用 timer.cancel() 取消定时器
// timer.cancel();
},
);
}
@override
Widget build(BuildContext context) {
_showTimer(context);
return Material(
// 设定为半透明
type: MaterialType.transparency,
child: Center(
child: Container(
height: 300,
width: 300,
color: Colors.white,
child: Column(
children: [
Padding(
padding: EdgeInsets.all(10),
child: Stack(
children: [
Align(
alignment: Alignment.center,
child: Text(title!),
),
Align(
alignment: Alignment.centerRight,
child: InkWell(
child: Icon(Icons.close),
onTap: () {
Navigator.pop(context);
},
),
)
],
),
),
Divider(),
Container(
padding: EdgeInsets.all(10),
width: double.infinity,
child: Text(
content!,
textAlign: TextAlign.left,
),
)
],
),
),
),
);
}
}
- 引入 MyDialog
import '../../components/MyDialog.dart';
- 调用
class _DialogPageState extends State<DialogPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Demo"),
),
body: Container(
child: Column(
children: [
ElevatedButton(
onPressed: () {
showDialog(
context: context,
builder: (context) {
return MyDialog("关于我们", "关于我们");
},
);
},
child: Text("自定义 Dialog"),
),
],
),
),
);
}
}
网络请求
JSON 字符串与 Map 转换
import 'dart:convert';
void main(List<String> args) {
Map mapData = {"name": "Jack", "age": "20"};
String strData = '{"name":"Jack","age":"20"}';
print(json.encode(mapData)); // Map 转 JSON {"name":"Jack","age":"20"}
print(json.decode(strData)); // JSON 转 Map {name: Jack, age: 20}
}
http 第三方库
根据说明文档配置和引入。
使用 get 和 post:
class _HomePageState extends State<HomePage> {
String _msg = "";
@override
void initState() {
super.initState();
}
_getData() async {
var apiUrl = Uri.parse('https://jd.itying.com/api/httpGet');
var response = await http.get(apiUrl);
print('Response status: ${response.statusCode}'); // Response status: 200
print('Response body: ${response.body}'); // 字符串
// Response body: {"msg":"这是Get请求返回的数据"}
setState(() {
_msg = json.decode(response.body)["msg"];
});
}
_postData() async {
var apiUrl = Uri.parse('https://jd.itying.com/api/httpPost');
var response = await http.post(
apiUrl,
body: {
"username": "Jack",
"age": "20",
},
);
print('Response status: ${response.statusCode}'); // Response status: 200
print('Response body: ${response.body}'); // 字符串
// Response body: {"msg":"post成功","body":{"username":"Jack","age":"20"}}
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: [
ElevatedButton(
onPressed: _getData,
child: Text("Get 请求"),
),
Text(_msg),
ElevatedButton(
onPressed: _postData,
child: Text("Post 请求"),
),
],
),
);
}
}
来个例子 🌰
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
class FoodPage extends StatefulWidget {
FoodPage({Key? key}) : super(key: key);
@override
_FoodPageState createState() => _FoodPageState();
}
class _FoodPageState extends State<FoodPage> {
List _list = [];
@override
void initState() {
super.initState();
_getData();
}
_getData() async {
var apiUrl =
Uri.parse('https://www.***.cn/resource/foods.json');
var response = await http.get(apiUrl);
// print('Response status: ${response.statusCode}');
// print('Response body: ${json.decode(utf8.decode(response.bodyBytes))}');
setState(() {
_list = json.decode(utf8.decode(response.bodyBytes))["data"];
});
}
List<Widget> _getColumn() {
List<Widget> tmpList = [];
for (int i = 0; i < _list.length; i++) {
tmpList.add(
Card(
elevation: 0.5,
child: Column(
children: [
Text(
_list[i]["title"],
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
),
Image.network(
_list[i]["image"],
height: 100,
width: double.infinity,
fit: BoxFit.contain,
),
SizedBox(height: 10),
Text(
_list[i]["content"]["introduction"],
textAlign: TextAlign.left,
),
SizedBox(height: 10),
Wrap(
spacing: 10,
alignment: WrapAlignment.start,
children: [
Text(
_list[i]["content"]["comments"][0],
style: TextStyle(color: Colors.red[200]),
),
Text(
_list[i]["content"]["comments"][1],
style: TextStyle(color: Colors.red[200]),
),
Text(
_list[i]["content"]["comments"][2],
style: TextStyle(color: Colors.red[200]),
),
],
)
],
),
),
);
}
return tmpList;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("诗与美食"),
backgroundColor: Colors.red[50],
foregroundColor: Colors.red[200],
elevation: 0.5,
),
body: _list.isNotEmpty
? SingleChildScrollView(
child: Column(
children: _getColumn(),
),
)
: Center(child: Text("加载中")),
);
}
}
自此,Flutter 的基础部分就学习完毕了。我们可以通过已学知识撸出简单的 APP 了!