「 Flutter 」一文了解 Flutter 基础知识(万字长文)

1,527 阅读8分钟

本文从 Flutter 的运行环境搭建开始介绍如何使用 Flutter,之后以相关知识点的说明、代码演示以及实际结果展示来介绍基本控件的使用方法。

使用 Flutter 需要首先了解 Dart 的语法,如果你还不清楚如何使用 Dart,可以先去看看「 Dart 」一文了解 Dart 基础知识

搭建运行环境

  1. 安装并配置 JDK

  2. 下载安装 Android Studio

    下载网址:Download Android Studio and SDK tools

    安装到自定义目录

  3. 下载配置 Flutter SDK

    下载网址:Windows install | Flutter

    安装到自定义目录后,把 Flutter 安装目录的 bin 目录配置到 path 环境变量

    命令行输入 flutter -v 检查是否配置成功

  4. 配置 Flutter 国内镜像

    方法是将配置下面两条环境变量:

    FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn 
    PUB_HOSTED_URL: https://pub.flutter-io.cn
    
  5. 运行 flutter doctor 命令检测环境是否配置成功

    可能会遇到的问题:cmdline-tools component is missing

    运行 flutter doctor --android-licenses 后全部选择 y

    再次运行 flutter doctor 显示如下信息则表示配置成功:

    Doctor summary (to see all details, run flutter doctor -v):
    [√] Flutter (Channel stable, 2.5.3, on Microsoft Windows [Version 10.0.19042.1348], locale zh-CN)
    [√] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
    [√] Chrome - develop for the web
    [√] Android Studio (version 2020.3)
    [√] IntelliJ IDEA Ultimate Edition (version 2021.1)
    [√] Connected device (2 available)
    
    • No issues found!
    
  6. 在 Android Studio 配置 Flutter 插件

    打开 Android Studio,选择 Plugins -> Maketplace,搜索并安装 Flutter 插件

  7. 创建 Flutter 项目

    New Flutter Project -> Flutter -> 设置 Flutter SDK -> 设置项目信息 -> Finish

  8. 在 Android Studio 中导入运行 Flutter 项目

    导入项目文件夹中的 android 文件夹,等待下载 gradle 构建工具

  9. 在手机上调试

    用数据线连接手机和电脑,开启手机的调试模式并允许 USB 安装

    在 Android Studio 中选择 run -> run 'app' ,会在手机上安装应用

    打开应用(下图是默认的应用样式):

3DE8AB42ED234CBBD399B372477E43D0.jpg

在 VSCode 中开发

在 VSCode 中安装 Flutter 和 Flutter Widget Snippets 插件。

打开 Flutter 项目的文件夹,在终端中使用flutter run 命令来运行项目。

在终端中按下下列按键可实现相应效果:

r 键:点击后热加载,即重新加载。 
R 键:点击后重新启动。
p 键:显示网格,让开发者掌握布局情况。
o 键:切换 android 和 iOS 等预览模式。
q 键:退出调试预览模式。

Hello Flutter

目录结构

下面是生成的项目文件中我们主要用到的文件:

android:android 平台相关代码
ios:iOS 平台相关代码
lib:flutter 相关代码,是我们主要编写的代码
test:测试代码
pubspec.yaml:配置文件,一般存放一些第三方库的依赖

入口文件

Flutter 的入口文件是 lib/main.dart

void main(){
	runApp(MyApp());
}

main 方法是 Dart 的入口方法,runApp 是 Flutter 的入口方法, MyApp 是自定义的控件,应用程序显示的是 MyApp 控件中的内容。

基本控件

这一节介绍 Flutter 常用的基本控件,包括控件的作用、属性和使用样例。看完本节你可以了解到这些控件的使用场景和使用方法。

Center 控件

在代码中使用 Flutter ,你首先需要导入 Flutter 包,可以输入快捷指令 fimpmat

import 'package:flutter/material.dart';

Center 控件将其子 widget 居中显示在自身内部的 widget:

import 'package:flutter/material.dart';

void main() {
  runApp(
    Center(
      child: Text(
        'Hello Flutter',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}

在 Center 控件中使用了 Text 子控件。

**Ctrl + 鼠标左键 点击控件名查看控件的源码,了解传递的参数。 **

抽离控件

由于直接在 runApp 中写控件内容使得代码结构不够清晰,我们需要将 runApp 中的内容抽离出来单独作为一个控件。

Flutter 中自定义控件是一个类,它们必须继承 StatelessWidget 或 StatefulWidget 这两个抽象类。

StatelessWidget 是无状态控件,状态不可变
StatefulWidget 是有状态控件,状态可能在 widget 生命周期中改变

我们需要实现抽象类中的抽象方法,点击快速修复可以看到我们需要实现 build 方法,它需要返回一个 widget 。我们将原来 runApp 中的参数写到 build 方法中的返回参数中,并在 runApp 中使用自定义控件。

import 'package:flutter/material.dart';

void main() {
  runApp(MyAPP());
}

class MyAPP extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('Hello Flutter'),
    );
  }
}

444175ECC4BB50434191BE59B5568A39.jpg

Text 控件

我们可以给对 Text 控件中的内容进行修改。

class MyAPP extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        'Hello Flutter',
        // 文字方向
        textDirection: TextDirection.ltr,
        // 文字样式
        style: TextStyle(
          fontSize: 40.0,
          color: Colors.yellow,
        ),
      ),
    );
  }
}

使用 Material Design

MaterialApp 控件

MaterialApp 封装了应用程序实现 Material Design 所需要的一些 widget。一般作为顶层 widget 使用。

常用属性:home 主页、title 标题、color 颜色、theme 主题、routes 路由等。

Scaffold 控件

Scaffold 是 Material Design 布局结构的基本实现,Scaffold 即脚手架,提供了用于显示 drawer、snackbar 和底部 sheet 的 API。

常用属性:

appBar - 显示在界面顶部的一个 AppBar
body - 当前界面所显示的主要内容 Widget
drawer - 抽屉菜单控件

使用这两个控件:

import 'package:flutter/material.dart';

void main() {
  runApp(MyAPP());
}

class MyAPP extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // 主页
      home: Scaffold(
        // 导航栏
        appBar: AppBar(
          // 标题
          title: Text('Flutter Demo'),
        ),
        // 页面主体
        body: HomeContent(),
      ),
      // 主题
      theme: ThemeData(
        // 主题颜色
        primarySwatch: Colors.yellow,
      ),
    );
  }
}

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        'Hello Flutter',
        style: TextStyle(
          fontSize: 40.0,
          color: Colors.yellow,
        ),
      ),
    );
  }
}

94D6350EBC651FA9E6DF9FFC2F786895.jpg

现在,我们的页面有了大致框架。

Container 控件

Container widget 可以用来创建一个可见的矩形元素。 Container 可以使用 BoxDecoration 来进行装饰,如背景、边框、阴影等。 Container 还可以设置外边距、内边距和尺寸的约束条件等。另外,Container可以使用矩阵在三维空间进行转换。

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        child: Text('Hello Flutter'),
        alignment: Alignment.topCenter,
        height: 300.0,
        width: 300.0,
        decoration: BoxDecoration(
          color: Colors.yellow,
          border: Border.all(
            color: Colors.blue,
            width: 2.0,
          ),
        ),
      ),
    );
  }
}

Image 控件

图片控件是显示图像的控件,常用 Image.asset 导入本地图片、Image.network 导入网络图片。

引入网络图片

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        width: 300,
        height: 300,
        decoration: BoxDecoration(
          color: Colors.yellow,
        ),
        child: Image.network(
          "https://flutter.cn/static/4ea7d7f5f72649f0bcec.png",
          alignment: Alignment.topLeft,
          // 背景颜色
          // color: Colors.blue,
          // 图片混合模式
          // colorBlendMode: BlendMode.luminosity,
          // 图片适配方式
          fit: BoxFit.contain,
          repeat: ImageRepeat.repeat,
        ),
      ),
    );
  }
}

实现图片的圆角

  1. 使用 BoxDecoration 实现
class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        width: 300,
        height: 300,
        decoration: BoxDecoration(
          color: Colors.yellow,
          // 圆角半径
          borderRadius: BorderRadius.circular(150),
          // 修饰图片
          image: DecorationImage(
            // 提供图片
            image: NetworkImage(
                "https://flutter.cn/static/4ea7d7f5f72649f0bcec.png"),
            fit: BoxFit.contain,
            repeat: ImageRepeat.repeat,
          ),
        ),
      ),
    );
  }
}
  1. 使用 ClipOval 实现
class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        child: ClipOval(
          child: Image.network(
            "https://flutter.cn/static/4ea7d7f5f72649f0bcec.png",
            height: 200,
            width: 200,
            fit: BoxFit.cover,
          ),
        ),
      ),
    );
  }
}

引入本地图片

  1. 新建两个目录

    项目根目录\images\(2.0x/3.0x)image 目录相当于 1.0x

  2. 将图片放入images文件夹和两个子文件夹中

  3. 配置 pubspec.yaml 文件

    在 flutter 下配置 assets:

flutter:
  uses-material-design: true
  assets:
    - images/demo.jpg
    - images/2.0x/demo.jpg
    - images/3.0x/demo.jpg
  1. 在代码中使用

    使用方法类似 Images.network

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        child: ClipOval(
          child: Image.asset(
            "images/demo.jpg",
            height: 200,
            width: 200,
            fit: BoxFit.cover,
          ),
        ),
      ),
    );
  }
}

image.png

ListView 控件

垂直列表

ListView 默认为垂直列表:

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: EdgeInsets.all(10),
      children: <Widget>[
        // 数组里可以放其他 Widget
        ListTile(
          // 设置前置图标
          leading: Icon(
            Icons.settings,
            // 改变图标样式
            color: Colors.yellow,
            size: 30,
          ),
          title: Text(
            '点亮你的Vue技术栈,万字Nuxt.js实践笔记来了',
            // 设置字体
            style: TextStyle(
              fontSize: 16,
            ),
          ),
          subtitle: Text('作为一位 Vuer(vue开发者),如果还不会这个框架,那么你的 Vue 技术栈还没被点亮'),
          // 设置后置图标
          trailing: Icon(Icons.sentiment_satisfied_sharp),
        ),
        ListTile(
          title: Text('如何用 docker 打造前端开发环境'),
          subtitle: Text('如何使用 docker 打造前端开发环境 docker 的用法很多,除了可以用来部署项目,还可'),
        ),
        ListTile(
          title: Text('如何做前端Code Review'),
          subtitle: Text('向互联网大厂学习,从代码格式、代码错误、代码习惯、代码优化四个角度进行前端Co'),
        ),
      ],
    );
  }
}

image.png

水平列表

配置 scrollDirection 属性为 Axis.horizontal,将排列方向改为水平排列,得到水平列表:

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 240,
      child: ListView(
        // 配置方向,默认为垂直列表
        scrollDirection: Axis.horizontal,
        children: <Widget>[
          Container(
            width: 180,
            color: Colors.yellow,
          ),
          Container(
            width: 180,
            color: Colors.orange,
            // 列表嵌套
            child: ListView(
              children: <Widget>[
                Image.network(
                  "https://flutter.cn/static/4ea7d7f5f72649f0bcec.png",
                ),
                Image.network(
                  "https://flutter.cn/static/4ea7d7f5f72649f0bcec.png",
                ),
              ],
            ),
          ),
          Container(
            width: 180,
            color: Colors.red,
          ),
        ],
      ),
    );
  }
}

动态列表

动态列表可以动态循环数据。

class HomeContent extends StatelessWidget {
  // 自定义私有方法
  List<Widget> _getData() {
    List<Widget> list = [];
    for (var i = 0; i < 20; i++) {
      list.add(ListTile(
        title: Text('this is list $i'),
      ));
    }
    return list;
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: this._getData(),
    );
  }
}

使用外部数据:

// lib/res/listData.dart
List listData = [
  {
    "title": "4 年经验裸辞 2 个月,40 场面试、一路的心态变化及经验总结",
    "author": "天明夜尽",
    "avatar":
        "https://p3-passport.byteacctimg.com/img/user-avatar/aa2fe44ba824fc1b5e21a73b565ef0db~300x300.image",
    "imgUrl":
        "https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/78e7025d384846e9b0314a53a2adab36~tplv-k3u1fbpfcp-zoom-crop-mark:1304:1304:1304:734.awebp?",
  },
  {
    "title": "[万字总结]我还在正确的道路上么?2021年一个前端新人的半年学习工作总结",
    "author": "速冻鱼",
    "avatar":
        "https://p9-passport.byteacctimg.com/img/user-avatar/1a85c9561d83fc5e8a441432767677fb~300x300.image",
    "imgUrl":
        "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e4a813587ec84152b243c3d54323d01e~tplv-k3u1fbpfcp-zoom-crop-mark:1304:1304:1304:734.awebp?",
  }
];


// lib/main.dart
import 'res/listData.dart';
...
class HomeContent extends StatelessWidget {
  // 自定义私有方法
  List<Widget> _getData() {
    var tempList = listData.map((value) {
      return ListTile(
        leading: Image.network(value["imgUrl"]),
        title: Text(value["title"]),
        subtitle: Text(value["author"]),
      );
    });
    return tempList.toList();
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: this._getData(),
    );
  }
}

使用 ListView.builder:

ListView的标准构造函数适用于数目比较少的场景,如果数目比较多的话,最好使用ListView.builder。因为 ListView 的标准构造函数会将所有 item 一次性创建,而 ListView.builder 会创建滚动到屏幕上显示的 item。

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: listData.length,
      itemBuilder: (context, index) {
        return ListTile(
          leading: Image.network(listData[index]["imgUrl"]),
          title: Text(listData[index]["title"]),
          subtitle: Text(listData[index]["author"]),
        );
      },
    );
  }
}

还可以这么写:

class HomeContent extends StatelessWidget {
  List<Widget> _getData() {
    var tempList = listData.map((value) {
      return ListTile(
        leading: Image.network(value["imgUrl"]),
        title: Text(value["title"]),
        subtitle: Text(value["author"]),
      );
    });
    return tempList.toList();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: this._getData().length,
      itemBuilder: (context, index) {
        return this._getData()[index];
      },
    );
  }
}

image.png

GridView 控件

创建网格布局主要有两种方式,GridView.countGridView.builder

GridView.count

import 'res/listData.dart';
...

class HomeContent extends StatelessWidget {
  List<Widget> _getListData() {
    var tmpList = listData.map((value) {
      return Container(
        child: Column(
          children: <Widget>[
            Image.network(value["imgUrl"]),
            // Image 和 SizeBox 之间的距离
            SizedBox(height: 10),
            Text(value["title"]),
          ],
        ),
      );
    });
    return tmpList.toList();
  }

  @override
  Widget build(BuildContext context) {
    return GridView.count(
      // 一行的 widget 数量
      crossAxisCount: 2,
      // 两个 Widget 的水平距离
      crossAxisSpacing: 20,
      // 两个 Widget 的垂直距离
      mainAxisSpacing: 20,
      // 整个 GridView 的内边距
      padding: EdgeInsets.all(10),
      // 每个 Widget 的宽高比例
      // childAspectRatio: 1.7,
      children: this._getListData(),
    );
  }
}

GridView.bulider

class HomeContent extends StatelessWidget {
  Widget _getListData(context, index) {
    return Container(
      child: Column(
        children: <Widget>[
          Image.network(listData[index]["imgUrl"]),
          // Image 和 SizeBox 之间的距离
          SizedBox(height: 10),
          Text(listData[index]["title"]),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      // 循环数据的数量
      itemCount: listData.length,
      itemBuilder: this._getListData,
      // 设置主要布局
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        // 一行的 widget 数量
        crossAxisCount: 2,
        // 两个 Widget 的水平距离
        crossAxisSpacing: 20,
        // 两个 Widget 的垂直距离
        mainAxisSpacing: 20,
      ),
    );
  }
}

页面布局控件

Padding 控件

Padding 控件可以给子控件添加内边距:

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.fromLTRB(0, 0, 10, 0),
      child: GridView.count(
        crossAxisCount: 2,
        childAspectRatio: 1.7,
        children: <Widget>[
          Padding(
            padding: EdgeInsets.fromLTRB(10, 10, 0, 0),
            child: Image.network(
              "https://i0.hdslb.com/bfs/feed-admin/e03e0d22e0c361f1f0ec544dcd718e306b371980.jpg@880w_388h_1c_95q",
              fit: BoxFit.cover,
            ),
          ),
          Padding(...
        ],
      ),
    );
  }
}

Row 控件

Row 控件即行控件:

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 600,
      child: Row(
        // 主轴显示方式 
        mainAxisAlignment: MainAxisAlignment.center,
        // 次轴显示方式
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          IconContainer(Icons.home),
          IconContainer(Icons.search, color: Colors.yellow),
          IconContainer(Icons.mail, color: Colors.orange),
        ],
      ),
    );
  }
}

// 自定义按钮控件
class IconContainer extends StatelessWidget {
  double size;
  Color color;
  IconData icon;
  IconContainer(this.icon, {this.color = Colors.red, this.size = 32});
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 100,
      width: 100,
      color: color,
      child: Center(
        child: Icon(
          icon,
          size: size,
          color: Colors.white,
        ),
      ),
    );
  }
}

image.png

Column 控件

Column 控件即列控件,使用方法类似 Row 控件:

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 600,
      width: 200,
      child: Column(
        // 主轴显示方式 
        mainAxisAlignment: MainAxisAlignment.center,
        // 次轴显示方式
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          IconContainer(Icons.home),
          IconContainer(Icons.search, color: Colors.yellow),
          IconContainer(Icons.mail, color: Colors.orange),
        ],
      ),
    );
  }
}

image.png

Expanded 控件

Expanded 控件类似于 Web 开发中的 flex 布局。

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Expanded(
          // 类似 web 中的 flex 属性,此时 width 属性失效
          flex: 1,
          child: IconContainer(Icons.home),
        ),
        Expanded(
          flex: 2,
          child: IconContainer(Icons.search, color: Colors.yellow),
        ),
      ],
    );
  }
}
class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        // 下面的代码使得左侧固定宽度,右侧自适应
        Expanded(
          child: IconContainer(Icons.home),
        ),
        Expanded(
          flex: 2,
          child: IconContainer(Icons.search, color: Colors.yellow),
        )
      ],
    );
  }
}

image.png

Stack 控件

相当于 css 里的绝对定位。

Stack 控件将其中的子控件堆叠在一起,定位所有的子控件只需要使用 Stack 控件:

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      // 将 Stack 中的元素堆到一起
      child: Stack(
        // 表示让其中所有的元素居中
        // alignment: Alignment.center,
        // 自定义方位,参数为 x 和 y,值域为 -1 到 1
        alignment: Alignment(0, -0.1),
        // 数组中的内容按照先后次序堆叠
        children: <Widget>[
          Container(
            height: 400,
            width: 300,
            color: Colors.red,
          ),
          Text(
            "this is text1",
            style: TextStyle(fontSize: 30),
          )
        ],
      ),
    );
  }
}

image.png

与 Align 控件一起使用

如果想对 Stack 控件中的每个子控件分别定位,我们可以结合 Align 控件一起使用:

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        height: 400,
        width: 300,
        color: Colors.red,
        child: Stack(
          children: <Widget>[
            Align(
              alignment: Alignment.topLeft,
              child: Icon(
                Icons.home,
                size: 40,
                color: Colors.white,
              ),
            ),
            Align(
              alignment: Alignment.topCenter,
              child: Icon(
                Icons.search,
                size: 40,
                color: Colors.white,
              ),
            ),
            Align(
              alignment: Alignment.topRight,
              child: Icon(
                Icons.mail,
                size: 40,
                color: Colors.white,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

image.png

与 Position 控件一起使用

相较于Align 控件的 alignment 属性只能定义大致方位,Position 通过 top、 bottom、 left、 right 四个参数控制控件的方位,类似 CSS的绝对定位。

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        height: 400,
        width: 300,
        color: Colors.red,
        child: Stack(
          children: <Widget>[
            Positioned(
              left: 0,
              top: 0,
              child: Icon(
                Icons.home,
                size: 40,
                color: Colors.white,
              ),
            ),
            Positioned(
              left: 50,
              top: 50,
              child: Icon(
                Icons.search,
                size: 40,
                color: Colors.white,
              ),
            ),
            Positioned(
              left: 100,
              top: 100,
              child: Icon(
                Icons.mail,
                size: 40,
                color: Colors.white,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

image.png

AspectRatio 控件

可以调整子元素的宽高比。

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 300,
      child: AspectRatio(
        // 设置子元素的宽高比
        aspectRatio: 2.0 / 1.0,
        child: Container(
          color: Colors.pink,
        ),
      ),
    );
  }
}

Card 控件

实现卡片效果。

import 'res/listData.dart';
...

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      children: listData.map((value) {
        return Card(
          // 外边距
          margin: EdgeInsets.all(10),
          child: Column(
            children: <Widget>[
              AspectRatio(
                aspectRatio: 16 / 9,
                child: Image.network(
                  value["imgUrl"],
                  fit: BoxFit.cover,
                ),
              ),
              ListTile(
                // 添加头像
                leading: CircleAvatar(
                  backgroundImage: NetworkImage(value["avatar"]),
                ),
                title: Text(
                  value["title"],
                  // 最大行数
                  maxLines: 2,
                  // 文字溢出
                  overflow: TextOverflow.ellipsis,
                ),
                subtitle: Text(value["author"]),
              )
            ],
          ),
        );
      }).toList(),
    );
  }
}

image.png

Wrap 控件

单行的 Wrap 控件与 Row 控件几乎一致,当子控件所需的 mainAxis 主轴位置大于一行时, Wrap 控件会扩展到 crossAxis 交叉轴上显示。

首先定义一个 MyButton 控件,ElevatedButton 控件的详细用法在下文有介绍。

class MyButton extends StatelessWidget {
  final String? text;
  const MyButton(this.text);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      // 按下触发事件
      onPressed: () {},
      child: Text(text!),
    );
  }
}
class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Wrap(
      // 主轴方向,默认水平
      direction: Axis.horizontal,
      // 主轴对齐方式
      alignment: WrapAlignment.start,
      // 主轴子控件的间距
      spacing: 10,
      // 文本方向
      textDirection: TextDirection.ltr,
      // 交叉轴方向,默认从上到下
      verticalDirection: VerticalDirection.down,
      // 交叉轴对齐方式
      runAlignment: WrapAlignment.start,
      // 交叉轴子控件间距
      runSpacing: 2,
      children: <Widget>[
        MyButton("第 1 集"),
        MyButton("第 2 集"),
        MyButton("第 3 集"),
        MyButton("第 4 集"),
        MyButton("第 5 集"),
        MyButton("第 6 集"),
        MyButton("第 7 集"),
      ],
    );
  }
}

image.png

StatefulWidget 有状态控件

前面提到的控件都是 StatelessWidget 无状态控件,下面我们来讲 StatefulWidget 有状态控件。

StatefulWidget 持有的状态可能在 widget 生命周期内发生改变。如果想改变页面中的数据就需要用到 StatefulWidget

在 VSCode 中安装 Awesome Flutter Snippets 插件可用于自动生成自定义控件架构。

在无状态控件中实现点击按钮改变控件中数据的功能是不可行的:

class HomeContent extends StatelessWidget {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: <Widget>[
          SizedBox(height: 100),
          Text(
            "You have pressed the button $count time(s).",
            style: TextStyle(fontSize: 20),
          ),
          SizedBox(height: 100),
          ElevatedButton(
            onPressed: () {
              this.count++; // count 不发生变化
            },
            child: Text(
              "Press here!",
              style: TextStyle(fontSize: 20),
            ),
          )
        ],
      ),
    );
  }
}

使用有状态控件:

// 有状态控件
class HomeContent extends StatefulWidget {
  // 构造函数
  HomeContent({Key? key}) : super(key: key);

  @override
  _HomeContentState createState() => _HomeContentState(/* 可在此传参 */);
}

class _HomeContentState extends State<HomeContent> {
  int count = 0;
  
  // 可通过构造函数接收参数
  
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: <Widget>[
          SizedBox(height: 100),
          Text(
            "You have pressed the button $count time(s).",
            style: TextStyle(fontSize: 20),
          ),
          SizedBox(height: 100),
          ElevatedButton(
            onPressed: () {
              // 使用 setState 可以重新渲染
              setState(() {
                count++;
              });
            },
            child: Text(
              "Press Here!",
              style: TextStyle(fontSize: 20),
            ),
          ),
        ],
      ),
    );
  }
}

image.png

BottomNavigationBar 控件

用于配置底部的导航栏。

class MyAPP extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Tabs(),
      theme: ThemeData(
        primarySwatch: Colors.yellow,
      ),
    );
  }
}

class Tabs extends StatefulWidget {
  Tabs({Key? key}) : super(key: key);

  @override
  _TabsState createState() => _TabsState();
}

class _TabsState extends State<Tabs> {
  // 定义私有变量,表示选中的选项序号
  int _currentIndex = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Demo'),
      ),
      body: Text("TabBar"),
      bottomNavigationBar: BottomNavigationBar(
        // 默认选中的选项(从 0 开始)
        currentIndex: _currentIndex,
        // 点击回调函数,参数为点击的选项序号
        onTap: (int index) {
          setState(() {
            _currentIndex = index;
          });
        },
        // 设置图标大小
        // iconSize: 48.0,
        // 选中的选项颜色
        fixedColor: Colors.red,
        // 底部导航栏选中动画样式,默认 fixed,可选 shifting
        // type: BottomNavigationBarType.shifting,
        // 底部的选项卡
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: "Home",
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.category),
            label: "Category",
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            label: "Settings",
          ),
        ],
      ),
    );
  }
}

现在,我们可以通过点击导航栏的选项来改变导航栏中的图标效果。为了实现完整的功能,即点击选项跳转到相应的页面,我们还需要准备这些页面。

// lib/pages/tabs/Home.dart
import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
  HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Text("This is Home page.");
  }
}

// Category 和 Settings 页面同理

// lib/pages/Tab.dart 
import 'package:flutter/material.dart';
import './tabs/Category.dart';
import './tabs/Home.dart';
import './tabs/Settings.dart';

class Tabs extends StatefulWidget {
  ...
}

class _TabsState extends State<Tabs> {
  int _currentIndex = 0;
  // 存放相应的页面
  final List _pageList = [
    HomePage(),
    CategoryPage(),
    SettingsPage(),
  ];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Demo'),
      ),
      // 实现切换页面的功能
      body: _pageList[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(...
    );
  }
}

image.png

Navigator 路由控件

Flutter 通过 Navigator 控件以栈的形式管理路由并提供了相应的方法:Navigator.pushNavigator.pop

Flutter 提供了两种配置路由跳转的方式:基本路由和命名路由。

基本路由

实现界面的跳转和传值。

// 定义 Search 页面和 Form 页面
// lib/pages/Form.dart
import 'package:flutter/material.dart';

class FormPage extends StatelessWidget {
  String content;
  FormPage({this.content = "default content"});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("From Page"),
      ),
      body: Text(content),
      // 除了自带的返回按钮,我们也可以自己定义一个按钮实现相同的功能
      floatingActionButton: FloatingActionButton(
        child: Text("Back"),
        onPressed: () {
          // 路由跳转,出栈
          Navigator.of(context).pop();
        },
      ),
    );
  }
}
// lib/pages/tabs/Home.dart
import 'package:flutter/material.dart';
import '../Search.dart';
import '../Form.dart';

class HomePage extends StatefulWidget {
  ...
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          ElevatedButton(
            onPressed: () {
              // 路由跳转 入栈
              Navigator.of(context).push(
                MaterialPageRoute(
                  builder: (context) => SearchPage(),
                ),
              );
            },
            child: Text("点击跳转到搜索页面"),
          ),
          ElevatedButton(
            onPressed: () {
              // 路由跳转 入栈
              Navigator.of(context).push(
                MaterialPageRoute(
                  builder: (context) =>
                      FormPage(content: 'content provided by home page'),
                ),
              );
            },
            child: Text("点击跳转到表单页面并传值"),
          ),
        ],
      ),
    );
  }
}

命名路由

首先在根控件的 routes 参数中以 Map 的数据类型定义命名路由。

通过 Navigator.pushNamed(context, '/routeName') 跳转。

// main.dart 配置命名路由
import 'package:flutter/material.dart';
import 'pages/Tabs.dart';
import 'pages/Form.dart';
import 'pages/Search.dart';

void main() {
  runApp(MyAPP());
}

class MyAPP extends StatelessWidget {
  // 定义路由变量
  final routes = {
    '/form': (context, {arguments}) => FormPage(arguments: arguments),
    '/search': (context) => SearchPage(),
  };

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Tabs(),
      theme: ThemeData(
        primarySwatch: Colors.yellow,
      ),
      // routes: routes, 命名路由不传参时可以这样写
      // 命名路由传参时
      onGenerateRoute: (RouteSettings settings) {
        // 统一处理
        final String? name = settings.name;
        final Function? pageContentBuilder = routes[name];
        if (pageContentBuilder != null) {
          if (settings.arguments != null) {
            final Route route = MaterialPageRoute(
                builder: (context) =>
                    pageContentBuilder(context, arguments: settings.arguments));
            return route;
          } else {
            final Route route = MaterialPageRoute(
                builder: (context) => pageContentBuilder(context));
            return route;
          }
        }
      },
    );
  }
}

// Home.dart 使用命名路由跳转
...
class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          ElevatedButton(
            onPressed: () {
              // 路由跳转 入栈
              Navigator.pushNamed(context, '/search');
            },
            child: Text("点击跳转到搜索页面"),
          ),
          ElevatedButton(
            onPressed: () {
              // 路由跳转 入栈
              Navigator.pushNamed(context, '/form', arguments: {
                "content": 'content provided by home page',
              });
            },
            child: Text("点击跳转到表单页面并传值"),
          ),
        ],
      ),
    );
  }
}

// Form.dart 被跳转的路由
class FormPage extends StatelessWidget {
  Map? arguments;
  FormPage({this.arguments});
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("From Page"),
      ),
      body: Text(arguments != null ? arguments!["content"] : 'default content'),
      floatingActionButton: FloatingActionButton(
        child: Text("Back"),
        onPressed: () {
          // 自定义返回按钮
          // 路由跳转,出栈
          Navigator.of(context).pop();
        },
      ),
    );
  }
}

分离路由代码

由于路由部分的代码较为复杂,我们可以将路由部分的代码分离成单独的文件:

// lib/routes/Route.dart
import 'package:flutter/material.dart';
import '../pages/Form.dart';
import '../pages/Search.dart';
import '../pages/Tabs.dart';

final routes = {
  '/': (context) => Tabs(),
  '/form': (context, {arguments}) => FormPage(arguments: arguments),
  '/search': (context) => SearchPage(),
};

// 命名路由传参
Route? onGenerateRoute(RouteSettings settings) {
  // 统一处理
  final String? name = settings.name;
  final Function? pageContentBuilder = routes[name];
  if (pageContentBuilder != null) {
    if (settings.arguments != null) {
      final Route route = MaterialPageRoute(
          builder: (context) =>
              pageContentBuilder(context, arguments: settings.arguments)
      );
      return route;
    } else {
      final Route route = MaterialPageRoute(
              builder: (context) => pageContentBuilder(context)
      );
      return route;
    }
  }
}

// lib/main.dart
import 'package:flutter/material.dart';
import 'routes/Routes.dart';

void main() {
  runApp(MyAPP());
}

class MyAPP extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // home: Tabs(),
      // 初始化时加载的路由
      initialRoute: '/',
      theme: ThemeData(
        primarySwatch: Colors.yellow,
      ),
      onGenerateRoute: onGenerateRoute,
    );
  }
}

路由替换

  1. 返回到上一级
Navigator.of(context).pop();
  1. 替换路由

替换是指将当前页面路由出栈,把替换的路由入栈。

Navigator.of(context).pushReplacementNamed('/Search');
  1. 返回到根路由
Navigator.of(context).pushAndRemoveUntil(
    new MaterialPageRoute(
        // 跳转到下标为 1 的导航栏选项页
        builder: (context) => new Tabs(index: 1)
    ), 
    // 将路由栈置为空
    (route) => route == null,
);

// tab 页面有改动,可以接收导航栏选项下标,这里省略

AppBar 控件

AppBar 控件用来修改页面顶部内容。

class AppBarDemoPage extends StatelessWidget {
  const AppBarDemoPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("AppBarDemoPage"),
        // 标题是否居中显示
        centerTitle: true,
        // 改变导航栏的背景颜色
        backgroundColor: Colors.red,
        // 修改左侧的按钮图标,默认为返回上一级
        // leading: Icon(Icons.menu),
        // 不同于图标控件,图标按钮控件有点击事件
        leading: IconButton(
          onPressed: () {
            print("this is menu icon");
          },
          icon: Icon(Icons.menu),
        ),
        // 在导航栏尾部放图标
        actions: <Widget>[
          IconButton(
            onPressed: () {
              print("this is search icon");
            },
            icon: Icon(Icons.search),
          ),
          IconButton(
            onPressed: () {
              print("this is settings icon");
            },
            icon: Icon(Icons.settings),
          ),
        ],
      ),
      body: Text("AppBar Demo"),
    );
  }
}

// 去掉 debug 标签,在 main.dart 中
class MyAPP extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // 去掉 debug 图标
      debugShowCheckedModeBanner: false,
      ...
    );
  }
}

在 AppBar 中实现 Tabs 切换

class AppBarDemoPage extends StatelessWidget {
  const AppBarDemoPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: Text("顶部导航栏 Demo"),
          // TabBar 选项,在顶部 AppBar 的底部
          bottom: TabBar(
            tabs: <Widget>[
              Tab(text: "热门"),
              Tab(text: "推荐"),
            ],
          ),
        ),
        // TabBar 内容
        body: TabBarView(
          children: <Widget>[
            Text("这是热门内容"),
            Text("这是推荐内容"),
          ],
        ),
      ),
    );
  }
}

image.png

这样写导航栏会同时生成一个导航栏标题和导航栏 Tabs 选项。如果是在已有 AppBar 的页面中,会与原来的导航栏标题一起显示,出现两个导航栏标题。

image.png

去掉冗余的标题

为了解决这一问题,我们可以删去内部导航栏的标题,把 TabBar 控件放在 title 选项的里面。

class _CategoryPageState extends State<CategoryPage> {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          // 背景颜色
          backgroundColor: Colors.white,
          title: TabBar(
            // 指示器颜色
            indicatorColor: Colors.blue,       
            // 是否可滑动,默认 false
            isScrollable: true,
            tabs: <Widget>[
              Tab(text: "热门"),
              Tab(text: "推荐"),
            ],
          ),
        ),
        // TabBar 内容
        body: TabBarView(
          children: <Widget>[
            Text("这是热门内容"),
            Text("这是推荐内容"),
          ],
        ),
      ),
    );
  }
}

使用 TabController

使用 TabController 可以监听页面的变化并做出相应动作,可用于刷新页面等操作。

class TabBarControllerPage extends StatefulWidget {
  ...
}

// 混入 SingleTickerProviderStateMixin 类
class _TabBarControllerPageState extends State<TabBarControllerPage>
    with SingleTickerProviderStateMixin {
  // 定义 _tabController
  late TabController _tabController;

  // 实例化 TabController
  @override
  // 声明周期函数
  void initState() {
    super.initState();
    _tabController = TabController(vsync: this, length: 2);
    // 监听页面变化
    _tabController.addListener(() {
      print(_tabController.index);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("TabController Demo"),
        bottom: TabBar(
          // 定义 controller 参数
          controller: _tabController,
          tabs: <Widget>[
            Tab(text: "推荐"),
            Tab(text: "热榜"),
          ],
        ),
      ),
      body: TabBarView(
        // 定义 controller 参数
        controller: _tabController,
        children: <Widget>[
          Text("这是热门内容"),
          Text("这是推荐内容"),
        ],
      ),
    );
  }
}

Drawer 侧边栏控件

Drawer 控件实现抽屉功能,可以拉出侧边栏,同时给 AppBar 增加相应按钮。

class _TabsState extends State<Tabs> {
  int _currentIndex = 0;
  // 存放相应的页面
  final List _pageList = [...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(...
      // 实现切换页面的功能
      body: _pageList[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(...
      // 左侧侧边栏
      drawer: Drawer(
        child: Column(
          children: <Widget>[
            ListTile(
              leading: CircleAvatar(
                child: Icon(Icons.home),
              ),
              title: Text("我的空间"),
            ),
            // 添加分割线
            Divider(),
            ListTile(
              leading: CircleAvatar(
                child: Icon(Icons.people),
              ),
              title: Text("用户中心"),
            ),
          ],
        ),
      ),
      // 右侧侧边栏
      endDrawer: Drawer(
        child: Text('hello'),
      ),
    );
  }
}

DrawHeader

定义侧边栏头部:

class _TabsState extends State<Tabs> {
  int _currentIndex = 0;
  // 存放相应的页面
  final List _pageList = [...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(...
      // 实现切换页面的功能
      body: _pageList[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(...
      // 左侧侧边栏
      drawer: Drawer(
        child: Column(
          children: <Widget>[
            Row(
              children: [
                Expanded(
                  // 定义侧边栏头部
                  child: DrawerHeader(
                    child: Text("Hello Flutter"),
                    decoration: BoxDecoration(
                      // 背景颜色
                      color: Colors.blue[300],
                      // 背景图片
                      image: DecorationImage(
                          image: NetworkImage(
                              "https://i1.hdslb.com/bfs/face/4e5d0a51273fe3f8fabc700b6a71bb8a38c9e21e.jpg@240w_240h_1c_1s.webp"),
                          fit: BoxFit.contain),
                    ),
                  ),
                ),
              ],
            ),
            ListTile(...
          ],
        ),
      ),
    );
  }
}

UserAccoutsDrawerHeader

可以更方便地显示用户信息:

child: UserAccountsDrawerHeader(
  accountName: Text("罗翔说刑法"),
  accountEmail: Text("luoxiang@163.com"),
  // 头像
  currentAccountPicture: CircleAvatar(
    backgroundImage: NetworkImage(
      "https://i1.hdslb.com/bfs/face/4e5d0a51273fe3f8fabc700b6a71bb8a38c9e21e.jpg@240w_240h_1c_1s.webp",
    ),
  ),
  // 背景
  decoration: BoxDecoration(
    color: Colors.white,
    image: DecorationImage(
        image: NetworkImage(
          "https://i0.hdslb.com/bfs/space/cb1c3ef50e22b6096fde67febe863494caefebad.png@2560w_400h_100q_1o.webp",
        ),
        fit: BoxFit.cover),
  ),
  // 其他图片,被放在右上角
  // otherAccountsPictures: <Widget>[],
),

image.png

侧边栏的路由跳转

...
ListTile(
  leading: CircleAvatar(
    child: Icon(Icons.home),
  ),
  title: Text("我的空间"),
  onTap: () {
    // 返回时默认侧边栏是展开状态,打开侧边栏也是入栈,下一行代码弹出侧边栏状态,用于隐藏侧边栏
    Navigator.of(context).pop();
    Navigator.pushNamed(context, '/form');
  },
),
...

按钮控件

ElevatedButton

下面介绍按钮控件,包括普通按钮、带图标的按钮、圆形按钮、圆角按钮:

class ButtonPage extends StatelessWidget {
  const ButtonPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Button Demo"),
      ),
      body: Column(
        children: <Widget>[
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              // 在按钮外层加 Container 用来加宽度和高度,如果用 Expanded 包裹,则宽度与屏幕一致
              Container(
                height: 40,
                width: 160,
                child: ElevatedButton(
                  // 按下事件
                  onPressed: () {
                    print("ElevatedButton");
                  },
                  child: Text("ElevatedButton"),
                  style: ButtonStyle(
                    // 阴影效果
                    elevation: MaterialStateProperty.all(2),
                    // 按钮颜色
                    backgroundColor: MaterialStateProperty.all(Colors.orange),
                    // 字体颜色
                    foregroundColor: MaterialStateProperty.all(Colors.black),
                  ),
                ),
              ),
              // 带图标的按钮
              ElevatedButton.icon(
                onPressed: () {
                  print("ElevatedButton with Icon");
                },
                icon: Icon(Icons.search),
                label: Text("ElevatedButton with Icon"),
              )
            ],
          ),
          Row(
            children: <Widget>[
              // 圆形按钮
              ElevatedButton(
                onPressed: () {
                  print("Circle Button");
                },
                child: Text("Circle Button"),
                style: ButtonStyle(
                  shape: MaterialStateProperty.all(
                    CircleBorder(
                      side: BorderSide(
                        // 边框样式,默认 solid,若为 none 后两个参数不生效
                        style: BorderStyle.solid,
                        // 边框颜色
                        color: Colors.orange,
                        // 边框宽度
                        width: 1,
                      ),
                    ),
                  ),
                ),
              ),
              // 圆角按钮
              ElevatedButton(
                onPressed: () {
                  print("Round ButtonR");
                },
                child: Text("Round Button"),
                style: ButtonStyle(
                  shape: MaterialStateProperty.all(
                    RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(10),
                    ),
                    // 还可以使用 StadiumBorder
                    // StadiumBorder(
                    //   side: BorderSide(
                    //     // 边框样式
                    //     style: BorderStyle.none,
                    //   ),
                    // ),
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

TextButton、OutlinedButton、IconButton、ButtonBar

简单介绍一下文本按钮、轮廓按钮、图标按钮、按钮组的使用方法:

Row(
  children: <Widget>[
    // 文本按钮
    TextButton(
      onPressed: () {
        print("TextButton");
      },
      child: Text("TextButton"),
    ),
    // 轮廓按钮
    OutlinedButton(
      onPressed: () {
        print("OutlinedButton");
      },
      style: ButtonStyle(
        // 配置边框
        side: MaterialStateProperty.all(
          BorderSide(
            width: 1,
            color: Colors.grey,
          ),
        ),
      ),
      child: Text("OutlinedButton"),
    ),
    // 图标按钮
    IconButton(
      onPressed: () {
        print("IconButton");
      },
      icon: Icon(Icons.search),
    ),
  ],
),
Row(
  children: <Widget>[
    // 按钮组,可以放多个按钮
    ButtonBar(
      children: <Widget>[
        ElevatedButton(
          onPressed: () {},
          child: Text("ButtonBar 1"),
        ),
        ElevatedButton(
          onPressed: () {},
          child: Text("ButtonBar 2"),
        ),
      ],
    )
  ],
)

自定义按钮控件

自定义一个简单的按钮控件:

// 自定义按钮控件
class MyButton extends StatelessWidget {
  final String text;
  final pressed;
  const MyButton({this.text = "custom button", this.pressed, Key? key})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: pressed,
      child: Text(text),
    );
  }
}

image.png

FloatingActionButton 控件

class ButtonPage extends StatelessWidget {
  const ButtonPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(...
      // 浮动按钮
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add, color: Colors.black, size: 30),
        onPressed: () {
          print("floatingActionButton");
        },
        // 配置样式
        backgroundColor: Colors.yellow,
      ),
      // 浮动按钮位置
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      body: Column(...
    );
  }
}

与底部导航栏结合

浮动按钮常常与底部导航栏结合:

class _TabsState extends State<Tabs> {
  int _currentIndex = 0;
  // 存放相应的页面
  final List _pageList = [...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(...
      // 外层加 Container 便于调整大小
      floatingActionButton: Container(
        height: 64,
        width: 64,
        padding: EdgeInsets.all(8),
        // 调整按钮位置
        margin: EdgeInsets.only(top: 4),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(32),
          color: Colors.white,
        ),
        // 浮动按钮
        child: FloatingActionButton(
          child: Icon(Icons.add, color: Colors.black, size: 30),
          onPressed: () {
            print("floatingActionButton");
            // 跳转到相应页面
            setState(() {
              _currentIndex = 1;
            });
          },
          elevation: 0,
          // 配置样式,选中时改变颜色
          backgroundColor:
              _currentIndex == 1 ? Colors.yellow[600] : Colors.yellow,
        ),
      ),
     ...
    );
  }
}

表单

TextField

下面的代码可以提供默认值、获取输入框里的值:

class _TextFieldPageState extends State<TextFieldPage> {
  // 初始化时给表单赋值时需要实例化,否则直接 var _username;
  var _username = TextEditingController();
  var _password;

  // 赋初始值
  void initState() {
    super.initState();
    _username.text = "罗翔";
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Padding(
        padding: EdgeInsets.all(20),
        child: Column(
          children: [
            // 输入文本框
            TextField(
              decoration: InputDecoration(
                // 类似 placeholder
                hintText: "输入框的提示信息",
                // 默认一条下划线,改变为边框
                border: OutlineInputBorder(),
                // 可以添加标签名效果
                labelText: "用户名",
                // 添加图标
                // icon: Icon(Icons.people),
              ),
              // 默认值
              controller: _username,
              // 显示的行数,默认为 1
              // maxLines: 4,
              // 值为 true 时改为密码框,此时行数须为 1
              // obscureText: true,
              // 这里自动双向绑定输入框的 value 和 _username.text,所以不需要 onChange
            ),
            SizedBox(
              height: 20,
            ),
            TextField(
              decoration: InputDecoration(
                hintText: "输入密码",
                border: OutlineInputBorder(),
                labelText: "密码",
              ),
              obscureText: true,
              // 保存密码值
              onChanged: (value) {
                _password = value;
              },
            ),
            SizedBox(
              height: 20,
            ),
            Container(
              // 与外层同宽
              width: double.infinity,
              child: ElevatedButton(
                  onPressed: () {
                    print(_username.text);
                    print(_password);
                  },
                  child: Text("登录")),
            )
          ],
        ),
      ),
    );
  }
}

image.png

Checkbox 多选框

class _TextFieldPageState extends State<TextFieldPage> {
  bool? flag = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Padding(
        padding: EdgeInsets.all(20),
        child: Column(
          children: [
            Checkbox(
              // 是否选中
              value: flag,
              // 点击后触发事件
              onChanged: (value) {
                setState(() {
                  flag = value;
                });
              },
            ),
          ],
        ),
      ),
    );
  }
}

CheckboxListTail

CheckboxListTile(
  // 是否选中
  value: flag,
  // 点击后触发事件
  onChanged: (value) {
    setState(() {
      flag = value;
    });
  },
  title: Text('标题'),
  subtitle: Text('二级标题'),
  // secondary 用于添加单选框对面的内容,比如图标或者图片等
  secondary: Icon(Icons.home),
),

image.png

Radio

class _TextFieldPageState extends State<TextFieldPage> {
  Object? sex = 1;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Padding(
        padding: EdgeInsets.all(20),
        child: Column(
          children: [
            // 单选按钮
            Radio(
              // 按钮的值
              value: 1,
              // 按钮组的值,如果按钮组的值等于按钮的值,则选中该项
              groupValue: sex,
              onChanged: (value) {
                setState(() {
                  sex = value;
                });
              },
            ),
            Text("男"),
            Radio(
              value: 2,
              // 按钮组的序号
              groupValue: sex,
              onChanged: (value) {
                setState(() {
                  sex = value;
                });
              },
            ),
            Text("女"),
            Row(
              children: [
                Text(sex.toString()),
                Text(int.parse(sex.toString()) == 1 ? "男" : "女"),
              ],
            )
          ],
        ),
      ),
    );
  }
}

RadioListTail

class _TextFieldPageState extends State<TextFieldPage> {
  Object? sex = 1;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Padding(
        padding: EdgeInsets.all(20),
        child: Column(
          children: [
            RadioListTile(
              value: 1,
              groupValue: sex,
              onChanged: (value) {
                setState(() {
                  sex = value;
                });
              },
              // 相较于 Radio 新增
              title: Text("标题"),
              subtitle: Text("副标题"),
              // secondary 用于添加单选框对面的内容,比如图标或者图片等
              // 设置选中的高亮状态
              // selected: true,
            ),
            RadioListTile(
              value: 2,
              groupValue: sex,
              onChanged: (value) {
                setState(() {
                  sex = value;
                });
              },
              title: Text("标题"),
              subtitle: Text("副标题"),
            ),
          ],
        ),
      ),
    );
  }
}

image.png

Switch

class _TextFieldPageState extends State<TextFieldPage> {
  bool flag = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Padding(
        padding: EdgeInsets.all(20),
        child: Column(
          children: [
            Switch(
              value: this.flag,
              onChanged: (value) {
                setState(() {
                  flag = value;
                });
              },
            ),
          ],
        ),
      ),
    );
  }
}

image.png

日期控件

时间和时间戳转换

  1. 时间换时间戳
var now = new DateTime.now();
print(now.millisecondsSinceEpoch);//单位毫秒,13 位时间戳
  1. 时间戳换时间
var now = new DateTime.now();
var a=now.millisecondsSinceEpoch; //时间戳
print(DateTime.fromMillisecondsSinceEpoch(a));

引入第三方库 pub.dev/packages/da… 改变日期输出格式

自带日期控件

class _DatePageState extends State<DatePage> {
  var _now = DateTime.now(); //2021-12-07 22:46:48.717857

  _showDatePicker() async {
    // showDatePicker 类似于 js 的 promise 函数
    var result = await showDatePicker(
      context: context,
      // 初始化日期
      initialDate: _now,
      // 开始日期
      firstDate: DateTime(2010),
      // 截止日期
      lastDate: DateTime(2030),
    );
    print(result);

    setState(() {
      _now = result!;
    });
  }

  @override
  void initState() {
    super.initState();
    var a = _now.millisecondsSinceEpoch; //1638888408717
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Padding(
        padding: EdgeInsets.all(20),
        child: Column(
          children: [
            // 有触发波纹效果的控件
            InkWell(
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text("${formatDate(_now, [yyyy, "-", mm, "-", dd])}"),
                  Icon(Icons.arrow_drop_down),
                ],
              ),
              onTap: _showDatePicker,
            ),
          ],
        ),
      ),
    );
  }
}

image.png

自带时间控件

class _DatePageState extends State<DatePage> {
  TimeOfDay _nowTime = TimeOfDay(hour: 12, minute: 20);

  _showTimePicker() async {
    TimeOfDay? result = await showTimePicker(
      context: context,
      // 初始化时间
      initialTime: _nowTime,
    );

    setState(() {
      _nowTime = result!;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Padding(
        padding: EdgeInsets.all(20),
        child: Column(
          children: [
            InkWell(
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text("${_nowTime.format(context)}"),
                  Icon(Icons.arrow_drop_down),
                ],
              ),
              onTap: _showTimePicker,
            ),
          ],
        ),
      ),
    );
  }
}

image.png

设置语言为中文

  1. 配置依赖
flutter_localizations:
  sdk: flutter
  1. 在 main.dart 中导包
import 'package:flutter_localizations/flutter_localizations.dart'; 
  1. 在 main.dart 中设置
class MyAPP extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      ...
      // 配置
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: [
        const Locale('zh', 'CH'),
        const Locale('en', 'US'),
      ],
    );
  }
}

第三方日期控件库

flutter_datetime_picker | Flutter Package (pub.dev)

Dialog

AlertDialog

class _DialogPageState extends State<DialogPage> {
  _alertDialog() async {
    var result = await showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("提示信息"),
          content: Text("内容"),
          actions: <Widget>[
            TextButton(
              onPressed: () {
                // 按下按钮后关闭页面,并将 "cancel" 传递给 result
                Navigator.pop(context, 'cancel');
              },
              child: Text("取消"),
            ),
            TextButton(
              onPressed: () {
                Navigator.pop(context, 'confirm');
              },
              child: Text("确定"),
            ),
          ],
        );
      },
    );
    print(result);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Container(
        child: Column(
          children: [
            ElevatedButton(
              onPressed: _alertDialog,
              child: Text("AlertDialog"),
            )
          ],
        ),
      ),
    );
  }
}

image.png

SimpleDialog

class _DialogPageState extends State<DialogPage> {
  _simpleDialog() async {
    var result = await showDialog(
      context: context,
      builder: (context) {
        return SimpleDialog(
          title: Text("选择内容"),
          children: [
            SimpleDialogOption(
              child: Text("选项 1"),
              onPressed: () {
                Navigator.pop(context, '选项 1');
              },
            ),
            SimpleDialogOption(
              child: Text("选项 2"),
              onPressed: () {
                Navigator.pop(context, '选项 2');
              },
            ),
          ],
        );
      },
    );
    print(result);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Container(
        child: Column(
          children: [
            ElevatedButton(
              onPressed: _simpleDialog,
              child: Text("SimpleDialog"),
            )
          ],
        ),
      ),
    );
  }
}

image.png

showModalBottomSheet

class _DialogPageState extends State<DialogPage> {
  _showModalBottomSheet() async {
    var result = await showModalBottomSheet(
      context: context,
      builder: (context) {
        return Container(
          // 设置高度
          height: 200,
          child: Column(
            children: [
              ListTile(
                title: Text("点赞"),
                onTap: () {
                  Navigator.pop(context, 'like');
                },
              ),
              ListTile(
                title: Text("分享"),
                onTap: () {
                  Navigator.pop(context, 'share');
                },
              ),
              ListTile(
                title: Text("转发"),
                onTap: () {
                  Navigator.pop(context, 'forward');
                },
              ),
            ],
          ),
        );
      },
    );
    print(result);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Container(
        child: Column(
          children: [
            ElevatedButton(
              onPressed: _showModalBottomSheet,
              child: Text("showModalBottomSheet"),
            )
          ],
        ),
      ),
    );
  }
}

image.png

fluttertoast 第三方库

fluttertoast | Flutter Package (pub.dev)

自定义 Dialog

  1. 定义 MyDialog 控件
// lib/components/MyDialog.dart
import 'dart:async';
import 'package:flutter/material.dart';

class MyDialog extends Dialog {
  String? title;
  String? content;

  MyDialog(this.title, this.content);

  // 3 秒后关闭弹窗
  _showTimer(context) {
    var timer;
    timer = Timer.periodic(
      Duration(milliseconds: 1000),
      // 定时器结束触发回调函数
      (t) {
        Navigator.pop(context);
        t.cancel();
        // 或者使用 timer.cancel() 取消定时器
        // timer.cancel();
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    _showTimer(context);
    return Material(
      // 设定为半透明
      type: MaterialType.transparency,
      child: Center(
        child: Container(
          height: 300,
          width: 300,
          color: Colors.white,
          child: Column(
            children: [
              Padding(
                padding: EdgeInsets.all(10),
                child: Stack(
                  children: [
                    Align(
                      alignment: Alignment.center,
                      child: Text(title!),
                    ),
                    Align(
                      alignment: Alignment.centerRight,
                      child: InkWell(
                        child: Icon(Icons.close),
                        onTap: () {
                          Navigator.pop(context);
                        },
                      ),
                    )
                  ],
                ),
              ),
              Divider(),
              Container(
                padding: EdgeInsets.all(10),
                width: double.infinity,
                child: Text(
                  content!,
                  textAlign: TextAlign.left,
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}
  1. 引入 MyDialog
import '../../components/MyDialog.dart';
  1. 调用
class _DialogPageState extends State<DialogPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Container(
        child: Column(
          children: [
            ElevatedButton(
              onPressed: () {
                showDialog(
                  context: context,
                  builder: (context) {
                    return MyDialog("关于我们", "关于我们");
                  },
                );
              },
              child: Text("自定义 Dialog"),
            ),
          ],
        ),
      ),
    );
  }
}

image.png

网络请求

JSON 字符串与 Map 转换

import 'dart:convert';

void main(List<String> args) {
  Map mapData = {"name": "Jack", "age": "20"};
  String strData = '{"name":"Jack","age":"20"}';
  print(json.encode(mapData)); // Map 转 JSON {"name":"Jack","age":"20"}
  print(json.decode(strData)); // JSON 转 Map {name: Jack, age: 20}
}

http 第三方库

http | Dart Package (pub.dev)

根据说明文档配置和引入。

使用 get 和 post:

class _HomePageState extends State<HomePage> {
  String _msg = "";

  @override
  void initState() {
    super.initState();
  }

  _getData() async {
    var apiUrl = Uri.parse('https://jd.itying.com/api/httpGet');
    var response = await http.get(apiUrl);
    print('Response status: ${response.statusCode}'); // Response status: 200
    print('Response body: ${response.body}'); // 字符串
    // Response body: {"msg":"这是Get请求返回的数据"}

    setState(() {
      _msg = json.decode(response.body)["msg"];
    });
  }

  _postData() async {
    var apiUrl = Uri.parse('https://jd.itying.com/api/httpPost');
    var response = await http.post(
      apiUrl,
      body: {
        "username": "Jack",
        "age": "20",
      },
    );
    print('Response status: ${response.statusCode}'); // Response status: 200
    print('Response body: ${response.body}'); // 字符串
    // Response body: {"msg":"post成功","body":{"username":"Jack","age":"20"}}
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: [
          ElevatedButton(
            onPressed: _getData,
            child: Text("Get 请求"),
          ),
          Text(_msg),
          ElevatedButton(
            onPressed: _postData,
            child: Text("Post 请求"),
          ),
        ],
      ),
    );
  }
}

image.png

来个例子 🌰

import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;

class FoodPage extends StatefulWidget {
  FoodPage({Key? key}) : super(key: key);

  @override
  _FoodPageState createState() => _FoodPageState();
}

class _FoodPageState extends State<FoodPage> {
  List _list = [];

  @override
  void initState() {
    super.initState();
    _getData();
  }

  _getData() async {
    var apiUrl =
        Uri.parse('https://www.***.cn/resource/foods.json');
    var response = await http.get(apiUrl);
    // print('Response status: ${response.statusCode}');
    // print('Response body: ${json.decode(utf8.decode(response.bodyBytes))}');
    setState(() {
      _list = json.decode(utf8.decode(response.bodyBytes))["data"];
    });
  }

  List<Widget> _getColumn() {
    List<Widget> tmpList = [];
    for (int i = 0; i < _list.length; i++) {
      tmpList.add(
        Card(
          elevation: 0.5,
          child: Column(
            children: [
              Text(
                _list[i]["title"],
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
              ),
              Image.network(
                _list[i]["image"],
                height: 100,
                width: double.infinity,
                fit: BoxFit.contain,
              ),
              SizedBox(height: 10),
              Text(
                _list[i]["content"]["introduction"],
                textAlign: TextAlign.left,
              ),
              SizedBox(height: 10),
              Wrap(
                spacing: 10,
                alignment: WrapAlignment.start,
                children: [
                  Text(
                    _list[i]["content"]["comments"][0],
                    style: TextStyle(color: Colors.red[200]),
                  ),
                  Text(
                    _list[i]["content"]["comments"][1],
                    style: TextStyle(color: Colors.red[200]),
                  ),
                  Text(
                    _list[i]["content"]["comments"][2],
                    style: TextStyle(color: Colors.red[200]),
                  ),
                ],
              )
            ],
          ),
        ),
      );
    }
    return tmpList;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("诗与美食"),
        backgroundColor: Colors.red[50],
        foregroundColor: Colors.red[200],
        elevation: 0.5,
      ),
      body: _list.isNotEmpty
          ? SingleChildScrollView(
              child: Column(
                children: _getColumn(),
              ),
            )
          : Center(child: Text("加载中")),
    );
  }
}

image.png

自此,Flutter 的基础部分就学习完毕了。我们可以通过已学知识撸出简单的 APP 了!