Flutter 入门学习

创建项目

在目标目录下执行指令:

flutter create --platforms web <name>

运行项目

直接运行main即可

启动文件说明以及基础内容

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:
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 任意组件间 复杂状态管理

父传子

步骤:

  1. 子组件定义接收属性
  2. 子组件在构造函数中接收参数
  3. 父组件传递属性给子组件
  4. 有状态组件在’对外的类’接收属性,'对内的类’通过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),),
    );
  }
}

子传父

步骤:

  1. 父组件传递一个函数给子组件
  2. 子组件调用这个函数
  3. 父组件通过回调函数获取参数
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
基本使用: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!!!!!!!"),
    );
  }
}
Logo

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

更多推荐