flutter入门学习
本文纯属个人学习使用
Flutter 入门学习
创建项目
在目标目录下执行指令:
flutter create --platforms web <name>
运行项目

启动文件说明以及基础内容
runApp函数是Flutter内部的一个函数,启动一个Flutter就是从调用这个函数开始的
Widget表示控件,组件,部件的含义
Flutter默认的Material库内置设计规范,例如:颜色,文字排版,动画…
组件
MaterialApp
整个用户都是被MaterialApp所包裹的
常见属性:
- title:用于展示窗口的标签
- theme:用于设计应用主题
- home:用于展示窗口的主题内容
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(MaterialApp(
title: "Flutter组件体验", // 标签名称
theme: ThemeData(scaffoldBackgroundColor: Colors.blue), // 这里面设置了颜色
home: Scaffold(), // 一个主题
));
}
// 这段代码的效果是展示一个蓝色的屏幕
Scaffold
用于构建Material Design 风格页面的核心布局组件,提供标准、灵活配置的骨架
常见属性:
- appBar:页面顶部的应用栏,用于显示标题,导航按钮和操作菜单
- body:页面的主要内容,可以防止其他组件
- bottonNavigationBar:底部导航栏
- backgroundColor:设置整个Scaffold背景颜色
- floatingActionButton:悬浮操作按钮,用于触发页面的主要动作
实例代码:
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(MaterialApp(
title: "Flutter组件体验",
theme: ThemeData(scaffoldBackgroundColor: Colors.blue),
home: Scaffold(
appBar: AppBar(
centerTitle: (true),
title: Text("头部区域"),
),
body: Container(
child: Center(
child: Text("中部区域"),
),
),
bottomNavigationBar: Container(
height: 80.3, // 底部的高,不设置的话底部就占满了出了头部以外的部分
child: Center(
child: Text("底部区域"),
),
),
),
));
}
自定义组件
1.无状态组件
- 创建后状态不可变(只读)
- 外观由配置参数决定
- 只有一个类
- 继承StatelessWidget并实现build方法,build返回一个Widget方法,适合用于纯展示组件,没有用户交互操作
实例代码:
class MainPage extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: "test1",
home: Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("头"),
),
body: Container(
child: Center(
child: Text("中"),
),
),
bottomNavigationBar: Container(
height: 80.5,
child: Center(
child: Text("底"),
),
),
),
);
}
}
2.有状态组件
- 创建后状态可变,可以管理内部状态
- 两个关联的类:Widget和State
- 创建一个继承StatefulWidget的类,返回一个用于状态管理的类;再创建第二个类继承state<第一个类名>,用于管理可变的数据和业务逻辑,并且实现build方法
实例代码:
// 第一个类,对外
class MainPage extends StatefulWidget {
State<StatefulWidget> createState() {
// return 第二个类的对象
return _MainPageState();
}
}
// 第二个类,对内, 负责管理数据 处理业务逻辑 渲染视图
class _MainPageState extends State<MainPage> {
Widget build(BuildContext context) {
return MaterialApp(
title: "test1",
home: Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("头"),
),
body: Container(
child: Center(
child: Text("中"),
),
),
bottomNavigationBar: Container(
height: 80.5,
child: Center(
child: Text("底"),
),
),
),
);
}
}
组件的生命周期
无状态组件:组件被创建或父组件状态变化导致需要重新构建时,build方法会被调用
有状态组件:
- 创建阶段:StatefulWidget被创建 -> createState -> initState(只执行一次,State对象插入Widget树时立刻执行) -> didChangeDependencies(initState后立刻执行,当所依赖的InheritedWidget被调用时可能多次) -> build(构建UI方法,初始化、更新时调用)
- 更新阶段:父组件重建/配置变更 -> didUpdateWidget(父组件传入新配置时调用)
- 销毁阶段:组件被移除 -> deactivate(将State对象从树中暂时移除时调用) -> dispose(当State对象被永久移除时调用,释放资源)
暂时插入部分其他知识
点击事件
事件:用户和应用程序交互时出发的各种动作
GestureDetector(手势检测)
实例代码:
body: Container(
child: Center(
child: GestureDetector(
// 点击事件
onTap: () {
print("点击了中间");
},
onDoubleTap: () {
print("双击中部");
},
child : Text("中"),
)
),
),
// -------- 或者 -----------
body: Container(
child: Center(
child : TextButton(onPressed: () {
print("你按下了按钮");
}, child: Text("这是一个按钮"))
),
),
状态更新
setState
我的理解来看setState就是再次执行build
实例代码:
body : Center (
child: Row(
children: [
TextButton(
onPressed: (){
setState(() {
cnt --;
});
}, child: Text("减"),
),
Text(cnt.toString()),
TextButton(
onPressed: (){
setState(() {
cnt ++;
});
}, child: Text("加"),)
],)
)
其他知识部分结束,继续回到组件部分
Container
- 可以通过多种方式定理大小:明确宽高 > constraints约束 > 父组件约束 > 自适应组件大小
- 通过decoration属性实现视觉效果,但是与color属性互斥
- 提供内外边距和对齐方式
- 支持进行矩阵变化,如:旋转、倾斜、平移等
常见属性:
- 布局定位:alignment,控制child在容器内部的对齐方式
- 尺寸控制:width/height/constraints,设置容器宽度高度/为容器设置更复杂的尺寸约束(最大最小宽高)
- 间距留白:padding/margin,按照比例分配剩余空间,实现自适应布局
- 装饰效果:color/decoration,为容器设置一个简单的背景颜色/复杂的背景装饰
- 变换效果:transform,对容器及内容进行矩阵变换
- 子组件:child,容器内包含的唯一直接子组件
实例代码:
return MaterialApp(
home : Scaffold(
body: Container(
transform: Matrix4.rotationZ(0.05), //旋转的弧度
//其中,rotationX是3D形左右旋转,rotationY是3D形上下旋转,rotationZ是水平旋转
margin: EdgeInsets.all(20), // 外边距
alignment: Alignment.center, // 设置变换参照点
width: 200, // 宽
height: 200, // 长
// color: Colors.red, // 背景颜色
decoration: BoxDecoration(
color: Colors.red, // 背景颜色
borderRadius: BorderRadius.circular(15), // 圆角
border: Border.all(width: 3, color: Colors.green) // 边框
),
child: Text("hello!!", style: TextStyle(
color: Colors.white // 文本颜色
),
),
),
)
);
Center
将子组件和父容器在空间内进行水平和垂直方向上的居中排列
Center的大小取决于其父组件传递给它的约束,所以不能设置宽高
Align
- 作用:可以精确控制其子组件在父容器空间内的对其位置
- alignment(对齐方式):子组件在父容器内的对齐方式
- widthFactor(宽度因子):Align的宽度将是子组件宽度乘该因子
- heightFactor(高度因子):同上
可以理解为:Center是Align的一个特例,继承自Align,相当于一个将alignment属性为居中的Align.center
实例代码:
body: Container(
color: Colors.blue,
child :Align(
alignment: Alignment.center,
widthFactor: 2,
heightFactor: 2,
child : Icon(Icons.star, size: 150,color: Colors.yellow,),
),
)
Padding - 内边距组件
作用就是为其子组件添加内边距但是感觉好像没什么必要额外添加一个这个组件,因为好像可以直接用属性来设置
实例代码:
body: Container(
width: 200,
height: 200,
margin: EdgeInsets.all(20),
// padding : EdgeInsets.all(20); //其实这样就行了
decoration: BoxDecoration(
color: Colors.red,
border: Border.all(width: 5,color: Colors.yellow)
),
child: Padding(
child: Container(
// margin: EdgeInsets.all(0), 这样也行
width: 50,
height: 50,
color: Colors.blue,
),
// padding: EdgeInsets.only(left: 10, right: 20),
// padding: EdgeInsets.symmetric(horizontal: 50, vertical: 30),
padding: EdgeInsets.all(20)),
),
Column
属性:
- mainAxisAlignment:控制子组件在主轴(垂直方向)上的排列方式
- crossAxisAlignment:控制子组件在交叉轴(水平方向)上的对齐方式
- mainAxisSize:决定Column本身在垂直方向上的尺寸策略:是沾满所有可用空间(max),或者是紧紧包裹子组件内容(min)
- children:需要被垂直排列的子组件列表
Column组件不支持滚动,不支持设置宽高
实例代码:
body: Container(
width: double.infinity,
height: double.infinity,
// margin: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.red,
// border: Border.all(width: 5,color: Colors.yellow)
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 100,
height: 100,
color: Colors.amber,
),
Container(
width: 100,
height: 100,
color: Colors.amber,
),
Container(
width: 100,
height: 100,
color: Colors.amber,
),
Container(
width: 100,
height: 100,
color: Colors.amber,
)
],
),
),
Row
Row和Column基本一致,就是主轴和交叉轴反过来
Flex - 弹性布局
允许沿着一个主轴(水平或者垂直)排列其子组件,并且还可以灵活地控制子组件们在主轴上的尺寸比例和空间分配
属性:
- direction:主轴方向
- mainAxisAlignment:子组件在主轴上的对齐方式
- crossAxisAlignment:子组件在交叉轴上的对齐方式
- mainAxisSize:Flex容器自身在主轴上的尺寸策略
Expanded
是Flexible是子类
会自动填充所有可分配空间
等于
Flexible(
fit: FlexFit.tight,
...
)
大部分情况用Expanded,需要按照内容自适应大小用Flexible
实例代码:
// 比例为 2:1
child: Flex(
// direction: Axis.horizontal,
direction: Axis.vertical,
children: [
Expanded(
flex: 2,
child: Container(
height: 100,
width: 100,
color: Colors.yellow,
),
),
Expanded(
flex: 1,
child: Container(
height: 100,
width: 100,
color: Colors.green,
),
)
],
),
// 分三层
body: Container(
color: Colors.amber,
child: Flex(
direction: Axis.vertical,
children: [
Container(
color: Colors.blue,
height: 100,
),
Expanded(
child: Container(
color: Colors.blueGrey,
),
),
Container(
color: Colors.red,
height: 100,
)
],
),
),
Wrap - 流式布局
流式布局:当子组件在主轴方向上排列不下时,它会自动换行
Wrap组件像是Flex组件加上了换行特性
当子组件的内容时根据数据动态生成时,就要使用Wrap
常用属性:
- direction: 设置主轴方向
- spacing: 主轴方向上,子组件之间的间隔
- runSpacing: 交叉轴方向上,行(或列)之间的间隔
- alignment: 子组件在主轴方向上的对齐方式
- runAlignment: 交叉轴方向上的对齐方式
实例代码:
List<Widget> getList () {
List<Widget> L = List.generate(30, (index) {
return Container(
// margin: EdgeInsets.all(10),
color: Colors.blue,
width: 100,
height: 100,
);
});
return L;
}
...
child: Wrap(
alignment: WrapAlignment.center,
spacing: 10,
runSpacing: 10,
direction: Axis.horizontal,
children: getList(),
),
Stack/Positioned - 层叠布局组件
允许将多个子组件按照Z轴方向进行叠加排列
stack子组件:
- alignment: 控制非定位子组件的对齐方式,默认左上角
- fit: 控制非定位子组件如何适应Stack的尺寸
- clipBehavior: 控制子组件超出Stack边界时的裁剪方式
- children: 需要被层叠排列的子组件列表
Positioned用于对子组件进行精确定位控制,并且必须作为Stack的直接子组件
Positioned通过left,right,top,bottom将子组件“钉”在Stack的某个地方
stack基本用法示例代码:
body: Container(
color: Colors.amber,
width: double.infinity,
height: double.infinity,
child: Stack(
children: [
Container(
width: 300,
height: 300,
color: Colors.blue,
),
Container(
width: 200,
height: 200,
color: Colors.red,
)
],
alignment:Alignment.bottomCenter
),
),
Positioned实例代码:
如果Positioned中left和right同时有值,那么就覆盖width的赋值
body: Container(
color: Colors.amber,
width: double.infinity,
height: double.infinity,
child: Stack(
children: [
Container(
width: 500,
height: 500,
color: Colors.blue,
),
Positioned(
left: 10,
top: 10,
child: Container(
width: 300,
height: 300,
color: Colors.red,
)
),
Positioned(
right: 10,
bottom: 10,
child: Container(
width: 300,
height: 300,
color: Colors.grey,
)
)
],
// alignment:Alignment.bottomCenter
),
),
适用场景:
- 叠加效果:图像上的水印,文本
- 浮层交互:模态对话框,提示弹窗,操作菜单
- 悬浮按钮:按钮悬浮在特定内容之上
Text
用于显示文本
常用属性:
- data: 要显示的文本内容
- style: 文本样式
- textAlign: 文本在容器内的对齐方式(.left .center)
- maxLines: 文本显示的最大行数
示例代码:
// 基本组件
body: Container(
alignment: Alignment.center,
width: double.infinity,
height: double.infinity,
color: Colors.amberAccent,
child: Text(
"咕咕嘎嘎 咕咕嘎嘎 ...",
style: TextStyle(
fontSize: 40, // 大小
color: Colors.green, // 字体颜色
fontStyle: FontStyle.italic, // 斜体
fontWeight: FontWeight.w900, // 加粗
decoration: TextDecoration.underline, // 下划线
decorationColor: Colors.red // 下划线颜色
),
maxLines: 2, // 最大行数
overflow: TextOverflow.ellipsis, // 超出范围的表达方式(...)
),
),
// 同一文本中不同格式
child: Text.rich(TextSpan(
text: "nihao",
style: TextStyle(
color: Colors.red,
fontStyle: FontStyle.italic,
fontSize: 40
),
children: [
TextSpan(
text: "segai",
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
)
)
],
)
),
image
图片分类:
- image.asset(): 加载项目资源目录assets中的图片,需要在pubspec.yaml文件中声明资源路径
- image.network(): 直接从网络地址加载图片
- image.file(): 加载设备本地存储中的图片文件
- image.memory(): 加载内存中的图片数据
常用属性:
- width/height: 宽高
- fit: 控制图片如何适应其显示区域,例如:是否拉伸、裁剪、保持原比例
- alignment: 图片在其显示区域内的对齐方式
- repeat: 当图片小于显示区域时,设置是否以及如何重复平铺图片
首先先到pubspec.yaml文件中找到assets:
然后取消注释,把配置位置改为lib/images/
使用本地图片示例代码:
// 获取本地图片
child: Image.asset(
"lib/images/test1.png",
width: 100,
height: 100,
fit: BoxFit.contain,
),
// 获取网络上的图片
child: Image.network( "https://img.grtn.cn/material/mediaserver/img/2022/12/c95aefd37c07861a224e87ebf48a690c.jpg",
// width: 200,
// height: 200,
),
TextField
常用属性:
- controller: 文本编辑器控制器,或与获取、设置文档内容及监听变化
- decortation: 当时输入框的外观(标签,提示文字,图标,边框)
- style: 输入文本的样式
- maxLines: 最大行数
- onChanged: 输入内容发生变化时执行的回调函数
- onSubmitted: 用户提交输入时的回调函数
登陆界面示例代码:
class _MainPageState extends State<MainPage> {
// Controller定义
TextEditingController _nameController = TextEditingController();
TextEditingController _keyController = TextEditingController();
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("登录"),
centerTitle: true,
),
body: Container(
padding: EdgeInsets.all(20),
color: Colors.white,
child: Column(
children: [
TextField(
controller: _nameController,
onChanged: (value){ // 值改变的时候执行函数
print(value);
},
onSubmitted: (value){ // 值提交时执行函数(web上就是按下回车)
print(value);
},
decoration: InputDecoration(
// contentPadding: EdgeInsets.all(20), // 内容内边距
hintText: "please input your name", // 提示文本
fillColor: const Color.fromARGB(255, 246, 235, 202),
filled: true, // 是否填充
border: OutlineInputBorder(
borderSide: BorderSide.none, // 取消边框
borderRadius: BorderRadius.circular(25), // 圆角
)
),
),
SizedBox(height: 15),
TextField(
controller: _keyController,
obscureText: true, // 不显示输入内容,用“.”表示
decoration: InputDecoration(
hintText: "please input your keyward",
fillColor: const Color.fromARGB(255, 246, 235, 202),
filled: true,
border: OutlineInputBorder(
borderSide: BorderSide.none, // 取消边框
borderRadius: BorderRadius.circular(25), // 圆角
)
),
),
SizedBox(height: 40),
Container(
width: double.infinity,
height: 50,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(25)
),
child: TextButton(
onPressed: (){
print("你的名字是: ${_nameController.text}");
print("你的密码是: ${_keyController.text}");
},
child: Text(
"登录",
style: TextStyle(
color: Colors.white
),
)
)
),
],
),
),
)
);
}
}
SingleChildScrollView - 滚动组件
让单个子组件可以用滚动,所有内容一次性渲染,适合用于内容不固定但总量不多的页面
用法:包裹一个子组件,让单个子组件具备滚动能力
实例代码:
body: SingleChildScrollView(
child: Column(
children: getList(),
),
),
控制滚动:
controller: 给组件的controller绑定ScrollController对象
ScrollController _controller = ScrollController(); // 滚动条控制器
示例:
// 这是一个控制移动到最顶部的按钮:
SingleChildScrollView(
controller: _controller,
child: Column(
children: getList(),
),
),
Positioned(
top: 10,
right: 10,
child: GestureDetector(
onTap: (){
_controller.jumpTo(0);
},
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(100)
),
child: Center(
child: Text(
"到最顶部",
style: TextStyle(
fontSize: 20,
color: Colors.white
),
),
)
),
)
),
// 到最底部:
_controller.jumpTo(_controller.position.maxScrollExtent);
如果要有跳转动画而不是直接“闪现”的话,应该如此:
_controller.animateTo(_controller.position.maxScrollExtent, duration: Duration(seconds: 1), curve: Curves.bounceIn);
滚动方向:通过scrollDirection属性控制,默认为垂直方向(Axis.vertical),可以设置为水平方向:(Axis.horizontal)
特点:一次性构建所有子组件,如果嵌套的Column/Row中包含大量子项,可能会导致性能问题,这个时候就建议使用ListView
ListView - 滚动组件
先行列表,通过builder可以实现懒加载(按需渲染),只构建当前可见区域的列表项,适合用于聊天记录等单列滚动的数据列表
方式:提供多种构造函数,例如默认构造函数、ListView.builder\ListView.separated
ListView - builder模式
作用:处理长列表或动态数据
方式:接受一个itemBuilder回调函数来按需构建列表项,通过itemCount控制列表长度
示例代码:
body: ListView.builder(
itemCount: 200, // 列表长度
itemBuilder: (BuildContext context, int index) {
return getContainer(index);
},
),
ListView - separated模式
作用:在ListView.builder的基础上额外提供了构建分割线的能力,分割线就是子组件之间插入的空隙
方式:需要同时提供itemBuilder、separatorBuilder、itemCount三个属性
示例代码:
body: ListView.separated(
itemCount: 200,
separatorBuilder: (BuildContext context, int index) {
return Divider();
},
itemBuilder: (BuildContext context, int index) {
return getContainer(index);
},
),
GridView - 滚动组件
作用:用于创建二维可滚动网格布局,支持懒加载,可以固定列数,适合用于应用图标列表
构建方式:GridView.count、GridView.extent、GridView.builder、默认方式(写起来最繁琐)等
GridView.count是基于固定列数的网格布局
GridView.extent是基于固定子项的最大宽度/高度的网格布局
GridView.bulider用于网格项数量巨大或动态生成的情况,需要接受gridDelegate布局委托属性
gridDelegate:SliverGridDelegateWithFixedCrossAxisCount: 固定列数mainAxisSpacing主轴间隔
and
SliverGridDelegateWithMaxCrossAxisExtent: 最大宽度crossAxisSpacing 交叉轴间距
scrollDirection设置滚动防线横向/纵向(默认)
实例代码:
// gridView.count(固定交叉轴上的子项数量)
body: GridView.count(
scrollDirection: Axis.horizontal, // 设置主轴
padding: EdgeInsets.all(20),
mainAxisSpacing: 10, // 主轴间隔
crossAxisSpacing: 10, // 交叉轴的间隔
crossAxisCount: 3, // 交叉轴上的数量限制
children: getList(),
),
// gridView.extent(固定子项大小,子项数量会随着屏幕大小而变化)
body: GridView.extent(
maxCrossAxisExtent: 100, //
children: getList(),
)
// gridView.builder
body: GridView.builder(
itemCount: 100,
// gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4), // 选择count
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 100,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 2 // 宽高比
), // 选择extent
itemBuilder: (BuildContext context, int index) {
return getContainer(index);
}
)
CustomScrollView - 滚动组件
自定义滚动容器,用于组合多个可滚动组件,例如列表、网格,实现统一协调的滚动效果
Slive: Flutter中描述可滚动视图内部一部分内容的组件,它是滚动视图的“切片”
用法:通过slivers属性接收一个Sliver组件列表
Sliver组件对应关系:
- SliverList => ListView
- SliverGrid => GridView
- SliverAppBar => AppBar
- SliverPadding => padding
- SliverToBoxAdapter => ToBoxAdapter(用于包裹普通Widget)
- SliverPersistentHeader(粘性吸顶,就是这个组件不会因为往下滑而消失,而是会吸在顶部)
示例代码:
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Container(
color: Colors.blue,
height: 250,
alignment: Alignment.center,
child: Text("图片",style: TextStyle(color: Colors.white, fontSize: 50),),
),
),
SliverPersistentHeader(delegate: _StickyCategory(), pinned: true), // pinned 是否吸顶
SliverList.separated(itemCount: 100,itemBuilder: (BuildContext context, int index) {
return getContainer(index);
}, separatorBuilder: (BuildContext context, int index) {
return SizedBox(
height: 10,
);
} )
],
),
class _StickyCategory extends SliverPersistentHeaderDelegate {
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
// color: Colors.white,
margin: EdgeInsets.only(top: 10, bottom: 10),
// color: Colors.amber,
child: ListView.builder(
itemCount: 30,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 10),
width: 200,
color: Colors.green,
alignment: Alignment.center,
child: Text("图片${index + 1}",style: TextStyle(color: Colors.white, fontSize: 20)),
);
}),
);
}
double get maxExtent => 100; // 最大展开高度
double get minExtent => 60; // 最小折叠高度
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { // 是否需要重建
return false;
}
}
PageView - 滚动组件
用于实现分页滚动视图:整页滚动效果,支持横向和纵向,适合用于应用引导页,书记翻页
方式:提供多种构建方式,默认构造方式、PageView.builder等
示例代码:
child: PageView.builder(itemCount: 10, itemBuilder: (BuildContext context, int index) {
return getContainer(index);
}),
组件示例代码(类似电商平台的界面,屎山)
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(MainPage());
}
PageController _controller = PageController();
int _currentIndex = 0;
class MainPage extends StatefulWidget {
MainPage({Key? key}) : super(key: key);
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
List<Widget> getList() {
List<Widget> L = List.generate(100, (index){
return getContainer(index);
});
return L;
}
List<Widget> getPointList(int cnt) {
List<Widget> L = List.generate(cnt, (index){
return getPointContainer(index);
});
return L;
}
Widget getContainer (int index) {
return Container(
// margin: EdgeInsets.all(20),
alignment: Alignment.center,
color: Colors.blue,
width: double.infinity,
height: 100,
child: Text(
"这是一个序号为${index}的方块",
style: TextStyle(
fontSize: 15,
color: Colors.white
),
)
);
}
Widget getPointContainer (int index) {
return GestureDetector(
onTap: () {
setState(() {
_currentIndex = index;
});
_controller.animateToPage(index, duration: Duration(seconds: 1), curve: Curves.linear);
},
child: Container(
margin: EdgeInsets.all(5),
alignment: Alignment.center,
// width: double.infinity,
width: 10,
height: 10,
decoration: BoxDecoration(
color: ((index == _currentIndex)? Colors.red :Colors.white) ,
borderRadius: BorderRadius.circular(50)
),
),
);
}
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("示例"),
centerTitle: true,
),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Stack(
children: [
Container(
color: Colors.blue,
height: 250,
alignment: Alignment.center,
child: PageView.builder(controller: _controller, itemCount: 10, itemBuilder: (BuildContext context, int index) {
return getContainer(index);
}),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
height: 30,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: getPointList(10),
))
],
)
),
SliverPersistentHeader(delegate: _StickyCategory(), pinned: true),
SliverGrid.builder(itemCount: 100,gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 200, mainAxisSpacing: 10, crossAxisSpacing: 10), itemBuilder: (BuildContext context, int index){
return getContainer(index);
})
],
),
)
);
}
}
class _StickyCategory extends SliverPersistentHeaderDelegate {
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
// color: Colors.white,
margin: EdgeInsets.only(top: 10, bottom: 10),
// color: Colors.amber,
child: ListView.builder(
itemCount: 30,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 10),
width: 200,
color: Colors.green,
alignment: Alignment.center,
child: Text("图片${index + 1}",style: TextStyle(color: Colors.white, fontSize: 20)),
);
}),
);
}
double get maxExtent => 100; // 最大展开高度
double get minExtent => 60; // 最小折叠高度
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { // 是否需要重建
return false;
}
}
组件通信
| 通信方式 | 方向 | 适用场景 |
|---|---|---|
| 构造函数传递 | 父 => 子 | 简单的数据传递 |
| 回调函数 | 子 => 父 | 子组件通知父组件 |
| inheritedWidget | 祖先 => 后代 | 跨层级数据共享 |
| Provider | 任意组件间 | 状态管理推荐方案 |
| EventBus | 任意组件间 | 全局事件通信 |
| Bloc/Riverpod | 任意组件间 | 复杂状态管理 |
父传子
步骤:
- 子组件定义接收属性
- 子组件在构造函数中接收参数
- 父组件传递属性给子组件
- 有状态组件在’对外的类’接收属性,'对内的类’通过Widget对象获取对应属性
子组件定义接收属性必须使用final关键字,因为属性由父组件决定
无状态子组件传递示例代码:
child: Column(
children: [
Text("父组件", style: TextStyle(color: Colors.blue, fontSize: 20),),
Child(message: "1111",),
],
),
class Child extends StatelessWidget {
final String? message;
const Child({Key? key, this.message}) : super(key: key);
Widget build(BuildContext context) {
return Container(
child: Text("子组件 ${message}",style: TextStyle(color: Colors.red, fontSize: 18),),
);
}
}
// 感觉和直接定义一个函数来调用一模一样
有状态子组件传递示例代码:
class Child extends StatefulWidget {
final String message;
Child({Key? key, required this.message}) : super(key: key);
_ChildState createState() => _ChildState();
}
class _ChildState extends State<Child> {
Widget build(BuildContext context) {
return Container(
child: Text("子组件 ${widget.message}",style: TextStyle(color: Colors.red, fontSize: 18),),
);
}
}
子传父
步骤:
- 父组件传递一个函数给子组件
- 子组件调用这个函数
- 父组件通过回调函数获取参数
children: List.generate(list.length, (int index){
return Child(message: list[index], index: index,delFood: (int index){
list.removeAt(index);
print("${index}");
setState((){});
}
class Child extends StatefulWidget {
final String message;
final int index;
final Function(int index) delFood;
Child({Key? key, required this.message, required this.index, required this.delFood}) : super(key: key);
_ChildState createState() => _ChildState();
}
class _ChildState extends State<Child> {
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.topRight,
children: [
Container(
alignment: Alignment.center,
color: Colors.blue,
child: Text("${widget.message}",style: TextStyle(color: Colors.white, fontSize: 18),),
),
IconButton(
color: Colors.red, onPressed: (){
widget.delFood(widget.index);
}, icon: Icon(Icons.delete)),
],
);
}
}
网络请求
Dio插件使用
首先,在当前项目的终端里面输入:
flutter pub add dio
然后就可以在pubspec.yaml里面看到:
基本使用:Dio().get(地址).then().catchError()
示例代码:
import 'package:dio/dio.dart';
void main(List<String> args) {
Dio().get("https://geek.itheima.net/v1_0/channels").then((res){
print(res);
}).catchError(((error){}));
}
Dio的封装
拦截器类型:请求拦截器,相应拦截器,错误拦截器
拦截器参数hander的方法:next() 放过请求,reject() 拦截请求
http状态码 2xx 成功 ,3xx 缓存,4xx 请求参数问题 ,5xx 服务器异常
示例代码:
class DioUtils {
final Dio _dio = Dio();
DioUtils() {
_dio.options.baseUrl = "https://geek.itheima.net/v1_0/";
_dio.options.connectTimeout = Duration(seconds: 10);
_dio.options.sendTimeout = Duration(seconds: 10);
_dio.options.receiveTimeout = Duration(seconds: 10);
// 拦截器
_addInterceptor();
}
_addInterceptor () {
_dio.interceptors.add(InterceptorsWrapper(
// 请求拦截器
onRequest:(options, handler) {
handler.next(options);
},
// 相应拦截器
onResponse: (response, handler) {
if (response.statusCode! >= 200 && response.statusCode! < 300) {
handler.next(response);
return ;
}else {
handler.reject(DioException(requestOptions: response.requestOptions));
}
},
// 错误拦截器
onError: (error, handler) {
handler.reject(error);
},
));
}
get(String url, {Map<String, dynamic>? params}) {
return _dio.get(url, queryParameters: params);
}
}
语法拓展:
_dio.options.baseUrl = "https://geek.itheima.net/v1_0/";
_dio.options.connectTimeout = Duration(seconds: 10);
_dio.options.sendTimeout = Duration(seconds: 10);
_dio.options.receiveTimeout = Duration(seconds: 10);
// 等同于
_dio.options..baseUrl = "https://geek.itheima.net/v1_0/"
..connectTimeout = Duration(seconds: 10)
..sendTimeout = Duration(seconds: 10)
..receiveTimeout = Duration(seconds: 10);
基础语法补充
dynamic定义:
res = result as List
强制类型转换:
data.cast<Map<String, dynamic>>()
获取数据
示例代码:
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(MainPage());
}
class MainPage extends StatefulWidget {
MainPage({Key? key}) : super(key: key);
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
List<Map<String, dynamic>> _list = [];
void initState() {
super.initState();
_getChannels(); // 获取频道数据
}
void _getChannels() async {
DioUtils util = DioUtils();
Response<dynamic> result = await util.get("channels");
Map<String, dynamic> res = result.data as Map<String, dynamic>;
List data = res["data"]["channels"] as List;
_list = data.cast<Map<String, dynamic>>();
setState(() {});
}
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("示例"),
centerTitle: true,
),
body: Text("111"),
),
);
}
}
class DioUtils {
final Dio _dio = Dio();
DioUtils() {
_dio.options.baseUrl = "https://geek.itheima.net/v1_0/";
_dio.options.connectTimeout = Duration(seconds: 10);
_dio.options.sendTimeout = Duration(seconds: 10);
_dio.options.receiveTimeout = Duration(seconds: 10);
// 拦截器
_addInterceptor();
}
_addInterceptor () {
_dio.interceptors.add(InterceptorsWrapper(
// 请求拦截器
onRequest:(options, handler) {
handler.next(options);
},
// 相应拦截器
onResponse: (response, handler) {
if (response.statusCode! >= 200 && response.statusCode! < 300) {
handler.next(response);
return ;
}else {
handler.reject(DioException(requestOptions: response.requestOptions));
}
},
// 错误拦截器
onError: (error, handler) {
handler.reject(error);
},
));
}
Future<Response<dynamic>> get(String url, {Map<String, dynamic>? params}) {
return _dio.get(url, queryParameters: params);
}
}
跨域
在‘flutter/packages/flutter_tools/lib.src/web/chrome.dart’文件里大概这个位置:
加入’–disable-web-security’
然后在’flutter/bin/cache’目录下找到flutter_tools.snaphot和flutter_tools.stamp并删除
然后执行flutter doctor -v重新运行项目
网络通信以及之前的内容的综合示例代码
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(MainPage());
}
class MainPage extends StatefulWidget {
MainPage({Key? key}) : super(key: key);
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
List<Map<String, dynamic>> _list = [];
void initState() {
super.initState();
_getChannels(); // 获取频道数据
}
void _getChannels() async {
DioUtils util = DioUtils();
Response<dynamic> result = await util.get("channels");
Map<String, dynamic> res = result.data as Map<String, dynamic>;
List data = res["data"]["channels"] as List;
_list = data.cast<Map<String, dynamic>>();
// print(_list);
setState(() {});
}
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("示例"),
centerTitle: true,
),
body: GridView.builder(
itemCount: _list.length,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 100,
mainAxisSpacing: 10,
crossAxisSpacing: 10),
itemBuilder: (BuildContext context, int index){
return Child(index: index, message: _list[index]['name'], delItem: (index){
_list.removeAt(index);
setState(() {});
},);
}
),
),
);
}
}
class DioUtils {
final Dio _dio = Dio();
DioUtils() {
_dio.options.baseUrl = "https://geek.itheima.net/v1_0/";
_dio.options.connectTimeout = Duration(seconds: 10);
_dio.options.sendTimeout = Duration(seconds: 10);
_dio.options.receiveTimeout = Duration(seconds: 10);
// 拦截器
_addInterceptor();
}
_addInterceptor () {
_dio.interceptors.add(InterceptorsWrapper(
// 请求拦截器
onRequest:(options, handler) {
handler.next(options);
},
// 相应拦截器
onResponse: (response, handler) {
if (response.statusCode! >= 200 && response.statusCode! < 300) {
handler.next(response);
return ;
}else {
handler.reject(DioException(requestOptions: response.requestOptions));
}
},
// 错误拦截器
onError: (error, handler) {
handler.reject(error);
},
));
}
Future<Response<dynamic>> get(String url, {Map<String, dynamic>? params}) {
return _dio.get(url, queryParameters: params);
}
}
class Child extends StatefulWidget {
final String message;
final Function(int index) delItem;
final int index;
Child({Key? key, required this.message, required this.delItem, required this.index}) : super(key: key);
_ChildState createState() => _ChildState();
}
class _ChildState extends State<Child> {
Widget build(BuildContext context) {
return Stack(
children: [
Container(
alignment: Alignment.center,
color: Colors.blue,
child: Text(
widget.message,
style: TextStyle(
fontSize: 20,
color: Colors.white
),
),
),
Positioned(
right: 0,
top: 0,
child: IconButton(
color: Colors.red,
onPressed: (){
widget.delItem(widget.index);},
icon: Icon(Icons.delete)
)
)
],
);
}
}
路由管理
路由管理是构建多页面应用的核心,它通过Navigator和Route来管理页面栈,实现页面跳转和返回
基本路由
场景:适合页面不多,跳转逻辑简单的场景
用法:无需提前注册路由,跳转时创建MaterialPageRoute即可
方法:
- 跳转新页面:Navigator.push(BuildContext context, Route route)
- 返回上一页:Navigator.pop(BuildContext context)
基本路由实例代码:
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(MainPage());
}
class MainPage extends StatelessWidget {
const MainPage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
home: ListPage()
);
}
}
class ListPage extends StatefulWidget {
ListPage({Key? key}) : super(key: key);
_ListPageState createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("列表"),
centerTitle: true,
),
body: ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index){
return TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage()));
},
child: Container(
padding: EdgeInsets.all(10),
alignment: Alignment.center,
width: double.infinity,
height: 150,
color: const Color.fromARGB(255, 26, 165, 203),
margin: EdgeInsets.only(top: 10),
child: Text(
"第${index + 1}个详情页",
style: TextStyle(
color: Colors.black,
fontSize: 20,
),
),
));
}),
);
}
}
class DetailPage extends StatefulWidget {
DetailPage({Key? key}) : super(key: key);
_DetailPageState createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("详情页"),
),
body: Center(
child: TextButton(onPressed: (){
Navigator.pop(context);
}, child: Text("返回上一个页面",style: TextStyle(fontSize: 20),)),
),
);
}
}
命名路由
场景:引用页面增多后,使用命名路由提升代码可维护性
用法:需要先在MaterialApp中注册一个路由表(routes),并设置initialRoute(首页)
其实就是注册一个路由表,然后在后续部分使用Navigator.pushNamed进行跳转
示例代码:
// 注册路由表
return MaterialApp(
title: "示例",
initialRoute: "/list",
routes: {
"/list": (context) => ListPage(),
"/detail": (context) => DetailPage()
},
// 不需要home
);
// 跳转
onPressed: () {
Navigator.pushNamed(context, "/detail");
},
跳转方法
| 方法 | 核心作用 | 典型场景 | 栈示例 |
|---|---|---|---|
| pushNamed | 进入新页面 | 常规页面跳转 | [A,B] -> [A,B,C] |
| pushReplacementNamed | 替换当前页面 | 登陆成功后跳转主页,并且无法返回登录页面 | [A,B] -> [A,C] |
| pushNamedAndRemoveUntil | 跳转新页面并清理栈 | 退出登陆后跳转登录页,并清空所有历史页面 | [A,B,C,D] -> [A,E] |
| popAndPushNamed | 返回并立刻跳转新页面 | 购物车页面结算后,返回商品列表并同时跳转到订单页 | [A,B,C] -> [A.B,D] |
| popUntil | 持续返回直到条件满足 | 从设置也得深层级,一件返回到主设置页面 | [A,B,C,D] -> [A,B] |
传递参数
命名路由传递参数
传递:Navigator.pushNamed(context, 地址, arguments: {参数})
接收:ModalRoute.of(context)?.settings.arguments
接收时机:initState获取不到路由参数,放置在Future.microtask(异步微任务)中
示例代码:
// 发送的时候是传递参数
Navigator.pushNamed(
context, "/detail",
arguments: {
"id": index + 1
});
// 接收的时候要在这里接收
void initState() {
super.initState();
Future.microtask((){
if(ModalRoute.of(context) != null) {
Map<String, dynamic> params =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
_id = params['id'].toString();
setState(() {});
}
});
}
// 然后就可以在后续部分使用了
基础路由传递参数
传递参数:通过组件构造函数传递参数 -(父传子)
接收参数:通过组件构造函数接收参数
接收时机:initState可获取到基础路由的构造函数传参
基础路由传递参数其实和父传子一模一样
onPressed里面是这样:
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(id: (index + 1).toString(),)));
},
底下的子组件是这样:
class DetailPage extends StatefulWidget {
final String id;
DetailPage({Key? key, required this.id}) : super(key: key);
_DetailPageState createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("详情页${widget.id}"),
),
body: Center(
child: TextButton(onPressed: (){
Navigator.pop(context);
}, child: Text("返回上一个页面",style: TextStyle(fontSize: 20),)),
),
);
}
}
动态路由与高级控制
场景:更复杂的场景,如需根据参数动态生成页面,或实现路由拦截,可以使用onGenerateRoute和onUnknownRoute
如果A页面要跳转到B页面,但是B页面没有在routes中,那么就会跳转到onGenerateRoute中去执行里面的代码
示例代码(onGenerateRoute):
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main(List<String> args) {
runApp(MainPage());
}
class MainPage extends StatefulWidget {
MainPage({Key? key}) : super(key: key);
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: "/goodsList",
routes: {
"/goodsList": (context) => GoodsList(),
},
onGenerateRoute: (settings) {
if(settings.name == "/carList") {
bool isLogin = false;
if(isLogin) {
return MaterialPageRoute(builder: (context) => CarList());
}else {
return MaterialPageRoute(builder: (context) => LoginPage());
}
}
},
);
}
}
// 商品列表
class GoodsList extends StatefulWidget {
GoodsList({Key? key}) : super(key: key);
_GoodsListState createState() => _GoodsListState();
}
class _GoodsListState extends State<GoodsList> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("商品列表"),
centerTitle: true,
),
body: Center(
child: TextButton(onPressed: (){
Navigator.pushNamed(context, "/carList");
}, child: Text("加入购物车")),
),
);
}
}
// 购物车
class CarList extends StatefulWidget {
CarList({Key? key}) : super(key: key);
_CarListState createState() => _CarListState();
}
class _CarListState extends State<CarList> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("购物车列表"),
centerTitle: true,
),
body: Center(
child: TextButton(onPressed: (){}, child: Text("去支付")),
),
);
}
}
// 登录
class LoginPage extends StatefulWidget {
LoginPage({Key? key}) : super(key: key);
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("登录界面"),
centerTitle: true,
),
body: Center(
child: TextButton(onPressed: (){}, child: Text("登录")),
),
);
}
}
onUnknowRoute:跳转一个未在路由表中注册,也未在onGenerateRoute中处理的路由,会调用此路由,通常显示”404“页面
onUnknownRoute: (settings) {
return MaterialPageRoute(builder: (context) => NotFound());
},
class _NotFoundState extends State<NotFound> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("错误"),
centerTitle: true,
),
body: Text("404!!!!!!!"),
);
}
}
更多推荐



所有评论(0)