Skip to content

介绍

官方文档 flutter.cn

社区文档 book.flutterchina.club

搭建开发环境

安装 Flutter SDK

js
// https://docs.flutter.dev/development/tools/sdk/releases 下载链接
//  Flutter SDK 自带了 Dart sdk 所以安装了 Flutter SDK 就不需要再单独安装 Dart sdk了

配置环境变量

js
// 和配置 Dart 环境变量一致
// flutter doctor -v 在命令行中输入检查是否配置成功

配置镜像

js
// 由于在国内访问 flutter 可能会受到限制,所以我们需要配置镜像来避免
// 和配置 Dart 环境变量一致,只不过是在用户变量中新建两个变量
// flutter doctor -v 在命令行中输入检查是否配置成功

需要在用户环境变量中新建两个变量,如下图

具体值:

PUB_HOSTED_URL=https://pub.flutter-io.cn

FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

使用 Android Studio 安装 Android SDK

js
// https://developer.android.google.cn/studio 下载地址

安装 vscode 插件

js
// 直接搜索 Flutter 安装插件

初始化项目

js
// flutter create 工程名     打开终端创建flutter 项目

启动 可以在 vscode 右下角切换运行的平台

如果在浏览器启动失败,就打开项目目录启动命令行运行如下命令

js
// flutter run -d chrome --web-port=8080 --web-hostname=127.0.0.1

目录结构

主文件介绍

main.dart

dart
@override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      // 指定应用程序的第一个页面
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }

Scaffold 组件

组件库使用

组件简介

无状态组件

dart
// 无状态组件只能通过外部重新创建该组件才能更新页面
// 导入组件库
import 'package:flutter/material.dart';

class MyWidget1 extends StatelessWidget {
  MyWidget1([this.text = '无状态组件']);
  // 无状态组件里的数据都是不可变的,所以必须使用 final 定义
  final String text;
  @override
  Widget build(BuildContext context) {
    // Container组件类似于 html 里的 span 标签
    // flutter 是通过组件嵌套来构建页面的
    return Container(
      // 设置组件背景色
      color: Colors.white,
      // 设置文字居中对齐
      alignment: Alignment.center,
      // 可以直接通过属性名使用属性
      child: Text(text),
    );
  }
}

有状态组件

dart
// 导入组件库
import 'package:flutter/material.dart';

// 在编译器里写入 st 即可唤出快捷创建组件代码块
class MyWidget2 extends StatefulWidget {
  const MyWidget2({super.key});
  @override
  State<MyWidget2> createState() => _MyWidget2State();
}

class _MyWidget2State extends State<MyWidget2> {
  int num = 0;
  @override
  Widget build(BuildContext context) {
    return Container(
      // 文字按钮组件
      child: TextButton(
          // 可以通过 style 属性设置样式
          child: Text('点击了$num次', style: TextStyle(fontSize: 30.0)),
          // 当 TextButton 被点击会触发 onPressed 函数
          onPressed: () {
            // 有状态组件中通过 setState 方法修改状态,当被修改时组件中所有依赖此状态的地方会自动更新
            setState(() {
              num++;
            });
          }),
    );
  }
}

生命周期

dart
// -----------------------------------组件生命周期---------------------------------------
// createState				组件创建 State 对象的方法,只调用一次(创建 state 时)

// initState				初始化状态时调用,只调用一次(在 state 被插入视图树时)

// didChangeDependencies	当前组件 state 对象依赖关系发生变化后,会在 initState 后立即调用								(initState 后及 state 对象发生变化时,widget 树中,若节点的父级,结构中层级或父级结构中的任一节点的widget类型发生变化,才会调用,若仅仅只是父结构某一节点的属性变化则不会调用。可以告诉你图层正在进行较大成本的重绘)

// build					渲染组件时(state 准备好数据需要渲染时)

// addPostFrameCallback		组件渲染结束之后的回调,只会调用一次

// didUpdateWidget			当Widget配置发生变化时,比如父Widget触发重建(即父Widget的状态发生变化							时),热重载,系统会调用这个函数(父组件更新状态触发重载时)

// deactivate				组件销毁前

// dispose					组件销毁后

// -----------------------------------app 生命周期---------------------------------------

// resumed 					可见并能响应用户输入

// inactive					处在不活跃状态无法响应用户输入

// paused 					不可见也无法响应用户输入

// -----------------------------------路由生命周期---------------------------------------
// RouteAware:监听路由变化
// RouteObserver 是一个配合 RouteAware 的一个类,通过这个类可以通知当前页面该执行那个生命周期方法否则只混入 RouteAware 是不能执行的。另外还有 RouteObserver 需要注册在 MaterialApp 中,这样才能在导航的过程中执行对应生命周期方法
//
// didPush					从其他页面跳转到当前页

// didPushNext				从当前页面跳转到其他页面调用

// didPop					从当前页退回到上个页面

// didPopNext				从其他页面退回到当前页
dart
// 导入组件库
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

// 在编译器里写入 st 即可唤出快捷创建组件代码块
class MyWidget2 extends StatefulWidget {
  const MyWidget2({super.key});
  @override
  // createState 表示创建状态时
  State<MyWidget2> createState() => _MyWidget2State();
}

class _MyWidget2State extends State<MyWidget2> {
  int num = 0;
  @override
  // 组件初始化调用
  void initState() {
    print('initState');
    // 注册监听回调,监听组件是否渲染结束
    SchedulerBinding.instance.addPersistentFrameCallback((timeStamp) {
      print('组件渲染完成');
    });
    super.initState();
  }

  @override
  // 当前组件依赖的数据发生变化时
  void didChangeDependencies() {
    print('didChangeDependencies');
    super.didChangeDependencies();
  }

  @override
  // 组件销毁前
  void deactivate() {
    print('deactivate');
    super.deactivate();
  }

  @override
  // 组件销毁后
  void dispose() {
    print('dispose');
    super.dispose();
  }

  @override
  // 渲染组件时
  Widget build(BuildContext context) {
    return Container(
      // 文字按钮组件
      child: TextButton(
          // 可以通过 style 属性设置样式
          child: Text('点击了$num次', style: TextStyle(fontSize: 30.0)),
          // 当 TextButton 被点击会触发 onPressed 函数
          onPressed: () {
            // 有状态组件中通过 setState 方法修改状态,当被修改时组件中所有依赖此状态的地方会自动更新
            setState(() {
              num++;
            });
          }),
    );
  }
}

基础组件

Container

dart
import 'package:flutter/material.dart';
class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        body: Container(
          // 当需要子盒子在父盒子中居中时需要在父组件中设置居中属性
          alignment: Alignment.center,
          child: Container(
            width: 300,
            height: 200,
            color: Colors.blue,
            padding: EdgeInsets.only(left: 30, top: 30, right: 30, bottom: 10),
            // 子组件默认是撑满父组件的
            child: Container(
              // 当设置了 decoration 属性后就不能再单独设置 color 属性了
              // color: Colors.yellow,
              decoration: BoxDecoration(
                  color: Colors.yellow,
                  border: Border.all(color: Colors.red, width: 2),
                  // 设置组件上下左右各角圆角都为10
                  // 如果想分别设置可以使用 BorderRadius.only
                  borderRadius: BorderRadius.circular(10)),
            ),
          ),
        ));
  }
}

Text

dart
import 'package:flutter/material.dart';
class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        body: Container(
          alignment: Alignment.center,
          child: Container(
            width: 200,
            height: 200,
            color: Colors.blue,
            child: Text(
              '文字组件,文字组件,文字组件,文字组件,文字组件',
              style: TextStyle(
                  color: Colors.red, fontSize: 24, fontWeight: FontWeight.w700),
              // 文字最大显示行数
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
            ),
          ),
        ));
  }
}

Image

加载本地资源

dart
import 'package:flutter/material.dart';
class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        body: Container(
          alignment: Alignment.center,
          child: Container(
              width: 300,
              height: 400,
              color: Colors.blue,
              // child: Image.network(),
              child: Image.asset(
                'assets/log.jpg',
                // 设置图片填充方式
                fit: BoxFit.cover,
                // Image 组件在加载新图片时默认先将旧图片清空,所以在新图加载展示完成之前会有一段空白内容,会造成 “闪一下”的问题,gaplessPlayback 设置为 true 可以指定在加载新图时不清空旧图直到新图展示出来为止,可以解决上述问题
                gaplessPlayback: true
              )),
        ));
  }
}

布局组件

线性布局

dart
import 'package:flutter/material.dart';
class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        // Center 组件等价于 Container组件设置alignment: Alignment.center居中属性,里面的子组件会居中显示
        body: Center(
          child: Container(
            width: 200,
            height: 240,
            color: Colors.yellow,
            // Column的子组件会纵向排列
            child: Column(
                // 主轴方向的对齐方式
                // mainAxisAlignment: MainAxisAlignment.start,
                // 辅轴方向的对齐方式
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Image.asset(
                    'assets/log.jpg',
                    width: 200,
                    height: 140,
                    fit: BoxFit.cover,
                  ),
                  Padding(
                    padding: EdgeInsets.only(top: 10),
                    child: Text('文字1'),
                  ),
                  Padding(
                    padding: EdgeInsets.only(top: 10),
                    child: Text('文字2'),
                  )
                ]),
          ),
        ));
  }
}
dart
import 'package:flutter/material.dart';

class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        body: Center(
          child: Container(
            height: 100,
            color: Colors.yellow,
            // Column的子组件会纵向排列
            child: Row(
                // 主轴方向的对齐方式
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                // 辅轴方向的对齐方式
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Image.asset(
                    'assets/log.jpg',
                    width: 86,
                    height: 86,
                    fit: BoxFit.cover,
                  ),
                  Text('文字1')
                ]),
          ),
        ));
  }
}

弹性布局

dart
import 'package:flutter/material.dart';

class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        body: Center(
          child: Container(
            height: 100,
            color: Colors.yellow,
            // Column的子组件会纵向排列
            child: Row(children: [
              Expanded(
                  flex: 1,
                  child: Image.asset(
                    'assets/log.jpg',
                    height: 140,
                    fit: BoxFit.cover,
                  )),

              // 使用Expanded组件解决文字溢出问题
              Expanded(
                  flex: 2,
                  child: Padding(
                    padding: EdgeInsets.only(left: 10),
                    child: Text('文字2文字2文字2文字2文字2文字2文字2文字2文字2文字2文字2文字2'),
                  ))
            ]),
          ),
        ));
  }
}

层叠布局

dart
import 'package:flutter/material.dart';

class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        body: Center(
          child: Stack(alignment: Alignment.center, children: [
            Positioned(
                top: 70,
                child: Container(
                  width: 200,
                  height: 200,
                  color: Colors.pink,
                )),
            Positioned(
                top: 0,
                child: Image.asset(
                  'assets/log.jpg',
                  width: 80,
                  height: 80,
                  fit: BoxFit.cover,
                ))
          ]),
        ));
  }
}

综合案例

dart
import 'package:flutter/material.dart';
class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        body: Container(
          padding: EdgeInsets.all(10),
          child: Column(
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  // Color 可以设置自定义颜色,0x 表示16进制颜色,FF表示透明度(不透明)后面则是色值
                  Text('2021-05-15 21:49:48',
                      style: TextStyle(color: Color(0xFF666666), fontSize: 13)),
                  Text('待发货',
                      style: TextStyle(color: Color(0xFFFF9246), fontSize: 14))
                ],
              ),
              Padding(
                  padding: EdgeInsets.only(top: 10),
                  child: Row(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Image.asset(
                          'assets/log.jpg',
                          width: 86,
                          height: 86,
                          fit: BoxFit.cover,
                        ),
                        Expanded(
                            child: Padding(
                                padding: EdgeInsets.only(left: 10),
                                child: Column(
                                    mainAxisAlignment: MainAxisAlignment.start,
                                    crossAxisAlignment:
                                        CrossAxisAlignment.start,
                                    children: [
                                      Row(
                                          mainAxisAlignment:
                                              MainAxisAlignment.spaceBetween,
                                          children: [
                                            Expanded(
                                                child: Text(
                                                    '茶水分离杯耐热隔热玻璃杯茶水分离杯耐热隔热玻璃杯',
                                                    style: TextStyle(
                                                        color:
                                                            Color(0xFF262626),
                                                        fontSize: 13))),
                                            Padding(
                                              padding:
                                                  EdgeInsets.only(left: 10),
                                              child: Text('x1',
                                                  style: TextStyle(
                                                      color: Color(0xFF262626),
                                                      fontSize: 15)),
                                            ),
                                          ]),
                                      Padding(
                                        padding: EdgeInsets.only(top: 6),
                                        child: Container(
                                          decoration: BoxDecoration(
                                            color: Color(0xFFF7F7F7),
                                            borderRadius:
                                                BorderRadius.circular(2),
                                          ),
                                          padding: EdgeInsets.only(
                                              left: 5,
                                              right: 2,
                                              top: 3,
                                              bottom: 3),
                                          child: Text(
                                            '规格:白色240ml',
                                            style: TextStyle(
                                                color: Color(0xFF888888),
                                                fontSize: 11),
                                          ),
                                        ),
                                      ),
                                      Padding(
                                        padding: EdgeInsets.only(top: 6),
                                        child: Text(
                                          '¥119.5',
                                          style: TextStyle(
                                              color: Color(0xFF888888),
                                              fontSize: 11),
                                        ),
                                      )
                                    ])))
                      ])),
              Padding(
                padding: EdgeInsets.only(top: 10),
                child: Container(
                  alignment: Alignment.centerRight,
                  child: Text(
                    '合计:¥195',
                    style: TextStyle(color: Color(0xFF262626), fontSize: 16),
                  ),
                ),
              ),
              Padding(
                padding: EdgeInsets.only(top: 10),
                child: Container(
                  alignment: Alignment.centerRight,
                  child: Container(
                      decoration: BoxDecoration(
                          border:
                              Border.all(color: Color(0xFF666666), width: 0.5),
                          borderRadius: BorderRadius.circular(2)),
                      padding: EdgeInsets.only(
                          left: 15, right: 15, top: 7, bottom: 7),
                      child: Text(
                        '再次购买',
                        style:
                            TextStyle(color: Color(0xFF262626), fontSize: 16),
                      )),
                ),
              ),
            ],
          ),
        ));
  }
}

滚动组件

ListView

默认构造函数方式创建

dart
import 'package:flutter/material.dart';

class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        body: Container(
          child: ListView(
            // 滚动效果:禁止滚动
            physics: NeverScrollableScrollPhysics(),
            // 滚动方向:纵向(默认)横向滚动
            scrollDirection: Axis.horizontal,
            children: [
              Container(
                height: 120,
                color: Colors.red,
                child: Text('item1'),
              ),
              Container(
                height: 120,
                color: Colors.yellow,
                child: Text('item1'),
              ),
              Container(
                height: 120,
                color: Colors.black,
                child: Text('item1'),
              ),
              Container(
                height: 120,
                color: Colors.white,
                child: Text('item1'),
              ),
              Container(
                height: 120,
                color: Colors.blue,
                child: Text('item1'),
              ),
              Container(
                height: 120,
                color: Colors.amberAccent,
                child: Text('item1'),
              ),
              Container(
                height: 120,
                color: Colors.grey,
                child: Text('item1'),
              ),
            ],
          ),
        ));
  }
}

命名构造函数 build 方式

dart
class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        body: Container(
          child: ListView.builder(
              // 用于指定item渲染的个数,如果不设置则会无限滚动
              itemCount: 10,
              // itemExtent:120 使用该属性代替给子组件设置高可以提高性能
              // itemBuilder 用于设置列表每个item项
              // context表示当前组件上下文,index 表示当前渲染的索引值
              itemBuilder: ((context, index) {
                return Container(
                  height: 120,
                  color: Colors.red,
                  child: Text('item$index'),
                );
              })),
        ));
  }
}

命名构造函数 separated

dart
class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        body: Container(
          child: ListView.separated(
              // 用于指定分割线
              separatorBuilder: ((context, index) {
                return Container(color: Colors.grey, height: 8);
              }),
              // 用于指定item渲染的个数,如果不设置则会无限滚动
              itemCount: 10,
              // itemBuilder 用于设置列表每个item项
              itemBuilder: ((context, index) {
                return Container(
                  height: 120,
                  color: Colors.red,
                  child: Text('item$index'),
                );
              })),
        ));
  }
}

GridView

默认构造函数方式

布局方式 1

固定次轴子元素个数

dart
class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        body: Container(
          child: GridView(
            // SliverGridDelegateWithFixedCrossAxisCount 默认是纵向排列滚动的,主轴是纵向的
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3, // 表示次轴 item 项个数
                mainAxisSpacing: 10, // 主轴间距
                crossAxisSpacing: 10, // 次轴间距
                // 当屏幕大小不一时 flutter 会按照这个比例自行计算宽高
                // 当用户将手机展示方向换为横屏时布局也会和竖屏一致
                childAspectRatio: 4 / 3 // 表示每个item项的长宽比
                ),
            children: [
              Container(
                color: Colors.red,
                child: Text('item'),
              ),
              Container(
                color: Colors.red,
                child: Text('item'),
              ),
              Container(
                color: Colors.red,
                child: Text('item'),
              ),
              Container(
                color: Colors.red,
                child: Text('item'),
              ),
              Container(
                color: Colors.red,
                child: Text('item'),
              ),
              Container(
                color: Colors.red,
                child: Text('item'),
              ),
            ],
          ),
        ));
  }
}
布局方式 2

固定次轴子元素最大宽度

dart
class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        body: Container(
          child: GridView(
            gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
                maxCrossAxisExtent: 60, // 表示次轴 item 项最大宽度
                mainAxisSpacing: 10, // 主轴间距
                crossAxisSpacing: 10, // 次轴间距
                childAspectRatio: 4 / 3 // 表示每个item项的长宽比
                ),
            children: [
              Container(
                color: Colors.red,
                child: Text('item'),
              ),
              Container(
                color: Colors.red,
                child: Text('item'),
              ),
              Container(
                color: Colors.red,
                child: Text('item'),
              ),
              Container(
                color: Colors.red,
                child: Text('item'),
              ),
              Container(
                color: Colors.red,
                child: Text('item'),
              ),
              Container(
                color: Colors.red,
                child: Text('item'),
              ),
            ],
          ),
        ));
  }
}

命名构造函数 builder

dart
class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        body: Container(
          child: GridView.builder(
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                mainAxisSpacing: 10,
                crossAxisSpacing: 10,
                childAspectRatio: 4 / 3),
            itemCount: 20, // 子元素个数
            // 指定子元素
            itemBuilder: (context, index) {
              return Container(
                color: Colors.red,
                child: Text('item'),
              );
            },
          ),
        ));
  }
}

命名构造函数 count 和 extent

dart
class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        body: Container(
          // GridView.count 可以直接设置子元素个数的布局方式
          // GridView.extent 可以直接设置子元素最大宽度的布局方式
          child: GridView.count(
            // 快速指定样式配置
              crossAxisCount: 3,
              mainAxisSpacing: 10,
              crossAxisSpacing: 10,
              childAspectRatio: 4 / 3,
              children: [
                Container(
                  color: Colors.red,
                  child: Text('item'),
                ),
                Container(
                  color: Colors.red,
                  child: Text('item'),
                ),
                Container(
                  color: Colors.red,
                  child: Text('item'),
                ),
                Container(
                  color: Colors.red,
                  child: Text('item'),
                ),
                Container(
                  color: Colors.red,
                  child: Text('item'),
                ),
                Container(
                  color: Colors.red,
                  child: Text('item'),
                ),
              ]),
        ));
  }
}

CustomScrollView

dart
class MyWidget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('头部部分'),
        ),
        body: Container(
            child: CustomScrollView(
          slivers: [
            // SliverGrid.count 对应 GridView.count
            SliverGrid.count(
                crossAxisCount: 3,
                mainAxisSpacing: 10,
                crossAxisSpacing: 10,
                childAspectRatio: 4 / 3,
                children: [
                  Container(
                    color: Colors.red,
                    child: Text('item'),
                  ),
                  Container(
                    color: Colors.red,
                    child: Text('item'),
                  ),
                  Container(
                    color: Colors.red,
                    child: Text('item'),
                  ),
                  Container(
                    color: Colors.red,
                    child: Text('item'),
                  ),
                  Container(
                    color: Colors.red,
                    child: Text('item'),
                  ),
                  Container(
                    color: Colors.red,
                    child: Text('item'),
                  ),
                ]),
            SliverList(
              // delegate 类似与 ListView 中的 itemBuilder
              delegate: SliverChildBuilderDelegate((context, index) {
                return Container(
                  height: 120,
                  child: Text('item$index'),
                );
              },
                  // 子元素个数
                  childCount: 10),
            )
          ],
        )));
  }
}

动画组件

平移动画

dart
class MyWidget2 extends StatefulWidget {
  @override
  State<MyWidget2> createState() => _MyWidget2State();
}

class _MyWidget2State extends State<MyWidget2>
    with SingleTickerProviderStateMixin {
  /// 动画控制器
  late AnimationController _animationController;

  /// 动画对象,Offset用于约束此动画是平移动画
  late Animation<Offset> _animation;
  @override
  void initState() {
    // 创建动画控制器
    // vsync 用于监听屏幕刷新,如果动画不在可展示区域,会自动停止动画提高性能
    // 将本组件指定给 vsync 即可,但本组件必须混入 SingleTickerProviderStateMixin,使本组件可以监听屏幕刷新
    // duration 用于指定动画执行时间
    _animationController =
        AnimationController(vsync: this, duration: Duration(seconds: 2));
    // 创建动画对象,Offset 表示偏移x、y的值,这里表示一倍宽高,Offset.zero 相当于Offset(0,0)
    // 还需要通过 animate 将动画挂载到动画控制器
    _animation = Tween<Offset>(begin: Offset.zero, end: Offset(1, -1))
        .animate(_animationController);
    // 启动动画,repeat 表示循环执行动画
    _animationController.repeat();
  }

  @override
  void dispose() {
    // 销毁动画
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('动画组件'),
        ),
        body: Center(
          // 平移组件
          child: SlideTransition(
            position: _animation,
            child: Container(
              width: 80,
              height: 80,
              color: Colors.red,
            ),
          ),
        ));
  }
}

缩放动画

dart
class MyWidget2 extends StatefulWidget {
  @override
  State<MyWidget2> createState() => _MyWidget2State();
}

class _MyWidget2State extends State<MyWidget2>
    with SingleTickerProviderStateMixin {
  /// 动画控制器
  late AnimationController _animationController;

  /// 动画对象,double用于约束此动画是缩放动画
  late Animation<double> _animation;
  @override
  void initState() {
    // 创建动画控制器
    _animationController =
        AnimationController(vsync: this, duration: Duration(seconds: 2));
        // 这里表示从0缩放到2倍
    _animation = Tween<double>(begin: 0, end: 2)
        .animate(_animationController);
    _animationController.repeat();
  }

  @override
  void dispose() {
    // 销毁动画
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('动画组件'),
        ),
        body: Center(
          // 缩放组件
          child: ScaleTransition(
            scale: _animation,
            child: Container(
              width: 10,
              height: 10,
              color: Colors.red,
            ),
          ),
        ));
  }
}

底部 tab 栏组件

dart
Widget build(BuildContext context) {
    return Scaffold(
      // 底部 tab 栏组件
      bottomNavigationBar: BottomNavigationBar(
          // 设置选中时的字号
          selectedFontSize: 10,
          // 设置未选中时的字号
          unselectedFontSize: 10,
          // 设置选中时的文字颜色
          selectedItemColor: const Color(0xFF3CCEAF),
          // 设置未选中时的文字颜色
          unselectedItemColor: const Color(0xFF383838),
          // 用于解决当 items 个数大于 3 个时组件会变得“透明”
          type: BottomNavigationBarType.fixed,
          items: [
            BottomNavigationBarItem(
                // 设置未选中时的图标
                icon: Icon(Icons.home),
                // 设置选中时的图标
                activeIcon: Image.asset('assets/home_sel.png'),
                label: '首页'
            ),
            BottomNavigationBarItem(icon: Icon(Icons.category), label: '分类'),
            BottomNavigationBarItem(
                icon: Icon(Icons.shopping_cart), label: '购物车'),
            BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
          ]),
    );
  }

主题组件

主题组件可以修改子组件的主题样式

dart
// 使用 Theme 组件修改 BottomNavigationBar 组件点击和长按时的背景色
Widget build(BuildContext context) {
    return Scaffold(
        bottomNavigationBar: Theme(
            data: ThemeData(
                // 设置点击时的背景颜色为透明
                splashColor: Colors.transparent,
                // 设置长按时的背景颜色为透明
                highlightColor: Colors.transparent),
            child: BottomNavigationBar(),
        ),
    );
}

分页组件

dart
// PageView 在使用时必须指定高度
Container(
    height: 200,
    child: PageView(
        // children 中每一项都作为一页
        children: [Text('第一页'), Text('第二页')],
        // PageView 翻页事件参数表示当前页索引
        onPageChanged: (i) {
            setState(() {
                isSingle = widget.CategoryGrids!
                    .sublist(i * 10, widget.CategoryGrids!.length)
                    .length <=
                    5;
            });
        },
    ),
),

高度变化动画组件

dart
// AnimatedContainer 组件用于高度宽度发生变化时展示动画
AnimatedContainer(
    // 动画执行的时间
    duration: Duration(milliseconds: 400),
    // 动画函数,ease 表示先快后慢的动画
    curve: Curves.ease,
    height: isSingle ? totalHeight : totalHeight2,
    child: PageView(
        children: _buildPages(widget.CategoryGrids as List),
        // PageView 翻页事件参数表示当前页索引
        onPageChanged: (i) {
            setState(() {
                isSingle = widget.CategoryGrids!
                    .sublist(i * 10, widget.CategoryGrids!.length)
                    .length <=
                    5;
            });
        },
    ),
)

富文本组件

dart
RichText(
    text: const TextSpan(
        text: '¥',
        style: TextStyle(
            color: Color(0xFFCF4444), fontSize: 9, fontWeight: FontWeight.w400),
        children: [
            TextSpan(
                text: '99',
                style: TextStyle(
                    fontSize: 12,
                )),
            TextSpan(
                text: '.0',
                style: TextStyle(
                    fontSize: 12,
                ))
        ],
    ));

吸顶组件

dart
// 吸顶组件
SliverPersistentHeader(
    // 设置是否吸顶
    pinned: true,
    delegate: CustomSlivePersistentHeaderDelegate());

// 自定义 SliverPersistentHeaderDelegate 子类,构建吸顶视图
class CustomSlivePersistentHeaderDelegate
    extends SliverPersistentHeaderDelegate {
  // 用于构建吸顶视图内容
  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
      // shrinkOffset 表示吸顶之后视图滚动的距离(范围是 0 - maxExtent)
    return Container(
      height: 44,
      color: Colors.red,
    );
  }

  // 定义吸顶视图最大高度
  @override
  double get maxExtent => 44;

  // 定义吸顶视图最小高度
  @override
  double get minExtent => 44;

  // 在视图吸顶时是否需要重新构建视图
  @override
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
    // oldDelegate 表示当前视图是否发生变化
    // 如果返回true表示在视图吸顶的时候需要重新构建吸顶视图
    return true;
  }
}

导航组件

dart
class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}
// 创建 TabController 控制器必须混入 TickerProviderStateMixin
class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
  /// TabController
  TabController? _controller;

  // 构建分类导航内容
  List<Widget> _buildTabBar(List? categoryBanners) {
    List<Widget> items = [];
    categoryBanners?.forEach((element) {
      items.add(Container(
        width: 60,
        height: 44,
        child: Text(element['name']),
      ));
    });
    return items;
  }

  @override
  void initState() {
    // 初始化 TabController 保证在页面初始化时 _controller 是有控制器的
    _controller = TabController(length: 0, vsync: this);
    getHttp();
    super.initState();
  }

  getHttp() async {
    try {
      Response response = await index();
      setState(() {
        // vsync 用于监听 TabController 的交互,直接传递当前组件即可(当前组件必须混入 TickerProviderStateMixin)
        _controller =
            TabController(length: categoryBanners!.length, vsync: this);
      });
    } catch (e) {
      debugPrint('$e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF7F7F8),
      appBar: AppBar(
        title: const Text('首页'),
      ),
      body: CustomScrollView(
        slivers: [
          SliverPersistentHeader(
              pinned: true,
              delegate: CustomSlivePersistentHeaderDelegate(
                  // TabBar 导航栏组件
                  tabBar: TabBar(
                 // 是否允许滚动
                isScrollable: true,
                // 选中和未选中文字颜色
                labelColor: const Color(0xFF27BA9B),
                unselectedLabelColor: const Color(0xFF333333),
                // 去除底部指示器
                indicator: const BoxDecoration(),
                // 取消文本左右间距
                labelPadding: EdgeInsets.zero,
                // 控制器
                controller: _controller,
                tabs: _buildTabBar(categoryBanners),
              ))),

            SliverToBoxAdapter(
            child: Container(
              padding: const EdgeInsets.only(left: 10, right: 10),
              height: 1200,
              // TabBarView 组件
              // controller 必须传递要与之联动的 TabBar 组件的控制器
              child: TabBarView(
                  controller: _controller,
                  // children 存放每个 TabBarView 的内容
                  children: _buildCategoryGoods(categoryBanners)),
            ),
          ),
        ],
      ),
    );
  }
}

手势组件

dart
// GestureDetector 组件可以处理各种手势,如点击双击等
GestureDetector(
    onTap: () {
        print('点击');
    },
    child: const Icon(Icons.person),
)

监听滚动状态组件

dart
// NotificationListener 可以监听滚动视图的状态
NotificationListener(
    // notification 保存滚动视图的滚动状态的
    onNotification: (ScrollNotification notification) {
        // notification.depth 获取当前滚动组件层级,这里指最底层的滚动视图
        // ScrollEndNotification 表示是否滚动结束
        if (notification.depth == 0 &&
            notification is ScrollEndNotification) {
            // notification.metrics.pixels 获取当前滚动的距离
            // MediaQuery.of(context).size.height 获取当前设备屏幕高度
            if (notification.metrics.pixels >=
                MediaQuery.of(context).size.height) {
                _isShowTop = true;
            } else {
                _isShowTop = false;
            }
            setState(() {});
        }
        // 返回 true 表示已经拿到需要的信息,不再继续冒泡下发通知
        return true;
    },
    child: Stack(
        children: [
            ListView()
        ]
    )
)

跳转组件

dart
Navigator.push(
    context,
    CupertinoPageRoute(
        // 表示页面推入的方式,false(默认值) 表示从屏幕右侧向左推入
        // true 表示从屏幕底向上推入
        fullscreenDialog: true,
        builder: (context) {
            // 表示要跳转的组件
            return const AccountLoginPage();
        }));
// 退出当前页面
Navigator.pop(context)

非组件文件跳转

dart
// 在 main.dart文件
// 创建 navigatorKey 用于在非组件文件做页面跳转
GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();


class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const RootPage(),
      builder: EasyLoading.init(),
      // 用于记录并获取全局的 navigatorKey 的状态(NavigatorState)
      navigatorKey: navigatorKey,
    );
  }
}
dart
// 在非组件文件跳转
// 在没有组件上下文的情况下跳转页面
navigatorKey.currentState!
    .push(CupertinoPageRoute(builder: (context) {
        // 这里 AccountLoginPage 是需要跳转组件
        return const AccountLoginPage();
    }));

携带参数

dart
Navigator.push(context, CupertinoPageRoute(builder: (context) {
    // 在跳转时携带参数(本质就是父子组件通信)
    return RegisterVerifyPage(mobile: _accountController.text);
}));
dart
// 在跳转的组件中声明参数
class RegisterVerifyPage extends StatefulWidget {
  // 准备参数
  final String? mobile;
  RegisterVerifyPage({this.mobile});

  @override
  State<RegisterVerifyPage> createState() => _RegisterVerifyPageState();
}
class _RegisterVerifyPageState extends State<RegisterVerifyPage> {
  @override
  Widget build(BuildContext context) {}
}

清除路由栈

dart
// 第二个参数表示需要推入的页面
// 第三个参数表示如何删除路由栈里的路由(true:不再清空路由,false: 清空所有路由)
// 顺序是先删除路由再推入第二个参数的页面
// ModalRoute.withName('/') 表示除首页保留其他全清除
Navigator.pushAndRemoveUntil(context,
            CupertinoPageRoute(builder: (context) {
          return const AccountLoginPage();
        }), ModalRoute.withName('/'));

appBar 组件

dart
AppBar(
    backgroundColor: const Color(0xFF00BF9B),
    // 左侧部分
    leading: IconButton(
        onPressed: () {
            Navigator.pop(context);
        },
        icon: const Icon(Icons.person),
    ),
    // 右侧部分可传递多个组件
    actions: [
        GestureDetector(
            onTap: () {
                debugPrint('注册');
            },
            child: Container(
                alignment: Alignment.center,
                padding: const EdgeInsets.only(right: 20),
                child: const Text(
                    '新用户注册',
                    style: TextStyle(fontSize: 14),
                ),
            ),
        )
    ],
),

滚动自适应组件

dart
// 当页面内容比较多当前屏幕无法放下时可自动生成滚动条,反之则不会生成
SingleChildScrollView(child: Column()),

表单输入组件

dart
class _AccountLoginPageState extends State<AccountLoginPage> {
  // 是否展示一键清空
  bool _isShowCleanAccount = false;
  // 是否展示一键清空
  bool _isShowCleanPassword = false;
  // 定义表单控制器
  final TextEditingController _accountController =
      TextEditingController(text: '');
  // 定义表单控制器 text 表示表单默认文本,后续获取表单内容可以 _passwodrController.text 获取
  final TextEditingController _passwodrController =
      TextEditingController(text: '');
  // 表单焦点
  final FocusNode _accountFocusNode = FocusNode();
  final FocusNode _passwordFocusNode = FocusNode();
  // 是否密文展示
  bool isObscureText = true;
  @override
  void initState() {
    // 监听表单焦点事件
    _passwordFocusNode.addListener(() {
      // hasFocus 表示当前表单是否获取到焦点
      print(_passwordFocusNode.hasFocus);
    });
    super.initState();
  }

  @override
  void dispose() {
    _passwodrController.dispose();
    _accountFocusNode.dispose();
    _passwordFocusNode.dispose();
    // _passwordFocusNode.unfocus(); 取消焦点实现关闭输入虚拟键盘
    super.dispose();
  }

  Widget _buildPasswordTextField() {
    return TextField(
        // 焦点
        focusNode: _passwordFocusNode,
        // 是否密文展示表单内容默认为 false
        obscureText: isObscureText,
        // 表单控制器
        controller: _passwodrController,
        // 表单内容变化事件
        onChanged: (value) {
            _passwordOnChanged(value);
        },
        // 光标设置
        cursorRadius: const Radius.circular(10),
        cursorColor: const Color(0xFFFFBD3B),
        cursorHeight: 18,
        style:
        const TextStyle(fontSize: 14, color: Color(0xFF333333)),
        decoration: const InputDecoration(
            // 文本居中
            contentPadding: EdgeInsets.only(top: 12),
            // 辅助文本居中
            isCollapsed: true,
            // 去除边框
            border: InputBorder.none,
            // 空表单提示文字
            hintText: '6到20位字母、数字符号组合',
            hintStyle:
            TextStyle(fontSize: 14, color: Color(0xFF333333)),
        ),
        // 表单类型
        keyboardType: TextInputType.text,
        // 控制键盘回车键
        textInputAction: TextInputAction.done,
    ),
      ),
      )),
      GestureDetector(
          onTap: () {
              // 清空表单
              _passwodrController.clear();
              setState(() {
                  _isShowCleanPassword = false;
              });
          },
          child: Text(_isShowCleanPassword ? '清空' : ''),
      ),
      GestureDetector(
          onTap: () {
              setState(() {
                  isObscureText = !isObscureText;
              });
          },
          child: const Text('看'),
      ),

禁止事件输入组件

dart
// AbsorbPointer 组件用于禁止组件事件()输入,absorbing 为 true 时表示禁止
// 如下写法可以阻止 TabBar 组件的点击事件
AbsorbPointer(absorbing: true,child: TabBar())

组件通信

父 call 子

GlobalKey 直接调用

子组件

dart
class FramsAnimation extends StatefulWidget {
  // 接收 key 并通过 super 传递给父组件(StatefulWidget 组件)
  const FramsAnimation({Key? key, this.initIndex = 1}) : super(key: key);
}

// 子组件改为非私有方便父组件进行类型约束(_FramsAnimationState 改为 FramsAnimationState)
class FramsAnimationState extends State<FramsAnimation> {
  void startAnimation() => _animationController.forward();
  void resetAnimation() => _animationController.reset();
}

父组件

dart
class RootPage extends StatefulWidget {
  const RootPage({super.key});
  @override
  State<RootPage> createState() => _RootPageState();
}

class _RootPageState extends State<RootPage> {
  // 父组件创建 GlobalKey 并指定泛型
  List framesKeys = [
    GlobalKey<FramsAnimationState>(debugLabel: 'home_key'),
    GlobalKey<FramsAnimationState>(debugLabel: 'category_key'),
    GlobalKey<FramsAnimationState>(debugLabel: 'cart_key'),
    GlobalKey<FramsAnimationState>(debugLabel: 'mine_key')
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      bottomNavigationBar: Theme(data: ThemeData(child: BottomNavigationBar(
            onTap: (i) {
              // 通过 GlobalKey 直接调用子组件方法
              framesKeys[i].currentState?.startAnimation();
              framesKeys[_curentIndex].currentState?.resetAnimation();
            },
      ),
    );
  }
}

网络请求

dart
// https://pub.flutter-io.cn/ 对标js中的npm

安装 dio

打开 pubspec.yaml 在 dependencies 节点下写入插件名以及版本号保存即可,flutter 会自行去下载安装

基本使用

dart
import 'package:flutter/material.dart';
// 引入 dio 插件
import 'package:dio/dio.dart';
class MyWidget2 extends StatefulWidget {
  @override
  State<MyWidget2> createState() => _MyWidget2State();
}

class _MyWidget2State extends State<MyWidget2> {
  /// 记录请求数据
  List _orderList = [];
  @override
  void initState() {
    getHttp();
    super.initState();
  }

  void getHttp() async {
    try {
      Response response =
          await Dio().get('https://mock.boxuegu.com/mock/1172/orders');
	// 修改状态更新组件
      setState(() {
        _orderList = response.data;
      });
    } catch (e) {
      print(e);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('dio'),
        ),
        body: Container(
          child: ListView.separated(
              separatorBuilder: ((context, index) {
                return Container(color: Colors.grey, height: 8);
              }),
              // 填充数据
              itemCount: _orderList.length,
              itemBuilder: ((context, index) {
                return Container(
                  height: 120,
                  color: Colors.red,
                  child: Text(_orderList[index]['name']),
                );
              })),
        ));
  }
}

封装 dio 单例类

在 lib/service/request.dart 写入

dart
import 'package:dio/dio.dart';

/// 网络请求单例类
class XTXRequestManager {
  // 声明dio
  Dio? dio;
  // 私有静态属性
  static XTXRequestManager? _instance;
  // 私有命名构造函数
  XTXRequestManager._initManager() {
    // 创建dio实例
    if (dio == null) {
      BaseOptions baseOptions = BaseOptions(
          // 配置连接超时时间和响应超时时间,单位都是毫秒
          connectTimeout: 15000,
          receiveTimeout: 5000,
          baseUrl: 'https://pcapi-xiaotuxian-front.itheima.net/');
      dio = Dio(baseOptions);
      dio!.interceptors.add(InterceptorsWrapper(
        // 请求拦截器
        onRequest: (options, handler) {
          // 设置请求头信息
          options.headers = {'Authorization': '', 'source-client': 'app'};
          return handler.next(options);
        },
        // 响应拦截器
        onResponse: (e, handler) {
          return handler.next(e);
        },
        // 异常拦截器
        onError: (e, handler) {
          return handler.next(e);
        },
      ));
    }
  }
  // 创建单例对象并向外界提供单例对象的方法
  factory XTXRequestManager() {
    // 只有当单例对象不存在时才创建
    _instance ??= XTXRequestManager._initManager();
    return _instance!;
  }
  // 封装处理请求的公共方法
  Future<Response> handleRequest(
    String path,
    String method, {
    data,
    Map<String, dynamic>? queryParameters,
  }) {
    return dio!.request(path,
        data: data,
        queryParameters: queryParameters,
        options: Options(method: method));
  }
}

新建 lib/service/homeApi.dart 写入

dart
import './request.dart';
import 'package:dio/dio.dart' show Response;
// 封装接口函数
Future<Response> index() => XTXRequestManager().handleRequest('home/index', 'get');

在组件中使用

dart
// 引入依赖及接口函数
import 'package:dio/dio.dart' show Response;
import '../../service/homeApi.dart' show index;

  void initState() {
    getHttp();
    super.initState();
  }

  getHttp() async {
    try {
      // 调用函数
      Response response = await index();
      print(response.data);
    } catch (e) {
      print(e);
    }
  }

捕获异常

dart
import 'package:dio/dio.dart' show Response, DioError;
try {
    Response userInfo =
        await login(_accountController.text, _passwodrController.text);
    // 捕获指定的异常(这里是捕获 dio 抛出的异常)
} on DioError catch (e) {
    EasyLoading.showToast(e.response!.data['message']);
}

无感刷新 token

dart
class XTXRequestManager {
  Dio? dio;
  static XTXRequestManager? _instance;
  XTXRequestManager._initManager() {
    if (dio == null) {
      BaseOptions baseOptions = BaseOptions(
          connectTimeout: 15000,
          receiveTimeout: 5000,
          baseUrl: 'https://pcapi-xiaotuxian-front.itheima.net/');
      dio = Dio(baseOptions);
      Map userInfo = {};
        // 异常拦截器
        onError: (e, handler) async {
          // 监听 401
          if (e.response!.statusCode == 401) {
            // 判断是否是刷新 token 接口出错
            if (e.requestOptions.path == 'login/refresh') {
              // 清空本地缓存
              await saveUserInfo({});
              // 在没有组件上下文的情况下跳转页面(还需在 main.dart 中做配置)
              navigatorKey.currentState!
                  .push(CupertinoPageRoute(builder: (context) {
                return const AccountLoginPage();
              }));
              // 以一个异常结束,而且异常不会再被 onError 拦截到
              return handler.reject(e);
            }
            Response tokenInfo =
                await refreshToken(userInfo['account'], userInfo['id']);
            await saveUserInfo(tokenInfo.data['result']);
            // 重新设置请求头
            e.requestOptions.headers['Authorization'] =
                'Bearer ${tokenInfo.data['result']['token']}';
            // 拿到新的token 再重新发起一次请求(刚因token失效调用失败的接口)
            Response res = await dio!.fetch(e.requestOptions);
            // 当成功后必须以 resolve 将此次正常的响应返回
            return handler.resolve(res);
          }
          return handler.next(e);
        },
      ));
    }
  }
}

项目中使用

实现底部导航栏

dart
import 'package:flutter/material.dart';
import '../cart/cart_page.dart';
import '../category/category_page.dart';
import '../home/home_page.dart';
import '../mine/minePage.dart';

class RootPage extends StatefulWidget {
  const RootPage({super.key});

  @override
  State<RootPage> createState() => _RootPageState();
}

class _RootPageState extends State<RootPage> {
  /// 页面列表
  List pages = [
    const HomePage(),
    const CategoryPage(),
    const CartPage(),
    const minePage()
  ];
  int _pageIndex = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: pages[_pageIndex],
      // 底部导航
      bottomNavigationBar:
          // 借助 Theme 去除长按和点击的默认背景色,Theme 组件用于自定义子组件主题样式
          Theme(
              data: ThemeData(
                  // 控制点击底部导航栏 icon 图标时的背景色
                  splashColor: Colors.transparent,
                  // 控制长按底部导航栏 icon 图标时的背景色
                  highlightColor: Colors.transparent),
              child: BottomNavigationBar(
                // 设置选中和未选中的图标 icon 和文字颜色
                selectedItemColor: Color(0xFF3CCEAF),
                unselectedItemColor: Color(0xFF383838),
                // 设置选中时的字体大小
                selectedFontSize: 10,
                // 设置未选中时的字体大小
                unselectedFontSize: 10,
                // 监听底部导航点击事件,可以得到当前点击图标的索引值
                onTap: (value) {
                  setState(() {
                    _pageIndex = value;
                  });
                },
                // 设置当前选中的图标项
                currentIndex: _pageIndex,
                // 设置底部导航样式
                type: BottomNavigationBarType.fixed,
                items: [
                  // 首页
                  BottomNavigationBarItem(
                      // 设置选中状态图标
                      // img 组件在加载新图时会默认先将旧图清空,再展示新图之前会有一段空白内容
                      // 给人感觉就是会“闪一下”,gaplessPlayback 属性设置为 true 时
                      // 可以不清空旧图
                      activeIcon: Image.asset('assets/log.jpg',gaplessPlayback: true),
                      // 设置未选中状态图标
                      icon: Image.asset('assets/log.jpg'),
                      // icon: Icon(Icons.home),
                      label: '首页'),
                  // 分类
                  BottomNavigationBarItem(
                      // 使用 flutter 内置的图标
                      icon: Icon(Icons.category), label: '分类'),
                  // 购物车
                  BottomNavigationBarItem(
                      icon: Icon(Icons.shopping_cart), label: '购物车'),
                  // 我的
                  BottomNavigationBarItem(
                      icon: Icon(Icons.person), label: '我的'),
                ],
              )),
    );
  }
}

自定义帧动画

dart
import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  /// 动画控制器
  late AnimationController _animationController;

  /// 动画对象
  late Animation _animation;
  @override
  void initState() {
    // 创建动画控制器
    _animationController =
        AnimationController(vsync: this, duration: Duration(seconds: 7));
    _animation = Tween(begin: 0.0, end: 6.0).animate(_animationController);
    _animationController.forward();
    //  _animationController.reset();
    // 监听动画刷新:flutter 动画每一秒中刷新60次(帧)
    _animationController.addListener(() {
      setState(() {});
    });
  }

  @override
  void dispose() {
    // 销毁动画
    _animationController.dispose();
    super.dispose();
  }

  // 准备帧动画内容
  List Frames = [
    Container(width: 20, height: 20, color: Colors.red),
    Container(width: 20, height: 20, color: Colors.orange),
    Container(width: 20, height: 20, color: Colors.yellow),
    Container(width: 20, height: 20, color: Colors.green),
    Container(width: 20, height: 20, color: Colors.cyan),
    Container(width: 20, height: 20, color: Colors.blue),
    Container(width: 20, height: 20, color: Colors.purple),
  ];
  @override
  Widget build(BuildContext context) {
    return Container(
        alignment: Alignment.center,
        // 渲染
        child: Frames[_animation.value.floor()]);
  }
}

打印

轮播图插件

flutter_swiper_null_safety: ^1.0.2 安装插件

dart
import 'package:flutter/material.dart';
import 'package:flutter_swiper_null_safety/flutter_swiper_null_safety.dart';

class imageSwiperWidget extends StatelessWidget {
  final List? imageBanners;
  final double? height;
  final double? borderRadius;
  imageSwiperWidget(
      {this.imageBanners, this.height = 140, this.borderRadius = 4});
  @override
  Widget build(BuildContext context) {
    return imageBanners != null
        ? Container(
            decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(borderRadius!)),
            // 用于当父元素设置圆角时同时切割子元素
            clipBehavior: Clip.antiAlias,
            height: height,
            // 使用时必须给 Swiper 设置高度
            child: Swiper(
              // 设置指示器
              // pagination: SwiperPagination(), 使用插件提供的默认指示器
              pagination: SwiperPagination(
                  // 使用自定义指示器
                  builder: CustomSwiperPagination()),
              // 指定自动播放
              autoplay: true,
              // 指定元素个数
              itemCount: imageBanners!.length,
              // 指定每个元素
              itemBuilder: (context, index) {
                return Image.network(
                  imageBanners![index]['imgUrl'],
                  fit: BoxFit.cover,
                );
              },
            ),
          )
        : Container(
            height: height,
            color: Color(0xFFEBEBEB),
          );
  }
}

// 自定义轮播图指示器
// 继承插件提供的 SwiperPlugin 可以拿到轮播图的信息,方便实现
class CustomSwiperPagination extends SwiperPlugin {
  @override
  Widget build(BuildContext context, SwiperPluginConfig config) {
    // config 可以提供指示器元素个数以及当前展示的指示器索引
    int itemCount = config.itemCount;
    int activeIndex = config.activeIndex;
    // 存放指示器
    List<Widget> items = [];
    // 循环创建指示器元素
    for (var i = 0; i < itemCount; i++) {
      bool isActive = i == activeIndex;
      items.add(Container(
        margin: EdgeInsets.only(left: 3, right: 3),
        width: 13,
        height: 3,
        // white60 表示半透明
        color: isActive ? Colors.white : Colors.white60,
      ));
    }
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: items,
    );
  }
}

瀑布流插件

yaml
flutter_staggered_grid_view: ^0.4.0 #安装插件
dart
Widget _buildItem(List CategoryGrids) {
    return StaggeredGridView.countBuilder(
        // 表示横向上要排列多少个元素
        // 最终每一行展示出的个数 = crossAxisCount / StaggeredTile.fit(1) = 5
        crossAxisCount: 5,
        // 设置一共有多少个元素
        itemCount: CategoryGrids.length,
        // 设置纵向上每个元素之间的距离
        mainAxisSpacing: 18,
        // 禁用滚动
        physics: NeverScrollableScrollPhysics(),
        // shrinkWrap 搭配 physics 解决滚动视图之间的滚动冲突
        shrinkWrap: true,
        // 指定每个元素
        itemBuilder: (context, index) {
            return Column(
                children: [
                    Image.network(CategoryGrids[index]['picture']),
                    Text(CategoryGrids[index]['name'])
                ],
            );
        },
        // 表示纵向上需要排列多少个元素
        staggeredTileBuilder: ((index) {
            return StaggeredTile.fit(1);
        }),
    );
}

轻提示插件

yaml
flutter_easyloading: ^3.0.3
dart
// 在 main.dart 文件中初始化插件
import 'package:flutter_easyloading/flutter_easyloading.dart';

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const RootPage(),
      // 初始化插件
      builder: EasyLoading.init(),
    );
  }
}
dart
// 在组件中使用
import 'package:flutter_easyloading/flutter_easyloading.dart';
EasyLoading.showToast('用户名密码必填!');

获取当前设备信息

dart
@override
Widget build(BuildContext context) {
    // 可以通过 context 拿到当前设备的一些信息
    // 获取屏幕宽度
    double screenWidth = MediaQuery.of(context).size.width;
    // 计算分类图标的宽度
    _ImageWidth = (screenWidth - 6 * 16.0) / 5;
}

UI 或文本溢出

dart
// 文本用 TextOverflow
// Row或Column溢出可将Row或Column替换为 Wrap 组件,Wrap可以将溢出的部分换行显示
return Scaffold(
	resizeToAvoidBottomInset:false // 可以通过设置这个属性防止键盘覆盖内容或者键盘撑起内容
)

不规则屏幕的处理方式

dart
// 通过 MediaQuery 即可获取不规则屏幕的范围
Widget build(BuildContext context) {
    // MediaQuery.of(context).padding.top 某些设备顶部不规则情况(苹果刘海屏)
    // 底部操作栏的高度:自身高度 + 不规则屏幕底部间距
    _bottomBarHeight = 60.0 + MediaQuery.of(context).padding.bottom;
  }

自定义顶部 appBar

dart
class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
    	backgroundColor: const Color(0xFFF7F7F8),
      	appBar: HomeAppBar(),
    );
  }
}

// 自定义 appBar 必须实现  PreferredSizeWidget 接口
class HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
  // 用于指定顶部 appBar 的高度 kToolbarHeight 默认是 56
  @override
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);
  @override
  Widget build(BuildContext context) {
    return Container(
        // MediaQuery.of(context).padding.top 获取顶部状态栏高度
        height: MediaQuery.of(context).padding.top + kToolbarHeight,
        color: const Color(0xFF00b796),
        padding: const EdgeInsets.only(left: 18, right: 18),
        // SafeArea 组件用于解决不规则屏幕(刘海屏)遮挡内容的问题
        child: SafeArea(
            child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            const Icon(Icons.home),
            Expanded(
                child: Container(
              padding: const EdgeInsets.only(left: 18, right: 18),
              height: 30,
              color: Colors.orange,
            )),
            const Icon(Icons.home),
          ],
        )));
  }
}
dart
import 'dart:io';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
  // 注意运行在浏览器端时可能会报错,这里用 try catch 包裹
  try {
    if (Platform.isAndroid) {
      SystemChrome.setSystemUIOverlayStyle(
          const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
    }
  } catch (e) {
    debugPrint('$e');
  }
}

图片插件

cached_network_image 插件可以快速实现图片缓存

dart
cached_network_image: ^3.1.1 // 安装
dart
import 'package:cached_network_image/cached_network_image.dart';
// 使用起来和原生 image 组件差不多
CachedNetworkImage(
    imageUrl: hotRecommends['leftIcon'],
    width: 100,
    height: 100,
    fit: BoxFit.cover,
    // 设置当图片还没加载完成时的占位图
      placeholder: (context, url) {
        return Container(
          width: width,
          height: height,
          color: const Color(0xFFEBEBEB),
        );
      },
      // 当图片加载失败时的占位图
      errorWidget: (context, url, error) {
        return Container(
          width: width,
          height: height,
          color: const Color(0xFFEBEBEB),
        );
      },
),

下拉刷新

dart
pull_to_refresh: ^2.0.0 // pull_to_refresh 可以快速实现下拉刷新和上拉加载
dart
import 'package:pull_to_refresh/pull_to_refresh.dart';

class _HomePageState extends State<HomePage>  {
  // 刷新数据插件的控制器
  late RefreshController _refreshConttroller;
  @override
  void initState() {
    // 创建刷新数据插件的控制器
    _refreshConttroller = RefreshController();
    getHttp();
    super.initState();
  }

  getHttp() async {
    try {
      Response response = await index();
      // 当网络请求完成时告诉插件已完成刷新
      _refreshConttroller.refreshCompleted();
    } catch (e) {
      debugPrint('$e');
      // 网络请求出错时
      _refreshConttroller.refreshFailed();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: const Color(0xFFF7F7F8),
        appBar: HomeAppBar(),
        // SmartRefresher 用于实现加载
        body: SmartRefresher(
          // 表示控制器,控制是否刷新成功
          controller: _refreshConttroller,
          // 是否允许下拉刷新
          enablePullDown: true,
          // 是否允许下拉加载
          enablePullUp: true,
          // 下拉刷新执行的回调
          onRefresh: () {
            getHttp();
          },
           // 下拉加载回调
          onLoading: _onLoading,
          // 下拉刷新时的加载动画,CustomHeader 用于自定义动画
          header: CustomHeader(
            builder: (context, mode) {
              String refreshText = '下拉刷新';
              // 这时表示用户刚刚往下拉时(空闲)
              if (mode == RefreshStatus.idle) {
                refreshText = '下拉刷新';
              } else if (mode == RefreshStatus.canRefresh) {
                // 这里表示用户已经拉到一定距离(可以刷新)
                refreshText = '松手刷新';
              } else if (mode == RefreshStatus.refreshing) {
                // 这里表示用户已经松手(正在刷新)
                refreshText = '刷新中';
              } else if (mode == RefreshStatus.completed) {
                // 这里表示刷新成功
                refreshText = '刷新成功';
              } else if (mode == RefreshStatus.failed) {
                // 这里表示刷新失败
                refreshText = '刷新失败';
              } else {
                // 这里处理一下其他情况
                refreshText = '再刷新看看吧';
              }
              return Container(
                alignment: Alignment.center,
                child: Text(refreshText),
              );
            },
          ),
          // 下拉加载动画
          footer: CustomFooter(
          builder: (BuildContext context,LoadStatus mode){
            Widget body ;
            if(mode==LoadStatus.idle){
              body =  Text("pull up load");
            }
            else if(mode==LoadStatus.loading){
              body =  CupertinoActivityIndicator();
            }
            else if(mode == LoadStatus.failed){
              body = Text("Load Failed!Click retry!");
            }
            else if(mode == LoadStatus.canLoading){
                body = Text("release to load more");
            }
            else{
              body = Text("No more Data");
            }
            return Container(
              height: 55.0,
              child: Center(child:body),
            );
          },
        ),
          // 其子组件必须是一个可滚动的组件
          child: CustomScrollView(
            slivers: [],
          ),
        ));
  }
}

回到顶部

dart
// 定义控制器
late ScrollController? _scrollController;
  void initState() {
	// 初始化滚动控制器
    _scrollController = ScrollController();
  }
  void dispose() {
    // 销毁控制器
    _scrollController?.dispose();
  }
@override
Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: const Color(0xFFF7F7F8),
        appBar: HomeAppBar(),
        body: CustomScrollView(
        // 使用滚动控制器
        controller: _scrollController,
            slivers: [
                SliverToBoxAdapter(
                        child: GestureDetector(
                            onTap: () {
                              // 400 毫秒后以先快后慢的动画回到顶部
                              _scrollController!.animateTo(0,
                                  duration: const Duration(milliseconds: 400),
                                  curve: Curves.ease);
                            })
            ])
}

缓存页面

第一步:改写根组件切换方式

dart
class RootPage extends StatefulWidget {
  const RootPage({super.key});

  @override
  State<RootPage> createState() => _RootPageState();
}

class _RootPageState extends State<RootPage> {
  // PageView 控制器
  PageController? _controller;
  /// 页面列表
  List<Widget> pages = [
    const HomePage(),
    const CategoryPage(),
    const CartPage(),
    const minePage()
  ];
  @override
  void initState() {
    _controller = PageController();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // body: pages[_pageIndex],
      // 将 PageView 包裹组件并由它来控制切换
      body: PageView(
        // 禁用滚动
        physics: const NeverScrollableScrollPhysics(),
        controller: _controller,
        children: pages,
      ),
      bottomNavigationBar:
          Theme(
                // 监听底部导航点击事件,可以得到当前点击图标的索引值
                onTap: (i) {
                  setState(() {
                    // 控制 PageView 跳转到指定页面
                    _controller!.jumpToPage(i);
                    _pageIndex = i;
                  });
                },
                currentIndex: _pageIndex,
                items: const [
                  BottomNavigationBarItem(
                      icon: Icon(Icons.home),
                      label: '首页'),
                  BottomNavigationBarItem(
                      icon: Icon(Icons.category), label: '分类'),
                  BottomNavigationBarItem(
                      icon: Icon(Icons.shopping_cart), label: '购物车'),
                  BottomNavigationBarItem(
                      icon: Icon(Icons.person), label: '我的'),
                ],
              )),
    );
  }
}

第二步:在需要缓存的组件使用 AutomaticKeepAliveClientMixin

dart
class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
  @override
  // 在需要缓存的组件混入 AutomaticKeepAliveClientMixin 并重写 wantKeepAlive 属性
  // 还需要调用父类的 build 方法
  // true 代表缓存该组件
  bool get wantKeepAlive => true;
  @override
  Widget build(BuildContext context) {
    // 缓存组件需要调用父类的 build 方法
    super.build(context);
  }
}

获取组件信息

dart
class _CategoryPageState extends State<CategoryPage> {
  // 定义 GlobalKey ,debugLabel 必须是一个全局惟一的字符串
  final GlobalKey _primaryCategoryGlobalKey =
      GlobalKey(debugLabel: 'primaryCategoryGlobalKey');
  }
  void _primaryCategoryOnTap(index) {
      // 获取组件位置
     // RenderBox box = _keys[i].currentContext?.findRenderObject() as RenderBox;
    // 表示从组件左上角取位置,dy表示获取纵坐标
    // box.localToGlobal(Offset.zero).dy;

    // 根据 GlobalKey 获取组件信息(这里获取组件高度信息)
    RenderBox box = _primaryCategoryGlobalKey.currentContext?.findRenderObject()
        as RenderBox;
    double primaryCategoryHeight = box.size.height;
  }

  Widget _buildPrimaryCategory() {
    return Container(
        child: ListView.builder(
            // 将 key 绑定到指定组件
            key: _primaryCategoryGlobalKey));
  }

  @override
  void initState() {
    // 初始化控制器
    _primaryController = ScrollController();
    super.initState();
  }

  @override
  void dispose() {
    // 清除控制器
    _primaryController?.dispose();
    super.dispose();
  }

本地缓存插件

yaml
shared_preferences: ^2.0.7
dart
import 'package:shared_preferences/shared_preferences.dart'
    show SharedPreferences;
import 'dart:convert' show json;

Future<bool> saveUserInfo(Map data) async {
  // 获取 SharedPreferences 单例
  SharedPreferences pre = await SharedPreferences.getInstance();
  // 存储数据,json.encode 将数据转化为 json 字符串
  return await pre.setString('userInfo', json.encode(data));
  // 读取本地存储并将 json 字符串转换为字典
  // json.decode(pre.getString('userInfo'));
}

内置图标库

https://fluttericon.cn

定时器

dart
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
    if (_time <= 0) {
        // 清除定时器
        _timer.cancel();
        return;
    }
});

GlobalKey 使用

子组件

dart
// 定义 GlobalKey,debugLabel 的值以一个全 app 唯一的 值
GlobalKey<_GoodsDetailAppBarState> goodsDetailAppBarKey =
    GlobalKey(debugLabel: 'goodsDetailAppBarKey');

class GoodsDetailAppBar extends StatefulWidget {
  // 声明 key
  const GoodsDetailAppBar({Key? key}) : super(key: key);
  @override
  State<GoodsDetailAppBar> createState() => _GoodsDetailAppBarState();
}
class _GoodsDetailAppBarState extends State<GoodsDetailAppBar> {
    // 子组件方法
    getGoodsDetailAppBarOpacity(double opacity) {
        if (myopacity != 1 - opacity) {
            setState(() {
                myopacity = 1 - opacity;
            });
        }
    }
}

父组件

dart
class GoodsDetailPage extends StatefulWidget {
  @override
  State<GoodsDetailPage> createState() => _GoodsDetailAppBarState();
}
class _GoodsDetailPageState extends State<GoodsDetailPage> {
  _goodsDetailOnscroll(double offset) {
    // 直接调用子组件方法
    goodsDetailAppBarKey.currentState?.getGoodsDetailAppBarOpacity(opacity);
  }
      @override
    Widget build(BuildContext context) {
        // 传递 key(这个为子组件定义的 GlobalKey)
        return GoodsDetailBottomBar(key: goodsDetailAppBarKey)
    }
}