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(...);
      },
    ),
  ),
)


有状态组件和无状态组件

  1. 有状态组件
    外壳(固定不变)+ 内核(能交互,能变)
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) 这些常用区域
  );
}
}

提示框

  1. AlertDialog
    对话框
//触发方法
// 这三行代码本质完全一样,只是变量名不同
BuildContext context; // 通用名
BuildContext imageContext; // 语义化:和图片相关的上下文
BuildContext dialogContext; // 语义化:和对话框相关的上下文

void showBuildContext context){//外层context:定位弹窗在哪个页面
showDialog(
context:context,//定位到当前页面
builder:(BuildContext context){//内层context:对话框的地址
return AlertDialog(
title://标题
content://内容
actions:[
TextButton(
onpress:(){
return Navigator.pop(context)//返回到上一页的地址
},
child://按钮上的内容
)
)
}
)
}

  1. SnackBar
    底部提示条
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration://持续时间
content://内容
//可选:按钮
action:SnackBarAction()
)
)

点击事件

  1. GestureDetect
    给任意组件加手势(点击事件)
    用法:用它包裹被点击的元素,加上onTap等

GestureDetect的可点击范围有子组件决定,且遵循视觉可见性优先,如果子组件有Text,Image等,就默认能接受点击,但是当子组件完全透明无内容,GestureDetect就无法接收点击,进而穿透,触发下层第一个可见有内容的事件

behavior: HitTestBehavior.translucent
//作用:强制透明遮盖可点击

  1. 按钮组件:TextButton,OutlineButton,–FloatingActionButton(右下角悬浮按钮),ElevaButton
    通过onPressed处理

  2. 视觉反馈:InkWell(onTap)

  3. 其他: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的简写,返回要跳转的页面
);
},

状态更新

  1. 数据变化驱动UI更新:setState
    setState只能在StatefulWight里面使用
//onTap是当你点击时,(index)是会把点的是第几个告诉我们:flutter会根据按钮顺序自动分配序号,所以点击时index是可知的
onTap:(index){
	setState((){
		_currentIndex = index;
	});
//setState((){})的作用是更新页面
}

如果有较少的页面切换,可以使用三元运算符

//格式
body:index == 0
? mappage()
: index == 1
? shoupage()
: homepage;

补充:三元运算符知识点
条件? 满足执行:不满足执行


布局组件

  1. 基础容器:
    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左右
  1. 线性布局
    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才能使用
  1. 弹性布局
    Flex
    允许沿着(垂直或者水平)方向,排列子组件,结合Column和Row
    Expanded(强制占满所有剩余空间)和Flexible作为Flex的子组件通过Flex属性来fenp空间
  2. 流式布局
    Wrap
    自动换行
    (Column/Row/Flex均不会换行)
    属性:direction(Axis.horizontal水平 Axis.vertical垂直),children,alignment,spacing,runspacing(间距)
  3. 层叠布局
    Stack/Positioned
    属性:alignment,children, fit
    Positioned作为Stackd的黄金搭档,进行精确定位(通过left,right,top,bottom)

基础组件

  1. Text
    文本组件
    属性:date,style,textAlign,maxLines(最大行数)
    需要在同一文本显示不同样式,用Text.rich构造函数配合TextSpan来实现
  2. Image
    图片组件
    属性:width,height,fit,alignment
  3. TextField
    文本输入组件
    属性:controller,decortation(类型:InputDecoration),style,maxLines,onChanged,onSubmitted,obscureText(用于密码)

controller控制器的用法

  1. 页面初始化的时候(build之前)创建监听器
  2. 绑定监听器
  3. 判断监听器获取到的内容,并弹出底部弹框
//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. 1个输入框一个TextEditingController
  2. 在弹窗的TextField里面,给TextEditingController绑定输入框
  3. 用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;
});
}
)


  1. 滚动组件
    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

  1. 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. 隐式动画:
    通过几行代码就能实现,背后实现原理和繁琐的操作细节被隐去
    (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(自定义动画效果)

  1. 显示动画
    需要创建AnimationController,通过它控制动画的开始,暂停等
    (1)RotationTransition
    (2)FadeTransition
    (3)SlideTransition
    (4)ScaleTrasition
    (5)AnimatedIcon(图标过渡)
  2. Hero
    路由(页面)之间飞入和飞出的widget

渲染

代码是想法(贴膜的购买需求),而渲染对象是落地的实物(贴在手机上的透明膜)
写地图点击效果时,屏幕坐标和地图坐标不一样,需要通过渲染对象来获取地图坐标,然后计算出减多少,才能把屏幕坐标转换成地图坐标
RenderBox类型的透明膜,可以自动把屏幕坐标转化成地图坐标,也就是可以独立完成坐标减法的计算

 RenderBox renderBox = imageContext.findRenderObject() as RenderBox;
 //用膜的上下文(名字牌)imageContext,调用findRenderObject()找膜的实体
 Offset localOffset = renderBox.globalToLocal(
                    details.globalPosition,
//点击屏幕后获取屏幕地址details.globalPosition,通过renderBox,把值自动转化成地图地址,然后由Offset储存x和y的值



网络请求

  1. 在pubspec.yaml里面找到dependencies,添加
dependencies:
  flutter:
    sdk: flutter
  dio: ^5.4.0   # 加入这一行(网络请求库)
  1. 在终端里面运行
flutter pub get
  1. 新建文件: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;
    }
  }
}
  1. 在main.dart初始化
void main() {
  Request.init(); 
  runApp(MyApp());
}
  1. 在登入页面对接接口

把账号密码发送给服务器(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"] ?? "账号或密码错误"),
      ),
    );
  }
}
}
  1. 在注册页面对接接口

把账号密码发送 给服务器(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(...);
  }
},

补充

  1. 加了_就是私有,只能在当前文件夹使用,外面不能访问
    如何定义私有方法:
Widget _buildStoreCard(){
return Container(
);
}
//引用:_buildStoreCard()
  1. SingleChildScrollView
    它的作用是让页面可以上下滚动

允许子组件无线长高(为了滚动)
但它自己的高度只由父组件决定,不会主动撑满屏幕
而sizebox.expand刚好强制子组件占满父容器的全部宽高
也就是说它只负责让页面滚动,但是要让他铺满整个页面,必须它一个明确的高度约束(expand)

  1. 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),
];

  1. 表格

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: [ ... ]
)
  1. 项目最基础格式
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()
    }
    }
  1. 登入到注册页面的跳转

点注册按钮->跳转注册页->等用户注册完->回来自动填账号
async:异步函数(只有被async标记的函数,才能写await)
await:等待异步操作完成并获取返回值

onPressed: () async {
  // 跳去注册页,等注册完回来
  var 手机号 = await Navigator.push(
    context,
    MaterialPageRoute(builder: (c) => RegisterPage()),
  );

  // 如果注册成功了,就自动填账号
  if (手机号 != null) {
    _nameCtrl.text = 手机号;
  }
},
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐