Skip to content

介绍

官方文档 flutter.cn

社区文档 book.flutterchina.club

目录结构

主文件介绍

runApp 函数

dart
void main() {
  // runApp 函数是 Flutter 内部提供的一个函数,启动一个 Flutter 应用就是从调用这个函数开始的
  runApp(const MyApp());
}

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

flutter 混合开发

flutter 工程模式

分为如下五种:

1.Flutter App:构建一个标准FlutterApp(统一管理模式),包含 Dart 层和平台层(iOS/Android)

2.Flutter Module:创建一个Flutter 模块(三端分离模式,即混合开发),以模块的形式分别嵌入原生项目

3.Flutter Package:纯 Dart 插件工程,不依赖 Flutter,仅包含Dart层的实现,通常用来定义一些公共库

4.Flutter Plugin:flutter 平台插件,包含 Dart 层与 Native 平台层的实现

5.Flutter Skeleton:自动生成 Flutter 模板,提供常用框架

创建

bash
# --template=type 指定不同的项目类型,有五种,如上面所述。
# --platforms=ios/android/windows/linux/macos/web 指定平台
# 创建 Flutter Module 模式用于混合开发
flutter create --template=module --platforms=android,ios helloiFlutter

flutter 与原生通信

混合开发中需要 flutter 消息通道来与原生语言来进行互相调用。

BasicMessageChannel

用于传递字符串和半结构化的信息

dart
// 创建 BasicMessageChannel 实例,用于后续与原生通信
// BasicMessageChannel 名称要与原生完全一致
static const _channel = BasicMessageChannel('messageChannel',StringCodec());

void initState() {
    super.initState();
    // 通过 BasicMessageChanneL 实例,注册一个接收回调,并且返回信息。
    _channel.setMessageHandLer((message) async {
        // 接收原生发送过来的字符串消息
        print('receive message: $message');
    });
}

Future<void>_sendMessage() async {
    // 向原生发送信息
    String? message = await _chonnel.send('Hello from dart');
    print(message);
}

EventChannel

用于数据流(event streams)的通信。和 BasicMessageChannel 类似。

dart
static const _channel = EventChannel('eventChannel');

void initState() {
    super.initState();
    // 通过 BasicMessageChanneL 实例,注册一个接收回调,用于接收消息。
    _channel.receiveBroadcastStream().listen((){
        (dynamic event) {
            print('Receive event: $event');
        };
    });
}

MethodChannel

此种方式最为常用,用于传递方法调用。

dart
static const _channel = MethodChannel('methodchannel');

void initState() async {
    super.initState();
    // 调用原生端准备好的方法并传递参数,再从函数中获取原生端的返回值
    String res = await _channel.invokeMethod('getFLutterInfo',{'name':'张三'});
}

组件

无状态组件

纯展示型组件,没有用户交互操作

dart
// 当组件被创建或者父组件状态发生变化导致其需要重新构建时,build 方法会被调用。组件才会更新
// 需要继承 StatelessWidget 类
class MainPage extends StatelessWidget {
    // 无状态组件里的数据都是不可变的,所以必须使用 final 定义
    final String title = '无状态组件';
    // 无状态组件需要重写 build 方法
    @override
    Widget build(BuildContext context) {
        return MaterialApp(
            // 使用变量
            title: title,
            home: Scaffold(
                appBar: AppBar(title: Center(child: Text('头部区域'))),
                body: Container(child: Center(child: Text('中部区域'))),
                bottomNavigationBar: Container(
                    height: 80,
                    child: Center(child: Text('底部区域')),
                ),
            ),
        );
    }
}

有状态组件

有状态组件是构建动态交互界面的核心,能够管理变化的内部状态,当状态改变时,组件会更新显示内容

dart
void main() {
  runApp(MainPage());
}
// 需要创建两个类
// 这个类需要继承 StatefulWidget 类,用于创建一个有状态组件
class MainPage extends StatefulWidget {
    @override
    State<StatefulWidget> createState() => _MainPageState();
}

// 这个类需要继承 State 类,用于构建视图和处理业务逻辑
// 并且类名需要以 _ 开头,表示这个类是私有的
class _MainPageState extends State<MainPage> {
    @override
    Widget build(BuildContext context) {
        return MaterialApp(
            title: '有状态组件',
            home: Scaffold(
                appBar: AppBar(title: Center(child: Text('头部区域'))),
                body: Container(child: Center(child: Text('中部区域'))),
                bottomNavigationBar: Container(
                    height: 80,
                    child: Center(child: Text('底部区域')),
                ),
            ),
        );
    }
}

生命周期

无状态组件只有一个生命周期钩子就是 build 函数。

有状态组件常用声明周期钩子函数如下:

dart
// 在编译器里写入 st 即可唤出快捷创建组件代码块
class MainPage extends StatefulWidget {
    @override
    State<StatefulWidget> createState() {
        print('createState 执行,第一个执行');
        return _MainPageState();
    }
}

class _MainPageState extends State<MainPage> {
    void initState() {
        print('initState 执行,第二个执行');
    }

    @override
    void didChangeDependencies() {
        print('didChangeDependencies 执行,第三个执行');
        super.didChangeDependencies();
    }

    @override
    void didUpdateWidget(covariant MainPage oldWidget) {
        print('didUpdateWidget 执行');
        super.didUpdateWidget(oldWidget);
    }

    @override
    void deactivate() {
        print('deactivate 执行');
        super.deactivate();
    }

    @override
    void dispose() {
        print('dispose 执行');
        super.dispose();
    }

    @override
    Widget build(BuildContext context) {
        print('build 执行,第四个执行');
        return MaterialApp(title: '有状态组件', home: Scaffold());
    }
}

详细生命周期函数如下:

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				从其他页面退回到当前页

setState

执行 setState 方法会让组件 build 函数重新执行,进而更新页面。

dart
class _MainPageState extends State<MainPage> {
    // 定义变量
    int num = 0;

    @override
    Widget build(BuildContext context) {
        return MaterialApp(
            title: '有状态组件',
            home: Scaffold(
                body: TextButton(
                    onPressed: () {
                        // setState 是异步的,只要调用此函数(即使函数体内没有任何逻辑)组件就会更新
                        setState(() {
                            num++;
                        });
                    },
                    // 使用变量
                    child: Text(num.toString()),
                ),
            ),
        );
    }
}

Material 组件库

是 Flutter 内置的一套独有的设计风格,里面有很多拆箱可用的 widget

基础组件

MaterialApp

整个应用被 MaterialApp 包裹,方便我们对整个应用的属性进行整体设计

dart
void main() {
  runApp(MaterialApp(
      // 用来展示窗口的标题内容
      title: 'Flutter',
      // 来设置整个应用的主题
      theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple)),
      // 来展示窗口的主体内容
      home: Scaffold(),
    ));
}

Scaffold

用于构建 Material Design 风格页面的核心布局组件,提供标准、灵活配置的页面骨架。常用属性如下:

appBar:页面顶部的应用栏,通常用于显示标题、导航按钮和操作菜单

body:页面的主要内容区域,可以放置任何其他组件,是页面的核心

bottomNavigationBar:底部导航栏,方便用户在不同核心功能页面间切换

backgroungColor:设置整个Scaffold的背景颜色

floatingActionButton:悬浮操作按钮,常用于触发页面的主要动作

dart
// 如下代码就会构建一个分为上中下结构的页面
void main() {
    runApp(
        MaterialApp(
            title: 'Flutte',
            home: Scaffold(
                appBar: AppBar(title: Center(child: Text('头部区域'))),
                body: Container(child: Center(child: Text('中部区域'))),
                // 底部栏目
                bottomNavigationBar: BottomNavigationBar(
                    // 是否展示未被选中栏目的文本
                    showUnselectedLabels: true,
                    // 未选中栏目的文字颜色
                    unselectedItemColor: const Color(0x009e9e9e),
                    // 选中栏目的文字颜色
                    selectedItemColor: Colors.black,
                    // 当前选中的栏目索引
                    currentIndex: 0,
                    // 底部导航栏的布局行为和视觉表现
                    // fixed 会强制所有导航项的宽度均等分配
                    type: BottomNavigationBarType.fixed,
                    // 切换栏目的回调函数
                    onTap: (value) {},
                    // 栏目列表
                    items: [
                        BottomNavigationBarItem(
                            icon: Icon(Icons.home_filled),
                            activeIcon: Icon(Icons.home),
                            label: '首页',
                        )
                    ],
                )
            ),
        ),
    );
}

AppBar组件

用于实现顶部应用栏。

dart
Scaffold(
    appBar: AppBar(
        title: Text('登陆'),
        backgroundColor: Colors.amber,
        // 标题是否居中显示,设置为 false 时标题会居左显示
        centerTitle: false,
        // 左侧按钮
        leading: IconButton(
            icon: const Icon(Icons.arrow_back),
            onPressed: () => Navigator.pop(context), // 返回上一页
        ),
        // 右侧按钮
        actions: [TextButton(onPressed: () {}, child: Text('右侧按钮'))],
    )
)

自定义 AppBar

dart
Scaffold(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),
            // SafeArea 组件用于解决不规则屏幕(刘海屏)遮挡内容的问题
            child: SafeArea());
    }
}

堆叠容器组件

会保留所有子组件的状态并仅显示指定索引的子组件,可用于构建首页选项卡切换场景。

页面切换时无需重新构建,切换流畅无卡顿,适合需要快速切换且保持状态的场景,但子组件过多或复杂时,可能导致内存压力和初始加载延迟

注意:IndexedStack 组件会一次性渲染所有子组件且切换页面后所有子页面都不会销毁

dart
int _currentIndex = 0;

Scaffold(
    body: SafeArea(
        child: IndexedStack(
            index: _currentIndex,
            children: [Text('首页'), Text('分类'), Text('购物车'), Text('我的')],
        ),
        bottomNavigationBar: BottomNavigationBar(
            // 栏目列表
            items: [
                BottomNavigationBarItem(label: '首页'),
                BottomNavigationBarItem(label: '分类'),
                BottomNavigationBarItem(label: '购物车'),
                BottomNavigationBarItem(label: '我的')
            ],
        )
    )
);

PageView 组件

同样可用于构建首页选项卡切换场景。

仅渲染当前页面,滑动时按需加载,可配合 AutomaticKeepAliveClientMixin 缓存页面状态,缓存的页面不会销毁,再次切换时不会重新构建。

dart

安全区域组件

用于解决设备屏幕边缘安全区域的适配问题,不同设备的非展示区域不一致如全面屏、刘海屏等

dart
// SafeArea 本质上是 MediaQuery 的封装
// 在组件中可通过 MediaQuery.of(context).padding 获取安全区域的内边距

Scaffold(
    body: SafeArea(
        // 有 top、bottom、left、right 四个属性控制是否在对应方向添加安全区域内边距默认均为 true
        left: false,
        // 指定最小内边距(例如避免内容过于靠近边缘)
        minimum: EdgeInsets.only(left: 20),
        child: Center(child: Text("内容不会进入刘海区域")),
);

事件组件

用户与应用程序交互时触发的各种动作,比如触摸屏幕、滑动、点击等

使用 GestureDetector 包裹需要被触发事件的元素即可

dart
GestureDetector(
    onTap: () {
        print('点击轻触事件');
    },
    child: Text('事件')
)

flutter 还有许多其他组件也提供事件,如:TextButton、Inkwell、Checkbox 等

dart
// 会渲染一个按钮,自带点击事件
TextButton(
    onPressed: () {
        print('按钮点击事件');
    },
    child: Text('中部区域'),
)

Container

是一个拥有绘制、定位、调整大小的组件

dart
Container(
    width: 200,
    height: 200,
    // 设置内边距,这里设置的是四个方向
    padding: EdgeInsets.all(20),
    // 设置外边距,这里设置的是四个方向
    margin: EdgeInsets.all(20),
    // 设置子元素水平垂直居中
    alignment: Alignment.center,
    // 设置旋转,这里设置的是弧度不是角度,所以0.06表示36度
    transform: Matrix4.rotationZ(0.06),
    decoration: BoxDecoration(
        // 设置背景色
        color: Colors.blue,
        // 设置圆角
        borderRadius: BorderRadius.circular(15),
        // 设置边框颜色
        border: Border.all(color: Colors.amber, width: 5),
        // 设置背景图
        image: DecorationImage(
            // AssetImage() 可设置本地背景图
            image: NetworkImage('图片地址'),
            fit: BoxFit.cover,
        ),
    ),
    child: Text(
        'Hello Container',
        style: TextStyle(color: Colors.white, fontSize: 20),
    ),
)

Center

水平、垂直剧中:将其子组件在父容器的空间内进行水平和垂直方向上的居中排列

默认占满父容器:Center不能设置宽高,Center的最终大小取决于其父组件传递给它的约束,Center会向它的父组件申请尽可能大的空间

dart
Center(child: Text('Hello World'))

Text

在用户界面中显示文本的基础组件

dart
Text(
    'Text 组件',
    // 文本在容器内的水平对齐方式,如.left,.center
    textAlign: TextAlign.center,
    // 文本显示的最大行数
    maxLines: 2,
    // 文本超出后的展示方式,ellipsis 表示省略号
    overflow: TextOverflow.ellipsis,
    // 文本样式
    style: TextStyle(
        // 字体大小
        fontSize: 30,
        // 字体颜色
        color: Colors.red,
        // 字体粗细
        fontWeight: FontWeight.bold,
        // 字体斜体
        fontStyle: FontStyle.italic,
        // 字体下划线
        decoration: TextDecoration.underline,
        // 字体下划线颜色
        decorationColor: Colors.blue,
    ),
)

Text/TextSpan

如果需要在同一段文本中显示不同样式,可用 Text.rich 配合 TextSpan 来实现

dart
Text.rich(
    TextSpan(
        text: '第一段文字',
        style: TextStyle(color: Colors.red, fontSize: 30),
        children: [
            TextSpan(
                text: '第二段文字',
                // 此段文本的字体大小会继承父组件设置的字体大小,也就是 30
                style: TextStyle(color: Colors.blue),
            ),
        ],
    ),
)

输入框组件

实现文本输入功能的核心组件,使用它时必须是一个有状态组件

dart
// 定义文本编辑器控制器 _controller.text 获取文本内容
// _controller.clear() 清空内容
final TextEditingController _controller = TextEditingController();

TextField(
    // 文本编辑器控制器,用于获取、设置文档内容及监听变化
    controller: _controller,
    // 输入框内容变化时触发此函数,函数参数能获取当前的输入内容
    onChanged: (value) {
        print(value);
    },
    // 输入框内容提交时触发此函数,函数参数能获取当前的输入内容
    onSubmitted: (value) {
        print(value);
    },
    // 光标设置
    cursorRadius: const Radius.circular(10),
    cursorColor: const Color(0xFFFFBD3B),
    // 表单类型
    keyboardType: TextInputType.text,
    // 默认为 false 设置为 true 时不显示实际内容,输入的内容会密文展示。即密码框
    obscureText: true,
    // 最大字符数
    maxLength: 5,
    // 最大行数,默认为 1
    maxLines: 1,
    // 输入文本的样式
    style: TextStyle(color: Colors.red),
    // 设置输入框样式
    decoration: InputDecoration(
        // 输入框内边距
        contentPadding: EdgeInsets.only(left: 20),
        // 输入框提示文本
        hintText: '请输入',
        // 输入框提示文本样式
        hintStyle: TextStyle(color: Colors.red),
        // 辅助提示信息,可用于校验表单时展示在输入框下面的提示文字
        helperText: '',
        // 辅助提示信息样式
        helperStyle: TextStyle(color: Colors.red),
        // 输入框填充颜色,即背景色
        fillColor: Colors.amber,
        // 设置背景色时必须设置 filled 为 true,表示允许展示填充颜色
        filled: true,
        border: OutlineInputBorder(
            // 设置输入框边框大小,none 为无边框
            borderSide: BorderSide.none,
            // 设置圆角
            borderRadius: BorderRadius.circular(10),
        ),
    ),
)

表单组件

Form 组件配合 TextFormField 组件可实现表单校验

dart
// 定义 GlobalKey 用于表单校验
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

Form(
    key: _formKey,
    child: Column(
        children: [
            // 密码框
            TextFormField(
                // 表单校验
                validator: (value) {
                    if (value == null || value.isEmpty) {
                        // 这里返回的字符会显示在输入框下方
                        return '密码不能为空';
                    }
                    // 使用正则校验
                    if (!RegExp(r"^1[3-9]\d{9}$").hasMatch(value)) {
                        return "手机号格式不正确";
                    }
                    return null;
                },
                controller: _phoneController,
                // 是否密文,即密码框
                obscureText: true,
                // 设置样式
                decoration: InputDecoration(
                    // 内容内边距
                    contentPadding: EdgeInsets.only(left: 20),
                    // 输入框提示文字
                    hintText: '请输入密码',
                    // 输入框填充颜色
                    fillColor: const Color.fromRGBO(243, 243, 243, 1),
                    filled: true,
                    border: OutlineInputBorder(
                        // 设置外边框
                        borderSide: BorderSide.none,
                        borderRadius: BorderRadius.circular(25),
                    ),
                ),
            ),
            // 单选框
            Checkbox(
                value: _isChecked,
                // 选中状态发生变化时触发
                onChanged: (bool? value) {
                    setState(() {
                        _isChecked = value ?? false;
                    });
                },
            ),
            // 按钮
            ElevatedButton(
                // 点击后触发
                onPressed: () {
                    // 根据 GlobalKey 调用 Form 组件的表单校验方法触发校验
                    if (_formKey.currentState!.validate()) {
                        // 校验通过
                    }
                },
                // 按钮样式
                style: ElevatedButton.styleFrom(),
                child: Text('登录')
            )
        ],
    ),
)

分割线组件

用于在界面中创建水平分隔线以区分内容区块。

dart
Divider(
    // 线条高度(含上下间距)
    height: 5,
    // 线条颜色
    color: Colors.grey,
    // 线条厚度
    thickness: 2,
    // 左侧缩进
    indent: 20,
    // 右侧缩进
    endIndent: 20
)

Align、Icon

可以精确控制其子组件在父容器空间内的对齐位置

Center 组件是 Align 组件的一个特例,继承自 Align,相当于一个将 alignment 属性为居中的 Align.center

dart
Align(
    // 设置子元素对其方式
    alignment: Alignment.center,
    // 宽度因子,Align 组件的宽度是子元素宽度的 n 倍,这里是 1 倍
    widthFactor: 1,
    // 高度因子,Align 组件的高度是子元素高度的 n 倍,这里是 1 倍
    heightFactor: 1,
    // Icon 组件,flutter 内置了许多 icon 图标,可使用 Icon 组件使用
    child: Icon(Icons.star, size: 150, color: Colors.yellow),
)

Padding

功能单一而纯粹,就是添加内边距。如果需求仅是为组件添加间距,那么直接使用 Padding 组件

dart
Padding(padding: EdgeInsets.all(20), child: Text('Padding 组件'))

Image

在用户界面中显示图片的核心部件

图片分类:

1)Image.asset():加载项目资源目录(assets)中的图片。需要在 pubspec.yaml 文件中声明资源路径

2)Image.network():直接从网络地址加载图片

3)Image.file():加载设备本地存储中的图片文件

4)Image.memory():加载内存中的图片数据

asset

1.先在 pubspec.yaml 文件声明资源路径

yaml
flutter:
  assets:
  # 设置本地资源路径
    - lib/images/
dart
Image.asset(
    // 图片路径
    'lib/images/7.png',
    width: 50,
    height: 50,
    // 控制图片如何适应其显示区域(父组件区域),例如是否拉伸、裁剪或保持原比例
    fit: BoxFit.none,
    // 图片对齐方式
    alignment: Alignment.center,
    // 当图片小于显示区域(父组件区域)时,设置是否以及如何重复平铺图片
    repeat: ImageRepeat.repeat,
    // Image 组件在加载新图片时默认先将旧图片清空,所以在新图加载展示完成之前会有一段空白内容,会造成 “闪一下”的问题
    // gaplessPlayback 设置为 true 可以指定在加载新图时不清空旧图直到新图展示出来为止,可以解决上述问题
     gaplessPlayback: true,
)

network

dart
// 加载网络图片
Image.network(
    // 当加载网络图片失败时会执行此函数
    errorBuilder: (context, error, stackTrace) {
        // 可在此函数返回一个默认图片进行展示
        return Image.asset('');
    },
    '网络地址'
)

裁切组件

用于将子组件裁剪为圆角矩形的组件,常用于实现圆角图片、按钮、卡片等视觉效果

dart
ClipRRect(
    // 圆角裁切 circular() 表示统一裁切(四个角)
    // only() 表示只裁切某一个角或多个角
    borderRadius: BorderRadius.circular(8),
    child: Image.network(
        '图片路径',
        width: 100,
        height: 140,
        fit: BoxFit.cover
    )
)

下拉刷新组件

RefreshIndicator 可快速实现下拉刷新的功能

需要包裹可滚动组件

dart
RefreshIndicator(
    // 指示器颜色
    color: Colors.blue,
    // 背景色
    backgroundColor: Colors.grey[100],
    // 触发距离
    displacement: 50,
    onRefresh: () async {
        print('触发下拉刷新');
    },
    child: CustomScrollView(slivers: [SliverToBoxAdapter()])
)

消息提示组件

在屏幕底部显示短暂的消息(如操作反馈、提示信息),支持自定义内容、操作按钮和自动隐藏

dart
// 可封装为工具函数,方便外部直接调用
// 定义当前是否有提示框在展示,避免同时出现多个提示框
bool _isShowLoading = false;

msg(BuildContext context, {String msg = '刷新成功', int seconds = 2}) {
    if (_isShowLoading) return;
    
    _isShowLoading = true;
    // 添加定时器,在指定时间后关闭提示框
    Future.delayed(Duration(seconds: seconds), () {
        _isShowLoading = false;
    });
    
    ScaffoldMessenger.of(context).showSnackBar(
        // 轻提示主体内容
        SnackBar(
            // 设置背景颜色
            backgroundColor: Colors.white,
            width: 120,
            // 设置圆角
            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(40)),
            // 控制消息显示位置
            // floating 表示悬浮在屏幕底部上方与屏幕底部保持一定间距(可通过 margin 属性调整)
            // fixed 默认值。表示固定在屏幕底部
            behavior: SnackBarBehavior.floating,
            // 提示停留时间
            duration: Duration(seconds: 10),
            // 必填,显示的主内容(Text、Icon等)
            content: Text('刷新成功', textAlign: TextAlign.center),
            // 可选,右侧操作按钮(如撤销、关闭)
            action: SnackBarAction(
                label: '确定',
                onPressed: () {
                    print('确定');
                },
            ),
        ),
    );
}


// 在页面中直接导入直接调用即可
msg(context);

弹框组件

dart
showDialog(
    context: context,
    builder: (context) => AlertDialog(
        // 弹框标题
        title: Text('提示'),
        // 主体提示信息
        content: Text('您还未登录,是否前往登录?'),
        // 底部按钮
        actions: [
            TextButton(
                onPressed: () {
                    // 关闭弹框
                    Navigator.pop(context);
                },
                child: Text('取消'),
            ),
            TextButton(onPressed: () {}, child: Text('确认')),
        ],
    ),
)

封装 loading 组件

会在屏幕中央出现一个转圈等待的 loading 效果

dart
// lib/utils/loading

showLoading(BuildContext context, {String msg = '加载中...'}) {
    showDialog(
        context: context,
        builder: (context) => Dialog(
            backgroundColor: Colors.transparent,
            child: Center(
                child: Container(
                    padding: EdgeInsets.all(20),
                    decoration: BoxDecoration(
                        color: Colors.white,
                        borderRadius: BorderRadius.circular(10),
                    ),
                    child: Column(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                            // 转圈加载组件
                            CircularProgressIndicator(),
                            SizedBox(height: 10),
                            Text(msg),
                        ],
                    ),
                ),
            ),
        ),
    );
}

// 页面中通过 Navigator.pop 关闭 loading 
Navigator.pop(context);

布局组件

线性布局

Column

用于垂直排列其子组件的核心布局容器

本身不支持滚动,如果内容超出,需要使用 ListView 或者 SingleChildScrollView 包裹

明确尺寸约束,父组件的大小直接影响 Column 的最终大小和子组件的布局行为

dart
SizedBox(
    // double.infinity 表示正无穷大,这里表示宽度占满整个父容器,类似于 css 中的 width: 100%
    width: double.infinity,
    height: double.infinity,
    child: Column(
        // 设置每个子组件主轴的间距
        spacing: 10,
       // 决定 Column 本身在垂直方向上的尺寸策略:是占满所有可用空间(max),还是仅仅包裹子组件内容(min)
        mainAxisSize: MainAxisSize.max,
        // 设置主轴(纵向)对齐方式,与 css 中 flex 布局类似
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        // 设置交叉轴(辅轴,横向)对齐方式,与 css 中 flex 布局类似
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
            Container(height: 100, width: 100, color: Colors.red),
            Container(height: 100, width: 100, color: Colors.blue),
            Container(height: 100, width: 100, color: Colors.green),
        ],
    ),
)

Row

用于水平排列其子组件的核心布局容器

本身不支持滚动,如果内容超出,需要使用 ListView 或者 SingleChildScrollView 包裹

明确尺寸约束,父组件的大小直接影响 Row 的最终大小和子组件的布局行为

dart
SizedBox(
    width: double.infinity,
    height: double.infinity,
    child: Row(
        // 设置每个子组件主轴的间距
        spacing: 10,
        // 决定 Row 本身在垂直方向上的尺寸策略:是占满所有可用空间(max),还是仅仅包裹子组件内容(min)
        mainAxisSize: MainAxisSize.max,
        // 设置主轴(横向)对齐方式,与 css 中 flex 布局类似
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        // 设置交叉轴(辅轴,纵向)对齐方式,与 css 中 flex 布局类似
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
            Container(height: 100, width: 100, color: Colors.red),
            Container(height: 100, width: 100, color: Colors.blue),
            Container(height: 100, width: 100, color: Colors.green),
        ],
    ),
)

弹性布局

Flex、Expanded

允许沿一个主轴(水平或垂直)排列其子组件,灵活地控制这些子组件在主轴上的尺寸比例和空间分配

Flex 的子组件常使用 Expanded 来控制空间分配

Flex 是 Column 和 Row 的结合体

Expanded 作为 Flex 的子组件通过 flex 属性来分配 Flex 组件空间

Expanded 组件强制子组件填满所有剩余空间,Flexible 组件根据自身大小调整,不强制占满空间

dart
Flex(
    // 决定主轴的方向,这里是纵向
    direction: Axis.vertical,
    // 决定 Row 本身在垂直方向上的尺寸策略:是占满所有可用空间(max),还是仅仅包裹子组件内容(min)
    mainAxisSize: MainAxisSize.max,
    // 设置主轴对齐方式,与 css 中 flex 布局类似
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    // 设置交叉轴(辅轴)对齐方式,与 css 中 flex 布局类似
    crossAxisAlignment: CrossAxisAlignment.center,
    children: [
        Container(height: 100, width: 100, color: Colors.red),
        Expanded(
            // 设置子组件在 Flex 组件中所占的比例。
            // 这里只有这一个子组件使用了 Expanded 则可以实现
            // 垂直方向上顶部和尾部组件高度固定,此组件高度动态占满 Flex 组件的效果
            flex: 1,
            child: Container(height: 100, width: 100, color: Colors.blue),
        ),
        Container(height: 100, width: 100, color: Colors.green),
    ],
)

流式布局

流式布局组件,当子组件在主轴方向上排列不下时,它会自动换行(或换列)

Column、Row、Flex内容超出均不会换行

Wrap 组件更像是 Flex 组件加了换行特性

dart
List<Widget> getList() {
    // 生成 20 个相同的组件
    return List.generate(
        20,
        (index) => Container(height: 50, width: 50, color: Colors.red),
    );
}

Wrap(
    // 决定主轴的方向
    direction: Axis.horizontal,
    // 子组件在主轴方向上的对齐方式,与 css 中 flex 布局类似
    alignment: WrapAlignment.spaceBetween,
    // 交叉轴(辅轴)方向上的对齐方式,与 css 中 flex 布局类似
    runAlignment: WrapAlignment.center,
    // 主轴方向上,子组件之间的间距
    spacing: 10,
    // 交叉轴(辅轴)方向上,行或列之间的间距
    runSpacing: 10,
    children: getList(),
)

层叠布局

Stack/Positioned

允许将多个子组件按照 Z 轴(深度方向)进行叠加排列。

Positiohed 是 Stack 的黄金搭档,对子组件进行精确定位控制。Positioned 必须作为 Stack 的直接子组件。

dart
Container(
    width: double.infinity,
    height: double.infinity,
    color: Colors.black,
    // Stack 组件自身并不能设置宽高,它的大小由父组件决定,它会撑满父组件
    child: Stack(
        // 控制非定位子组件(未被 Positioned 包裹)在 Stack 内的对齐方式,默认左上角
        alignment: AlignmentGeometry.topLeft,
        // 定义非定位子组件(未被 Positioned 包裹)如何适应 Stack 的约束条件
        // StackFit.expand 表示子组件强制扩展至 Stack 的最大约束尺寸。例如 Stack 约束为 300×500,子组件尺寸固定为 300×500。
        fit: StackFit.expand,
        // 控制子组件超出 Stack 边界时的裁剪方式
        // Clip.none 表示不裁剪,超出部分完全可见。
        clipBehavior: Clip.none,
        // children 中的子组件会按照顺序排列,后添加的会覆盖在前面组件之上
        children: [
            Container(width: 50, height: 50, color: Colors.blue),
            Container(width: 40, height: 40, color: Colors.red),
            // Positioned 组件可以设置子组件在 Stack 内的位置
            Positioned(
                // 当同时设置水平(left、right)或垂直(top、bottom)定位时,会自动拉伸容器
                left: 20,
                top: 20,
                child: Container(width: 30, height: 30, color: Colors.green),
            ),
        ],
    ),
)

使用技巧

dart
Stack(
    // 所有子组件会以 Stack 的中心点为基准进行对齐,实现水平 + 垂直双方向居中效果
    alignment: FractionalOffset.center,
    children: [
        Container(width: 40, height: 40, color: Colors.red),
        Positioned(
            // 此时子组件再设置 bottom(left、top 均可)后可快速实现水平居中,且距离容器底部 20 的位置
            bottom: 20,
            child: Container(width: 30, height: 30, color: Colors.green),
        ),
    ],
),

滚动组件

单个滚动组件

只能包含一个子组件,让单个子组件具备滚动能力。

此组件一次性构建所有子组件,如果嵌套的 Column 或 Row 中包含大量子项,可能会导致性能问题,建议使用 ListView

dart
// 定义控制器 _controller.jumpTo(100) 滚动到指定位置。
// _controller.jumpTo(0) 滚动到页面顶部
// _controllerr.position.maxScrollExten 滚动到页面底部
// 有过渡动画的滚动 如下代码表示会在 1s 内丝滑滚动到页面顶部
// _controller.animateTo(0, duration: Duration(milliseconds: 1), curve: Curves.easeIn);
final ScrollController _controller = ScrollController();

SingleChildScrollView(
    // 滚动方向 默认为垂直方向(Axis.vertical),也可设置为水平方向(Axis.horizontal)
    scrollDirection: Axis.vertical,
    // 控制器,用于控制滚动位置
    controller: _controller,
    // 设置内边距
    padding: EdgeInsets.only(left: 10, right: 10),
    child: Column(
        spacing: 20,
        children: List.generate(10, (index) {
            return Container(
                width: double.infinity,
                height: 100,
                color: Colors.blue,
            );
        }),
    ),
)

列表滚动组件

用于构建可滚动列表的核心部件,并提供流畅滚动体验

提供多种构造函数,如默认构造函数、ListView.builder、ListView.separated

采用按需渲染(懒加载),只构建当前可见区域的列表项,极大提升长列表性能

默认构造函数

会一次性构建所有子组件,适用于静态数量有限数据一次性构建所有表项

dart
final ScrollController _controller = ScrollController();

// 给控制器添加滚动事件
_controller.addListener(() {
    // 如果页面滚动到底部时触发
    if (_controller.position.pixels ==
        _controller.position.maxScrollExtent) {}
});

ListView(
    // 滚动效果,比如:禁用滚动、滚动回弹效果等等.这里是禁止滚动效果
    physics: const NeverScrollableScrollPhysics(),
    // 否根据子组件的总长度来设置 ListView 的长度。默认为 false
    shrinkWrap: true,
    // 滚动方向 默认为垂直方向(Axis.vertical),也可设置为水平方向(Axis.horizontal)
    scrollDirection: Axis.vertical,
    // 控制器,用于控制滚动位置
    controller: _controller,
    // 设置内边距
    padding: EdgeInsets.only(left: 10, right: 10),
    children: List.generate(10, (index) {
        return Container(
            width: double.infinity,
            height: 100,
            color: Colors.blue,
        );
    }),
)

builder 模式

处理长列表或动态数据的首选和推荐方式

按需构建,不会一次性创建所有子组件,而是根据用户的滚动的可视区域,动态地创建和销毁列表项

dart
ListView.builder(
    // 需要构建子组件的个数
    itemCount: 10,
    // 使用该属性代替给子组件设置高可以提高性能
    // 通过提前告知 Flutter 每个项的精确尺寸,避免布局过程中动态计算尺寸,显著提升长列表的滚动性能(减少布局抖动和GPU重绘)
    itemExtent: 100,
    // 负责根据索引 index 动态生成列表项组件
    itemBuilder: (context, index) {
        return Container(
            margin: EdgeInsets.only(bottom: 10),
            width: double.infinity,
            color: Colors.blue,
        );
    },
)

separated 模式

在 ListView.builder 的基础上,额外提供了构建分割线的能力

dart
ListView.separated(
    // 需要构建子组件的个数
    itemCount: 10,
    // 用于渲染列表的每个子组件
    itemBuilder: (context, index) {
        return Container(
            margin: EdgeInsets.only(bottom: 10),
            width: double.infinity,
            color: Colors.blue,
        );
    },
    // 设置分割线组件,会在每个子组件只见渲染此分割线内容
    separatorBuilder: (context, index) {
        return Container(
            margin: EdgeInsets.only(bottom: 10),
            width: double.infinity,
            color: Colors.red,
        );
    },
)

网格滚动组件

用于创建二维可滚动网格布局的核心组件

提供多种构建方式,GridView.count、GridView.extent、GridView.builder 等

count 模式

使用 GridView.count 创建固定列数网格

GridView.count 以列数为优先。指定网格多少列,Flutter 自动计算列的宽度,在空间内均匀排列

dart
GridView.count(
    // 内边距
    padding: EdgeInsets.only(left: 10, right: 10),
    // 辅轴(默认横向)的子组件数量
    crossAxisCount: 3,
    // 主轴(默认纵向)的子组件间距
    mainAxisSpacing: 10,
    // 辅轴(默认横向)的子组件间距
    crossAxisSpacing: 10,
    // 每个子组件的长宽比
    childAspectRatio: 0.5,
    // 设置滚动方向,默认是纵向
    scrollDirection: Axis.vertical,
    children: List.generate(
        9,
        (index) => Container(color: Colors.blue),
    ),
)

extent 模式

使用 GridView.extent 指定子项最大宽度或者高度,子组件在辅轴上展示个数不固定

GridView.extent 通过 maxCrossAxisExtent 设置子项最大宽度/高度来计算横向或者纵向有多少列

dart
GridView.extent(
    // 设置辅轴(默认横向)的每个子组件最大宽度
    maxCrossAxisExtent: 100,
    // 主轴(默认纵向)的子组件间距
    mainAxisSpacing: 10,
    // 辅轴(默认横向)的子组件间距
    crossAxisSpacing: 10,
    // 每个子组件的长宽比
    childAspectRatio: 1,
    // 设置滚动方向,默认是纵向
    scrollDirection: Axis.vertical,
    children: List.generate(
        20,
        (index) => Container(color: Colors.blue),
    ),
)

builder 模式

使用 GridView.builder 实现动态长网格(懒加载,只渲染可见区域)

dart
GridView.builder(
    // 布局委托,用于控制子组件的排列方式
    // SliverGridDelegateWithFixedCrossAxisCount:固定列数。和 GridView.count 一样
    // SliverGridDelegateWithMaxCrossAxisExtent:最大宽度或高度。和 GridView.extent 一样
    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        // 辅轴(默认横向)的子组件数量
        crossAxisCount: 3,
        // 主轴(默认纵向)的子组件间距
        mainAxisSpacing: 10,
        // 辅轴(默认横向)的子组件间距
        crossAxisSpacing: 10,
        // 每个子组件的长宽比
        childAspectRatio: 4 / 3,
    ),
    // 需要构建子组件的个数
    itemCount: 20,
    // 设置滚动方向,默认是纵向
    scrollDirection: Axis.vertical,
    itemBuilder: (context, index) {
        return Container(color: Colors.blue);
    },
)

自定义滚动组件

用于组合多个可滚动组件(如列表、网格),实现统一协调的滚动效果

用法:通过 slivers 属性接收一个 Sliver 组件列表,此列表不能直接放入之前的组件,必须是 Sliver 组件

常用 Sliver 组件对应关系:

SliverList => ListView

SliverGrid => GridView SliverAppBar => AppBar SliverPadding => Padcing SliverToBoxAdapter => ToBoxAdapter(用于包裹普通Widget) SliverPersistentHeader(粘性吸顶)

dart
CustomScrollView(
    // 切片列表。里面不可以直接放置 GridView、ListView、Container 等其他所有组件,否则会报错。
    // 需要放置特定和其功能一样的组件,比如 SliverGrid、SliverList、SliverToBoxAdapter 等。
    slivers: [
   // slivers 中如果想使用其他普通组件(除 GridView、ListView 等)需要使用 SliverToBoxAdapter 包裹
        SliverToBoxAdapter(child: Container()),
        SliverList.builder(
            itemCount: 10,
            itemBuilder: (context, index) =>
            Container(width: 50, height: 50, color: Colors.red),
        ),
        SliverGrid.count(
            crossAxisCount: 2,
            crossAxisSpacing: 10,
            mainAxisSpacing: 10,
            children: List.generate(
                10,
                (index) => Container(width: 50, height: 50, color: Colors.blue),
            ),
        ),
    ],
)

分页组件

用于实现分页滚动视图的核心组件

提供多种构建方式,默认构造方式、PageView.builder 等

默认构造方式

会一次性构建所有子组件

dart
// 跳转到指定页码
// _pageController.jumpToPage(2);
// 带平滑滚动动画的跳到指定页码
_pageController.animateToPage(2,duration: Duration(seconds: 1),curve: Curves.ease);

// PageView 默认会占满父组件
PageView(
    // 分页控制器
    controller: _pageController,
    // 翻页时触发
    onPageChanged: (index) {
        print('当前页码:$index');
    },
    // 滚动方向,默认 horizontal 横向滚动
    scrollDirection: Axis.horizontal,
    children: List.generate(
        5,
        (index) =>
        Container(color: Colors.blue, child: Text('第$index个页面')),
    ),
)

builder 模式

支持懒加载(按需渲染)

dart
PageView.builder(
    // 滚动方向,默认 horizontal 横向滚动
    scrollDirection: Axis.horizontal,
    // 子组件的个数
    itemCount: 5,
    itemBuilder: (itemBuilder, index) =>
    Container(color: Colors.blue, child: Text('第$index个页面')),
)

吸顶组件

当页面滚动了一定距离后再往下滚动,此组件则会固定在页面顶部

dart
CustomScrollView(
    slivers: [
        SliverToBoxAdapter(
            child: Container(height: 300, color: Colors.blue),
        ), 
        // 当在自定义滚动组件中使用时需要使用 SliverPersistentHeader 组件包裹
        // 使用吸顶组件 pinned 表示是否吸顶
        SliverPersistentHeader(delegate: MyPersistentHeaderDelegate(), pinned: true),
    ],
)

// 定义吸顶组件,需继承 SliverPersistentHeaderDelegate 类
class MyPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
    @override
    Widget build(
        BuildContext context,
        double shrinkOffset,
        bool overlapsContent,
    ) {
        // 此处放置吸顶组件内容
        return Container(height: 44, color: Colors.black);
    }

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

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

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

动画组件

过渡动画容器

dart
// 当样式发生变化时可以自动添加动画
AnimatedContainer(
    // 过渡动画时长,这里是 300 毫秒
    duration: Duration(milliseconds: 300),
    // 当宽度发生变化时自动添加过渡动画
    width: isShow ? 200 : 100,
    decoration: BoxDecoration(
        // 当背景色发生变化时自动添加过渡动画
        color: isShow ? Colors.amber : Colors.black,
    ),
)

平移动画

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

主题组件

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

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

导航组件

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
// 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
// AbsorbPointer 组件用于禁止组件事件()输入,absorbing 为 true 时表示禁止
// 如下写法可以阻止 TabBar 组件的点击事件
AbsorbPointer(absorbing: true,child: TabBar())

组件通信

父传子

构造函数方式

子组件是无状态组件时

dart
class Child extends StatelessWidget {
    // 1.在子组件中定义属性,用于接收父组件传递过来的信息
    // 子组件定义接收属性需要使用 final 关键字。因为属性由父组件决定,子组件不能随意更改
    final String txt;
    // 2.子组件在构造函数中接收参数
    const Child({super.key, required this.txt});

    Widget build(BuildContext context) {
        return Text('子组件$txt');
    }
}

class Parent extends StatelessWidget {
    Widget build(BuildContext context) {
        return Child(txt: '父组件信息');
    }
}

子组件是有状态组件时

dart
// 父组件传递数据同上
class Child extends StatefulWidget {
    // 1.在子组件中定义属性,用于接收父组件传递过来的信息
    // 子组件定义接收属性需要使用 final 关键字。因为属性由父组件决定,子组件不能随意更改
    final String txt;
    // 2.子组件在构造函数中接收参数
    const Child({super.key, required this.txt});
}

class _ChildState extends State<Child> {
    Widget build(BuildContext context) {
        // 3.子组件是有状态组件时,获取父组件传递的属性时需要通过 widget 来获取
        return Text('子组件${widget.txt}');
    }
}

父组件直接调用子组件方法

GlobalKey 是一个方法可以创建一个 key 绑定到 Widget 部件上可以操作 Widget 部件

dart
// 如果子组件是有状态组件
// 注意继承自 State 的类需要改为非私有(即去掉下划线)
class HmMoreListState extends State<HmMoreList> {
    clear() {
        print('执行');
    }
}

// 父组件
// 定义 GlobalKey 泛型设置为子组件继承自 State 的类即可
final GlobalKey<HmMoreListState> hmMoreListKey = GlobalKey<HmMoreListState>();
Future<void> onRefresh() async {
    // 直接调用子组件的方法
    hmMoreListKey.currentState?.clear();
}

Widget build(BuildContext context) {
    // 将 GlobalKey 传递给子组件
    return HmMoreList(controller: controller, key: hmMoreListKey);
}

子传父

回调函数方式

dart
class Child extends StatefulWidget {
    // 在子组件中定义函数,用于接收父组件传递过来的回调函数
    // 子组件定义接收属性需要使用 final 关键字。因为属性由父组件决定,子组件不能随意更改
    final Function req;
    // 子组件在构造函数中接收参数
    const Child({super.key, required this.req});
}

class _ChildState extends State<Child> {
    Widget build(BuildContext context) {
        return TextButton(
            onPressed: () {
                // 在子组件中使用父组件传递过来的回调函数并传递参数
                widget.req('hello');
            },
            child: Text('子组件'),
        );
    }
}

class Parent extends StatelessWidget {
    Widget build(BuildContext context) {
        // 在父组件中传递一个函数给子组件
        void req(String txt) {
            // 从回调函数中接收从子组件传递过来的
            print(txt);
        }
        // 将回调函数传递给子组件
        return child: Child(req: req);
    }
}

网络请求

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

安装 dio

可以直接通过命令的方式安装

bash
flutter pub add dio

之后打开 pubspec.yaml 查看依赖项是否安装成功

当然也可以直接将需要安装的插件名称以及对应版本号直接写入,保存即可,flutter 会自行下载安装

yaml
dependencies:
# 如直接粘贴 dio 与对应版本号保存 flutter 会自行下载安装
  dio: ^5.9.0

基本使用

dart
void main() {
    Dio()
        .get("https://geek.itheima.net/v1_0/channels")
        .then((res) => print(res))
        .catchError((e) {});
}

封装 dio

在 lib/utils/request.dart 写入

dart
class DioUtils {
    // 定义私有属性
    final Dio _dio = Dio(
        BaseOptions(
            // 基础路径
            baseUrl: 'https://geek.itheima.net/v1_0/',
            // 连接超时时间
            connectTimeout: Duration(seconds: 10),
            // 发送超时时间
            sendTimeout: Duration(seconds: 10),
            // 接收超时时间
            receiveTimeout: Duration(seconds: 10)
        ),
    );
    // 在默认构造函数做些初始化操作
    DioUtils() {
        // 添加拦截器
        _dio.interceptors.add(
            InterceptorsWrapper(
                // 请求拦截器
                onRequest: (context, handler) {
                    // handLer.next(requestoptions)放过请求
                    // handLer.reject(error)拦截请求
                    handler.next(context);
                },
                // 响应拦截器
                onResponse: (context, handler) {
                    // 请求成功
                    if (context.statusCode! >= 200 && context.statusCode! < 300) {
                        handler.next(context);
                        return;
                    }
                    // 请求失败时抛出异常
                    handler.reject(DioException(requestOptions: context.requestOptions));
                },
                // 错误拦截器
                onError: (error, handler) {
                    // 抛出异常
                    handler.reject(
                        // 发生异常时将服务器返回的错误信息放入
                        DioException(
                            requestOptions: error.requestOptions,
                            message: error.response?.data['msg'] ?? '请求失败',
                        ),
                    );
                },
            ),
        );
    }

    // 封装 get 方法
    Future<Response<dynamic>> get(String url, {Map<String, dynamic>? params}) =>
        _dio.get(url, queryParameters: params);
}

// 导出实例
final dioUtils = DioUtils();

在组件中使用

dart
// 引入依赖
import 'package:flutterproject/utils/request.dart';
import 'package:dio/dio.dart' show Response;

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

_getChannels() async {
    // 使用
    Response<dynamic> res = await dioUtils.get('channels').catchError(() {});
    print(res);
}

方法2

dart
// lib/utils/request.dart
final Dio dio = getDio();

// 初始化 dio
getDio() {
    print('dio初始化');
    // 此处省略对 dio 的基本配置与拦截器配置,详情可查看上一步
    Dio dio = Dio();
    dio.interceptors.add();
    return dio;
}

// 封装工具方法
Future<dynamic> request(
    String url, {
        Map<String, dynamic>? params,
        Map<String, dynamic>? data,
        Options? options,
    }) async {
    return Future(() async {
        // 捕获错误
        try {
            var res = await dio.request(
                url,
                // 作为请求体参数
                data: data,
                // 作为查询字符串参数,即参数会出现在接口地址之上
                queryParameters: params,
                // 可不用传递 method  dio 默认是 get 请求
                // options: Options(method: method)
                options:options
            );
            // 解构数据(dio 会默认在数据外包一层 data)
            return res.data;
        } catch (e) {
            // 直接写明错误,方便后续调用时不会报错
            return {'code': '-1', 'msg': (e as DioException).message};
        }
    });
}
dart
// lib/api/index.dart
// 导出接口函数
Future<dynamic> getBanner() => request('home/banner');
dart
// 引入工具函数以及接口路径
import 'package:flutterproject/api/index.dart' show getBanner;

// 页面中调用
getinfo() async {
    final res = await getBanner();
    if (res['code'] != '1') return;
    setState(() {
        _bannerList = res['result'];
    });
}

浏览器端跨域

默认情况下,flutter 运行 web 端加载网络资源会报跨域提示错误。

1.在flutter/packages/flutter_tools/lib/src/web/chrome.dart 如下位置添加 '--disable-web-security',大概在 223 行的位置

dart
// chrome.dart
final args = <String>[
    chromeExecutable,
    // 添加此配置即可解决跨域问题
    '--disable-web-security',
]

2.删除 flutter/bin/cache/ 下 flutter_tools.snaphot 和 flutter_tools.stamp 文件并重新启动项目即可

路由管理

路由管理是构建多页面应用的核心,它通过 Navigator 和 Route 来管理页面栈,实现页面跳转和返回

通过 Navigator.push 入栈(跳转页面)、Navigator.pop 出栈(回退页面)

基本路由

基本路由适合页面不多、跳转逻辑简单的场景

无需提前注册路由,跳转时创建 MaterialPageRoute 实例即可

MaterialApp 是路由系统的组件,只能有一个 MaterialApp 包裹

dart
void main() {
    // 使用 MaterialApp 渲染 A 页面
    runApp(MaterialApp(home: ListPage()));
}

// A 页面
class ListPage extends StatelessWidget {
    Widget build(BuildContext context) {
        return Scaffold(
            body: TextButton(
                onPressed: () {
                    // 跳转页面
                    Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => DetalsPage()),
                    );
                },
                child: Text('列表页'),
            ),
        );
    }
}

// B 页面
class DetalsPage extends StatelessWidget {
    Widget build(BuildContext context) {
        return Scaffold(
            body: TextButton(
                onPressed: () {
                    // 回退页面
                    Navigator.pop(context);
                },
                child: Text('详情页'),
            ),
        );
    }
}

路由传参

基本路由传参和组件通信中通过构造函数方式父传子类似

dart
Navigator.push(
    context,
    // 传递路由参数
    MaterialPageRoute(builder: (context) => DetalsPage(id:'1')),
);


// 接收参数
class DetalsPage extends StatelessWidget {
    // 接收参数
    final String id;
    Widget build(BuildContext context) {
        return Scaffold(
            body: TextButton(
                onPressed: () {
                    // 接收参数
                    print(id);
                },
                child: Text('详情页'),
            ),
        );
    }
}

命名路由

应用页面增多后,使用命名路提升代码可维护性。

需要先在 MaterialApp 中注册一个路由表(routes)并设置 initialRoute(首页)

dart
void main() {
  runApp(
    MaterialApp(
      // 默认展示的路由页面
      initialRoute: '/detals',
      // 注册路由表
      routes: {
        '/list': (context) => ListPage(),
        '/detals': (context) => DetalsPage(),
      }
    ),
  );
}

class ListPage extends StatelessWidget {
  Widget build(BuildContext context) {
    return Scaffold(
      body: TextButton(
        onPressed: () {
          // 跳转页面
          Navigator.pushNamed(context, '/detals');
        },
        child: Text('列表页'),
      ),
    );
  }
}

class DetalsPage extends StatelessWidget {
  Widget build(BuildContext context) {
    return Scaffold(
      body: TextButton(
        onPressed: () {
          // 跳转页面
          Navigator.pushNamed(context, '/list');
        },
        child: Text('详情页'),
      ),
    );
  }
}

路由传参

通过路由传递参数是实现页面间数据通信的常用方式

dart
// 跳转页面并添加路由参数
Navigator.pushNamed(context, '/detals', arguments: {'name': '张三'});


// 接收路由参数
void initState() {
    // 在 initState 无法直接获取到路由参数,需要借助微任务"等待一下",就能接收到
    Future.microtask(() {
        // 创建一个微任务用来接收路由参数
        var args = ModalRoute.of(context)?.settings.arguments;
        if (args != null) {
            print((args as Map<String, dynamic>)['name']);
        }
    });
}

动态路由

更复杂的场景,如需根据参数动态生成页面,或实现路由拦截,可以使用 onGenerateRoute 和 onUnknownRoute

dart
void main() {
    runApp(
        MaterialApp(
            initialRoute: '/detals',
            // 注册路由表
            routes: {
                '/detals': (context) => DetalsPage(),
            },
            onGenerateRoute: (settings) {
                // 如果路由表里没有找到路由,就会调用这个方法
                // 如果返回值是 null,则表示没有匹配到路由,会报错
                // 如果返回值是其他值,则表示匹配到了路由,会显示对应页面
                // settings.name 可以获取当前要跳转的路径
                if (settings.name == '/my') {
                    // 根据路由渲染对应组件
                    return MaterialPageRoute(builder: (context) => My());
                }
                return null;
            },
// 如果跳转一个未在路由表中注册、也未在 onGenerateRoute 中处理的路由,会调用此回调。通常显示"404"页面
            onUnknownRoute: (settings){
                return MaterialPageRoute(builder: (context) => NotFound());
            }
        ),
    );
}

class My extends StatelessWidget {
  Widget build(BuildContext context) {
    return Text('我的');
  }
}


class DetalsPage extends StatelessWidget {
  Widget build(BuildContext context) {
    return TextButton(
        onPressed: () {
          // 如果跳转的页面不在路由表里,则调用 onGenerateRoute
          Navigator.pushNamed(context, '/my');
        },
        child: Text('详情页'),
      );
  }
}

实用插件

轮播图插件

bash
# 安装
flutter pub add carousel_slider

使用

dart
CarouselSlider(
    // 轮播图图片项
    items: List.generate(
        5,
        (int index) => Image.network(
            '图片地址',
            fit: BoxFit.cover,
            width: double.infinity,
        ),
    ),
    options: CarouselOptions(
        // 图片高度
        height: 300,
        // 图片占容器的宽度比,即:图片宽度/容器宽度,这里设置为 1,即图片宽度等于容器宽度
        viewportFraction: 1,
        // 是否自动轮播
        autoPlay: true,
        // 自动轮播间隔时间
        autoPlayInterval: Duration(seconds: 3),
        // 当轮播图切换时出发
        onPageChanged:(index, reason) {},
    ),
)

持久化插件

bash
# 安装插件
flutter pub add shared_preferences

1.写入 Storagese 类

dart
// storagese.dart
class Storagese {
    // 获取本地存储实例
    final _prefs = SharedPreferences.getInstance();

    setInfo(String token) async {
        final storagese = await _prefs;
        // 写入数据
        storagese.setString('token', token);
        // storagese.getString('token'); 读取数据
        // 写入复杂数据需要先调用 jsonEncode 将数据序列化才可存储
        storagese.setString('name', jsonEncode({'name': '张三'}));
        // 读取时需要反序列化
        jsonDecode(storagese.getString('name'))['name']
        _token = token;
    }
}

数据共享插件

GetX 插件是一个轻量级、高性能的全功能框架,提供路由管理、状态管理、依赖注入等核心功能

bash
# 安装插件
flutter pub add get

简单使用

1.先创建数据共享类

dart
// lib/stores/index.dart

import 'package:get/get.dart';

// 这里放入需要共享的对象
class UserController extends GetxController {
    // 如果需要响应式数据则需要在数据末尾加上 .obs
    // 页面中使用时则需要使用 user.value 来读取值
    var user1 = ''.obs;
    var user = {'name': ''}.obs;

    // 定义更新数据的函数
    updataUser(info) {
        user.value = info;
    }

}

2.在需要使用数据的地方调用 put 方法

dart
import 'package:get/get.dart';
import 'package:flutterproject/stores/index.dart';

// 在需要共享数据的地方先调用 Get.put 方法,表示将数据填入
final UserController _userController = Get.put(UserController());

Container(
    // 在需要响应式数据的地方用 Obx 方法包裹
    child: Obx(() {
        return Text(
            // 读取数据
            // _userController.user1.value 读取简单数据
            _userController.user.value['name']
        );
    }),
)

3.在需要写入数据的地方调用 Get.find 方法

dart
// 在写入数据的地方调用 Get.find 方法
final UserController _userController = Get.find();

login() {
    // 写入数据
    _userController.updataUser('张三');
}

其他技巧

获取屏幕宽度

dart
Widget build(BuildContext context) {
    // 此方法需要在 build 函数中使用
    final double screenWidth = MediaQuery.of(context).size.width;// 屏幕宽度
}

项目中使用

自定义帧动画

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

内置图标库

https://fluttericon.cn

定时器

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