flutter基础知识(随时更新ing)
flutter构建页面
核心方式:父组件通过特定属性,内嵌小组件
知道这点以后,就可以看懂下面的代码了
Scaffold(//父组件
floatingActionButton://Scaffold的属性
FloatingActionButton(//floatingAcionButton的子组件
child://F~的属性
Icon(//child的组件
Icons.add//Icons是类
)
)
)
Drawer:左侧菜单栏
基本结构
drawer:Drawer(/侧边栏
child:ListView(//装菜单项
children:[
DrawerHeader(//头部
decoration: BoxDecoration(color:Colors.black),
child: Text("凯哥商店", style: TextStyle(color: Colors.white)),
),
ListTile(
leading:Icon(Icons.home),//加左侧图标
title: Text("首页")),//一个菜单选项
ListTile(title: Text("商家信息")),
ListTile(title: Text("退出登录")),
],
),
),
要是只写这个,你会发现在顶部菜单栏的左边并没有出现侧边栏,原因是:AppBar没有加菜单按钮,用户无法打开侧边栏
正确写法:在appBar里面加上菜单按钮
leading: IconButton(
icon: const Icon(Icons.menu, color: Colors.white),
onPressed: () {
Scaffold.of(context).openDrawer(); // 找到当前页面,打开侧边栏
},
),
但是有时候你会发现写了这个以后仅仅是在导航栏有侧边栏了,但是点击还是不能进入侧边栏的页面,原因之一可能是context的位置
如果你写的context在scaffold的外面,那context就没法找到页面的位置,因此我们需要用builder,builder可以等scaffold的页面创建好,返回一个新的context
Scaffold(
appBar: AppBar(
leading: Builder(
builder: (新context) {
// 这个 新context 是:房子造好以后,才生成的!
// 它属于 Scaffold 内部
return IconButton(...);
},
),
),
)
有状态组件和无状态组件
- 有状态组件
外壳(固定不变)+ 内核(能交互,能变)
class LoginPage extends StatefulWidget{//外壳
//定义一个登入页面,并且告诉flutter这个页面的可交互的
State<StatefulWidget> createState()
//固定写法,创建一个内核给这个外壳用
{
return _LoginPageState();
//告诉flutter,这个内核是_LoginPageState
{
}
//箭头写法 State<LoginPage> createState() => _LoginPageState();
class _LoginPageState extends State<LoginPage>{//内核
//告诉flutter当前这个内核是专门给LoginPage使用的
Widget build(BuildContext context) {
//用build画页面,context表示上下文
return Scaffold(
// 返回一个页面模板
//自带了顶部导航栏(appBar)、中间主内容区(body)、底部导航栏(bottomNavigationBar)、悬浮按钮(floatingActionButton) 这些常用区域
);
}
}
提示框
- AlertDialog
对话框
//触发方法
// 这三行代码本质完全一样,只是变量名不同
BuildContext context; // 通用名
BuildContext imageContext; // 语义化:和图片相关的上下文
BuildContext dialogContext; // 语义化:和对话框相关的上下文
void show(BuildContext context){//外层context:定位弹窗在哪个页面
showDialog(
context:context,//定位到当前页面
builder:(BuildContext context){//内层context:对话框的地址
return AlertDialog(
title://标题
content://内容
actions:[
TextButton(
onpress:(){
return Navigator.pop(context)//返回到上一页的地址
},
child://按钮上的内容
)
)
}
)
}
- SnackBar
底部提示条
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration://持续时间
content://内容
//可选:按钮
action:SnackBarAction()
)
)
点击事件
- GestureDetect
给任意组件加手势(点击事件)
用法:用它包裹被点击的元素,加上onTap等
GestureDetect的可点击范围有子组件决定,且遵循视觉可见性优先,如果子组件有Text,Image等,就默认能接受点击,但是当子组件完全透明无内容,GestureDetect就无法接收点击,进而穿透,触发下层第一个可见有内容的事件
behavior: HitTestBehavior.translucent
//作用:强制透明遮盖可点击
-
按钮组件:TextButton,OutlineButton,–FloatingActionButton(右下角悬浮按钮),ElevaButton
通过onPressed处理 -
视觉反馈:InkWell(onTap)
-
其他:IconButton等
点击后进入新页面的写法
ElevatedButton(
style:ElevatedButton.styleFrom(
backgroundColor: Colors.indigoAccent, // 按钮底色
foregroundColor: Colors.white, // 文字颜色
minimumSize: Size(120, 40), // 长方形:宽120,高40(可自己调)
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero, // 核心:圆角设为0,完全无圆角
),
),
),//修改button的样式
onPress:()
{
Navigator.push(//push是去新页面,pop是回上一页
context,//context表示当前页面的地址
MaterialPageRoute(//M~是跳转的动画规则(从右往左)
builder:(context)=>
//builder是构建器,告诉跳转到哪个页面
const NewPage()),//=>是return的简写,返回要跳转的页面
);
},
)
状态更新
- 数据变化驱动UI更新:setState
setState只能在StatefulWight里面使用
//onTap是当你点击时,(index)是会把点的是第几个告诉我们:flutter会根据按钮顺序自动分配序号,所以点击时index是可知的
onTap:(index){
setState((){
_currentIndex = index;
});
//setState((){})的作用是更新页面
}
如果有较少的页面切换,可以使用三元运算符
//格式
body:index == 0
? mappage()
: index == 1
? shoupage()
: homepage;
补充:三元运算符知识点
条件? 满足执行:不满足执行
布局组件
- 基础容器:
Container:
通过decoration实现视觉效果(和color互斥)
属性:alignment,width,height,padding,margin(外边距),color/decoration,transform(旋转位移),child
Center:
居中组件
Center不能设置宽高,如果要实现固定宽高且居中的组件,让Center去包裹Container/SizeBox组件
Align:
对齐组件
属性:alignment,widthFactor,heightFactor
Padding:
内边距组件
属性:child,padding(类型:Edgeinsets.all四个方向设置相同间距,~.symmetric设置对称方向)
EdgeInsets.all()//四个方向相同内边距
EdgeInsets.fromLTRB()//分别跟左上右下四个方向设置不同边距
EdgeInsets.only({})//只给指定方向设置
EdgeInsets.symmetric({vertical,horizontal})//对称设置,vertical上下,horizontal左右
- 线性布局
Column:
垂直排列
属性:mainAxisAlignment(主轴方向:上下方向),crossAxisAlignment(交叉轴:左右方向),children
crossAxisAlignment:CrossAxisAlignment.start
//cross开头的长单词作用是:控制元素怎么对齐
//.start是靠左的意思 .center居中 .end靠右
//mainAxisAlignment里面的spaceBetween表示在两边分开
Row:
水平排列
属性:同Column
在AppBar里面的Row默认会占满父组件的的全部水平宽度(也就是整个AppBar宽度),这个时候想要Row里面的内容居中,在AppBar里面加centerTitle没用,因为centerTitle是对Row起作用,管不了Row内部的图标和文字,首页内部的图标和文字会按照Row的对齐原则(靠左边)
//正确解决方法:加MainAxisSize.min
AppBar(
centerTitle: true, // 指令:把标题整体放在AppBar正中间
title: Row(
mainAxisSize: MainAxisSize.min, // 关键:让Row仅包裹图标+文字,形成“紧凑整体”
children: [Image(图标), Text(文字)],
),
)
//mainAxisSize专门控制Column/Row在主轴方向的尺寸
MainAxisSize.max//主轴方向撑满整个屏幕
MainAxisSize.min//主轴方向只占最小空间(包裹内容)
//Spacer()弹性占位符:唯一前提就是只有Column和Row设置了mainAxisSize:MainAxisSize.max才能使用
- 弹性布局
Flex:
允许沿着(垂直或者水平)方向,排列子组件,结合Column和Row
Expanded(强制占满所有剩余空间)和Flexible作为Flex的子组件通过Flex属性来fenp空间 - 流式布局
Wrap:
自动换行
(Column/Row/Flex均不会换行)
属性:direction(Axis.horizontal水平 Axis.vertical垂直),children,alignment,spacing,runspacing(间距) - 层叠布局
Stack/Positioned:
属性:alignment,children, fit
Positioned作为Stackd的黄金搭档,进行精确定位(通过left,right,top,bottom)
基础组件
- Text:
文本组件
属性:date,style,textAlign,maxLines(最大行数)
需要在同一文本显示不同样式,用Text.rich构造函数配合TextSpan来实现 - Image:
图片组件
属性:width,height,fit,alignment - TextField:
文本输入组件
属性:controller,decortation(类型:InputDecoration),style,maxLines,onChanged,onSubmitted,obscureText(用于密码)
controller控制器的用法:
- 页面初始化的时候(build之前)创建监听器
- 绑定监听器
- 判断监听器获取到的内容,并弹出底部弹框
//1.创建控制器:账号输入框专属
class _LoginPageState extends State<LoginPage> {
final usernameCtrl = TextEditingController();
// 密码输入框专属(和你的代码完全一致)
final passwordCtrl = TextEditingController();
Widget build(BuildContext context) {
// ... 后续代码
}
}
// 2. 绑定控制器:账号输入框(你的代码原版)
TextField(
controller: usernameCtrl, // 绑定专属控制器
hintText: "请输入邮箱",
// ... 其他样式属性
),
// 密码输入框绑定(你的代码原版,逻辑完全一致)
TextField(
controller: passwordCtrl,
obscureText: true, // 密码密文显示
hintText: "请输入密码",
// ... 其他样式属性
),
//3.判断监听器获取到的内容,并弹出底部弹框
onTap: () {
// 取值:账号输入框内容(去空格)
String account = usernameCtrl.text.trim();
// 取值:密码输入框内容(去空格)
String pwd = passwordCtrl.text.trim();
// 后续空值判断、保存全局变量,全靠这两个取到的值(你的核心业务)
if (account.isEmpty) { /* 弹请输入账号提示 */ }
if (pwd.isEmpty) { /* 弹请输入密码提示 */ }
},
弹窗改数据
- 1个输入框一个TextEditingController
- 在弹窗的TextField里面,给TextEditingController绑定输入框
- 用setState改变原始数据
// 弹窗方法
void showEditDialog() {
// 第一步
TextEditingController XXXCtrl1 = TextEditingController(text: 原数据变量1);
TextEditingController XXXCtrl2 = TextEditingController(text: 原数据变量2);
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("修改XXX信息"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 第二步:绑定输入框
TextField(controller: XXXCtrl1, hintText: "请输入XXX1"),
SizedBox(height: 10),
TextField(controller: XXXCtrl2, hintText: "请输入XXX2"),
],
),
}
固定弹窗写法
showDialog(
context:context,
builder:(context){
return AlertDialog(
title:顶部标题,
content:中间内容,
action:[底部按钮]。
);
},
);
弹窗里面的组件要变,必须用StatefulBuilder+setStateDialog
statefulBuilder
是弹窗专用的状态管理器
setStateDialog
是弹窗内部专用的setState
showDialog(
builder:(context){
return StatefulBuilder(
builder:(context,setStateDialog){
bool isEnabled = isItEnabled;
return AlertDialog(
Switch(
value:isEnable,
onChanged:(value){
setStateDialog((){
isEnabled = value;
});
}
)
- 滚动组件
SingleChildScrollView(单个):
控制滚动的方法是给组件的controller绑定ScrollController对象
ListView(可滚动列表组件):
(1) ListView.builder处理长列表或动态数据,通过itemCount控制列表长度,介绍一个itemBuilder回调函数来按需构建列表
ListView.builder的用法
ListView.builder(
//滑动方向的设置,horizontal横向,Vertical纵向
scrollDirection:Axis.horizontal,
//优化:滑到边缘有弹性
physics:BouncingScrollPhysics(),
//让ListView的高度和宽度仅包裹住它的内容,而不是占满父组件的,所以空间
shrinkWrap:true,
//声明数量
itemCount:15,
//构建列表的子项
itemBuilder:(context,index){
return ListTile(
title:Text(""),
subtitle:Text(""),
onTap:(){
Navigate.push(
context,
MaterialPageRouter()
builder:(context)=>...,
),
};
)
}
)
ListView中子组件必须按顺序排列,后一个子组件不能通过负边距覆盖前一个子组件的位置
but我们可以通过Transform.rotate来做视觉偏移
Transform.rotate(
angle:
)
(2)ListView.separated:
在上面的基础上,提供了构建分割线的能力。提供itemBuilder,separatorBuilder,itemCount三个属性
//ListView基本用法
ListView(
padding//内边距EdgeInsets.all(0)常用
children:[//放置列表项
ListTile(title:Text(“ ”)),
...
]
)
GridView(构建二维可滚动的网格布局):
(1)GridView.count基于固定列数
(2)GridView.extent基于固定子项最大宽度
CustomScrollView(组合多个可滚动组件)
PageView(分页滚动):
(1)PageView.builder
- Transform
几何变换组件(平移/旋转/缩放/斜切)
(1)Transform.scale
实现按比例缩放
(2)Transform.rotate
实现按角度旋转
(3)Transform.translate
实现平移
结合offset(dx,dy)
Transform.translate和Positioned不同在于,前者是可平移出父容器的,不影响任何周围组件的布局的,是视觉上的平移,而后者是布局定位
路由管理
它是构建多页面的核心,通过Navigator和Route管理页面栈,实现页面的跳转和返回。
Navigator–>push:新Route入栈到栈顶
–> pop:当前Route出栈
–>pushReplacement:新页面入栈到栈顶,同时移除并销毁当前页面
Navigator无法直接跳转到目标页面,目标页面是Widget类型的,而Navigator规定了只能处理路由对象(Route类型),所以我们需要用MaterialPageRoute这个标准封装器,把目标页面打包成路由对象,进而实现页面跳转
builder是构建器,它的返回值就是我们要跳转的页面
Navigator.push(
context,//当前页面的上下文
路由对象
//如MaterialPageRoute(builder: (context) => HomePage())
);
Navigator.pop(context);//上下文
Navigator.pushReplacement(
context, // 必传:当前页面(塔罗牌页)的上下文,作用同push
MaterialPageRoute( // 必传:路由对象,封装目标页面+跳转规则
builder: (context) => HomePage() // 必传:目标页面,同push
)
);
(1)基本路由
(2)命名路由
需要在MaterialApp中注册一个路由表(routes)并设置initialRoute(首页)

代码的意思是当路由遇到/list的时候,就会自动跳转到ListPage

传递参数(命名参数)
//传递参数
Navigator.pushNamed(context,地址,arguments:{参数})
//接受参数
ModalRoute.of(context)?.settings.arguments
//介绍时机 放置在 Future.microtask中
动画
分类:隐式/显示/自定义隐式/自定义显示/Hero动画
flag是动画控制的开关,通过切换属性值,触发动画
//例如:
bool flag = false;
...
ElevatedBotton(
onpressd:() {
setState(() {
flag = !flag;
});
},
)
....
width = false ? 100 :50
- 隐式动画:
通过几行代码就能实现,背后实现原理和繁琐的操作细节被隐去
(1) AnimatedContainer
AnimatedContainer(
duration://必须写
height/width:
color:
transform:
//重点!通过Matrix4.translationValues(x,y,z)让容器移动,x方向+右,-左,y方向+下,-上
)
(2)AnimatedPadding
duration://必写
padding://必写
curve://动画的曲线
(3)AnimatedOpacity(透明度)
(4)AnimatedPositioned
属性同上(差不多)
(5)AnimatedDefaultTextStyle(文字动画)
(6)AnimatedSwitcher(当子元素改变触发动画)
属性: transitionBuilder(自定义动画效果)
- 显示动画
需要创建AnimationController,通过它控制动画的开始,暂停等
(1)RotationTransition
(2)FadeTransition
(3)SlideTransition
(4)ScaleTrasition
(5)AnimatedIcon(图标过渡) - Hero
路由(页面)之间飞入和飞出的widget
渲染
代码是想法(贴膜的购买需求),而渲染对象是落地的实物(贴在手机上的透明膜)
写地图点击效果时,屏幕坐标和地图坐标不一样,需要通过渲染对象来获取地图坐标,然后计算出减多少,才能把屏幕坐标转换成地图坐标
RenderBox类型的透明膜,可以自动把屏幕坐标转化成地图坐标,也就是可以独立完成坐标减法的计算
RenderBox renderBox = imageContext.findRenderObject() as RenderBox;
//用膜的上下文(名字牌)imageContext,调用findRenderObject()找膜的实体
Offset localOffset = renderBox.globalToLocal(
details.globalPosition,
//点击屏幕后获取屏幕地址details.globalPosition,通过renderBox,把值自动转化成地图地址,然后由Offset储存x和y的值
网络请求
- 在pubspec.yaml里面找到dependencies,添加
dependencies:
flutter:
sdk: flutter
dio: ^5.4.0 # 加入这一行(网络请求库)
- 在终端里面运行
flutter pub get
- 新建文件:request.dart
import 'package:dio/dio.dart';
class Request {
static final Dio dio = Dio();
// 基础配置
static void init() {
dio.options.baseUrl = "http://接口地址"; // 例如:http://192.168.1.100:8080/api
dio.options.connectTimeout = Duration(seconds: 5);
dio.options.receiveTimeout = Duration(seconds: 5);
}
// GET 请求
static Future get(String url, {Map<String, dynamic>? params}) async {
try {
Response response = await dio.get(url, queryParameters: params);
return response.data;
} catch (e) {
print("请求错误:$e");
return null;
}
}
// POST 请求
static Future post(String url, {Map<String, dynamic>? data}) async {
try {
Response response = await dio.post(url, data: data);
return response.data;
} catch (e) {
print("请求错误:$e");
return null;
}
}
}
- 在main.dart初始化
void main() {
Request.init();
runApp(MyApp());
}
- 在登入页面对接接口
把账号密码发送给服务器(POST),服务器校验,返回成功/失败
onPressed(){
//1.拿到用户输入的内容
String username = _nameCtrl.text.trim();
String password = _pwdCtrl.text.trim();
//2.发送网络请求
var result = awiat Request.post(
"接口地址",
data:{
"username":username,
"password":password,
},
);
//3.根据服务器返回判断是否成功
if (result != null && result["code"] == 200) {
// 成功 → 跳首页
Navigator.push(
context,
MaterialPageRoute(builder: (c) => Home()),
);
} else {
// 失败 → 提示错误
showDialog(
context: context,
builder: (c) => AlertDialog(
title: Text("登录失败"),
content: Text(result?["msg"] ?? "账号或密码错误"),
),
);
}
}
}
- 在注册页面对接接口
把账号密码发送 给服务器(POST),服务器保存,返回成功/失败
onPressed: () async {
String account = _accountCtrl.text.trim();
String pwd = _pwdCtrl.text.trim();
String confirmPwd = _confirmPwdCtrl.text.trim();
// 非空判断
if (account.isEmpty || pwd.isEmpty || confirmPwd.isEmpty) {
showDialog(...);
return;
}
// 两次密码一致判断
if (pwd != confirmPwd) {
showDialog(...);
return;
}
// 发送注册请求
var result = await Request.post(
"/user/register",
data: {
"username": account,
"password": pwd,
},
);
// 判断结果
if (result != null && result["code"] == 200) {
// 成功 → 返回登录页,并自动填账号密码
Navigator.pop(context, {
"account": account,
"password": pwd,
});
} else {
showDialog(...);
}
},
补充
- 加了_就是私有,只能在当前文件夹使用,外面不能访问
如何定义私有方法:
Widget _buildStoreCard(){
return Container(
);
}
//引用:_buildStoreCard()
- SingleChildScrollView
它的作用是让页面可以上下滚动
允许子组件无线长高(为了滚动)
但它自己的高度只由父组件决定,不会主动撑满屏幕
而sizebox.expand刚好强制子组件占满父容器的全部宽高
也就是说它只负责让页面滚动,但是要让他铺满整个页面,必须它一个明确的高度约束(expand)
- class:类
类用来描述一个东西有哪些属性
商品是类,名称、价格是属性
//1. 定义一个商品的类/模板
class Goods{
//final是只能读,不能改的
//商品名称
final String name;
//商品价格
final double price;
//构造函数Goods()
//required就是强制必须传这个值 this.name里面this是当前这个商品
Goods({required this.name,required this.price})
}
//2. 创建一个商品列表,放多个值进去
List<Goods> goodList = [
Goods(name:"2",price:10),
Goods(name:"3",price:20),
];
- 表格
Table:整个表格
TableRow:整个表格里的一行
children:放所有行
Table(
children:[
//第一行
TableRow(
children:[
padding: EdgeInsets.all(8),
child: Text("序号"),
//...
],
),
//第二行
...
]
)
但是在写的过程中,会发现Table的列宽完全自适应,挤在一起
我们可以通过给Table加columnWidths,精准控制每列的宽度
Table(
columnWidths: const {
0: FixedColumnWidth(60), // 序号列:固定60px
1: FlexColumnWidth(2), // 商品名称列:占2份剩余空间
2: FlexColumnWidth(1), // 商品价格列:占1份剩余空间
3: FixedColumnWidth(120), // 操作列:固定120px
},
// 表格内部分隔线
border: TableBorder(
verticalInside: BorderSide(color: Colors.grey[300]!, width: 1),
horizontalInside: BorderSide(color: Colors.grey[300]!, width: 1),
),
children: [ ... ]
)
- 项目最基础格式
import 'package:flutter/material.dart';
void main(){
runApp(MyApp());
}
class MyApp extends StatelessWidget{
const MyApp({super.key});
Widget build(BuildContext context){
return MaterialApp(
home: LoginPage(),
theme: ThemeData(
primaryColor: Colors.black
),
);
}
}
class LoginPage extends StatefulWidget{
const LoginPage({super.key});
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
Widget build(BuildContext context){
return Scaffold()
}
}
- 登入到注册页面的跳转
点注册按钮->跳转注册页->等用户注册完->回来自动填账号
async:异步函数(只有被async标记的函数,才能写await)
await:等待异步操作完成并获取返回值
onPressed: () async {
// 跳去注册页,等注册完回来
var 手机号 = await Navigator.push(
context,
MaterialPageRoute(builder: (c) => RegisterPage()),
);
// 如果注册成功了,就自动填账号
if (手机号 != null) {
_nameCtrl.text = 手机号;
}
},
更多推荐

所有评论(0)