基础组件

Text

创建文字,并设置其样式。示例如下

Text('this is Text Widget',
    style: TextStyle(
       fontSize: 30,
       fontFamily: 'Futura',
       color: Colors.blue,
    )
)

Icon

创建图形符号,Flutter 将会为 MaterialCupertino 的应用提前加载 icon。示例如下

Icon(
    Icons.widgets,
    size: 50,
    color: Colurs.blue,
)

Image

加载图片

web 端会出现跨域问题

  • Image():通用方法,使用 ImageProvider 实现,下面方法相当于别名,本质上也是使用这个方法
  • Image.asset():加载资源图片,需配置资源路径,见下文
  • Image.file():加载本地图片文件
  • Image.network():加载网络图片
  • Image.memory():加载 Uint8List 资源图片

配置资源路径

  • 在工程根目录下创建一个 images 目录,并将图片 avatar.png 拷贝到该目录。也可以设置其他自定义目录,如 lib 下面的 images,方便管理。
  • pubspec.yaml 中的 flutter 部分添加如下内容(yaml 文件对缩进严格,必须按照每一层两个空格缩进的方式进行缩进):
    assets:
      # 指定全目录
      - assets/images/
      # 指定文件
      - assets/images/avatar.png
    

各种方法使用示例

Image(
  image: AssetImage("assets/images/avatar.png"),
  width: 100.0
);

Image.asset("assets/images/avatar.png",
  width: 100.0,
)

Image(
  image: NetworkImage(
      "https://avatars2.githubusercontent.com/u/20411648"),
  width: 100.0,
)

Image.network(
  "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
  width: 100.0,
)

注意点

  • Imagewidthheight 参数,当不指定宽高时,图片会根据当前父容器的限制,尽可能的显示其原始大小。 如果只设置 widthheight 的其中一个,那么另一个属性默认会按比例缩放,可以通过 fit 属性来指定适应规则,具体介绍参考文档
  • Image 缓存,Image 加载过得图片只在内存中会有缓存,无法缓存本地。一旦应用关闭,这个缓存就没有了,下次重新启动还会从网络下载并缓存
  • 不能直接将 assets 目录设置为资源目录,要在里面新建文件夹

Card

Card 的通常用于是包裹其他元素,其只接收一个子元素,但可以为 RowColumn

ListTile

ListTile 是一个内置的列表样式组件,其包含 3 行文本(其实只有两行,第三行是 subtitle 的换行),以及可选的前后图标

若其效果无法满足,也可通过 Row 来手动实现

注意点

ListTile 自带点击水波纹效果,如果将其放在 Container 内部,且 Container 设置了背景色时,水波纹效果将会不显示。如果想设置背景色但是又不想遮挡水波纹效果,可将其放在 Ink 组件下,通过 Ink 组件设置背景色

Container(
    // 此组件不设置背景色
    child: Ink(
        decoration: BoxDecoration(
            color: Colors.pink[50],
            borderRadius: BorderRadius.circular(10.0),
        ),
        child: ListTile()
    )
)

Wrap

Row 是不会自动换行的,要实现自动换行,可使用 Wrap

Row 和 Column

RowColumn 是基于 webflexbox 布局模型设计的

mainAxisSize

mainAxisSize 属性决定了它们两个能在主轴上占据多大的空间,可选值如下

  • MainAxisSize.max:默认值,占据主轴上所有空间
  • MainAxisSize.min:仅根据它们的 children 所需要的空间

mainAxisAlignment

mainAxisSize: MainAxisSize.max 时,它们会使用额外空间来对齐其 children

mainAxisAlignment 属性决定了它们如何在额外空间中的对齐方式。可选值如下(参考 web 的 justify-content)

  • MainAxisAlignment.start:默认值,在主轴起点处对齐
  • MainAxisAlignment.end:在主轴终点处对齐
  • MainAxisAlignment.center:主轴中心对齐
  • MainAxisAlignment.spaceBetween:两端对齐,其余平均分配
  • MainAxisAlignment.spaceEvenly:所有空间平均分配
  • MainAxisAlignment.spaceAround:第一个 child 之前和最后一个 child 之后的空间为其他空间的一半

crossAxisAlignment

crossAxisAlignment 属性决定了它们如何在其交叉轴上对齐其 children。可选值如下(参考 web 的 align-items)

注意交叉轴默认跟随子元素的尺寸,若全员尺寸一样,则无法看到效果

  • CrossAxisAlignment.start:交叉轴靠前
  • CrossAxisAlignment.end:交叉轴靠后
  • CrossAxisAlignment.center:默认值,居中
  • CrossAxisAlignment.stretch:在交叉轴上进行拉伸填充
  • CrossAxisAlignment.baseline:根据基线对齐(仅限 Text,且要求 textBaseline 属性设置为 TextBaseline.alphabetic

Flexible

Flexible 包裹一个 Widget,可以让这个 Widget 变得可以调整大小。此时这个 Widget 就成为了 Flexible 的子节点,并被视为 flexible。可通过调整其 flexfit 属性来调整大小

  • flex:将自身的 flex 因子与其他的比较,以决定自身占剩余空间的比例
  • fit:决定 FlexibleWidget 是否能够填充所有剩余空间。有以下两个可选值
    • FlexFit.loose:默认值,使用自身作为首选大小
    • FlexFit.tight:强制充满所有剩余空间

Expanded

ExpandedFlexible 类似,不过它是强制占满剩余空间

SizedBox

SizedBox 用于创建精确的尺寸,其 widgetheight 属性用于创建大小

  • 当其包裹有子元素时,子元素会根据 SizedBox 的尺寸变化
  • 没有子元素时,则创建一个空的空间

Spacer

SpacerSizedBox 类似,也能创建空间,不过它是基于 flex 属性创建的,且其无法包裹子元素

Container

可以添加 paddingmarginborderbackground 等属性,只有一个子元素,但这个子元素可以是 RowColumn根 widget

其子元素将会自动充满所有剩余空间,即使是 Text 也一样

其装饰属性通过 decoration 属性表示,其中的 borderRadiusshape: BoxShape.circle 同时使用,因为会冲突

示例如下

Widget A(){
    return Container{
        decoration: const BoxDecoration(
            // 其背景色通过 color 表示
            color: Colors.pink,
            border: Border.all(width: 10, color: Colors.blue),
            borderRadius: const BorderRadius.all(Radius.circular(8))
        ),
        margin: const EdgeInsets.all(4),
        padding: const EdgeInsets.all(4),
        child: Text('123')
    }
}

GridView

用于创建网格,其支持滚动。基础用法示例如下

GridView(
    gridDelegate: ***,
    children: []
),

其中主要的参数 gridDelegate 有两个可选值

  • SliverGridDelegateWithFixedCrossAxisCount:用于固定列数的场景,其包含的参数如下
    • crossAxisCount:列数,即一行有几个子元素
    • mainAxisSpacing:主轴方向上的间距
    • crossAxisSpacing:交叉轴方向上的间距
    • childAspectRatio:子元素的宽高比例,当子元素宽高比不为 1 时,需要设置
  • SliverGridDelegateWithMaxCrossAxisExtent:用于子元素有最大宽度限制的场景,其包含的参数如下
    • maxCrossAxisExtent:子元素最大的宽度
    • mainAxisSpacing:主轴方向上的间距
    • crossAxisSpacing:交叉轴方向上的间距
    • childAspectRatio:子元素的宽高比例,当子元素宽高比不为 1 时,需要设置

GridView.count 和 GridView.extent

分别为上面两个参数的简便写法。示例如下

GridView.count(
    crossAxisCount: 5,
    mainAxisSpacing: 10,
    crossAxisSpacing: 10,
    children: []
)
GridView.extent(
    maxCrossAxisExtent: 100,
    mainAxisSpacing: 10,
    crossAxisSpacing: 10,
    children: []
)

GridView.builder()

用于生成大量网格的场景,其会对列表进行优化。基本用法示例如下

GridView.builder(
    gridDelegate: ***,
    itemCount: 10000,
    itemBuilder: (BuildContext context, index) {
        return Container(
            decoration: BoxDecoration(
                color: Colors.pink,
                border: Border.all(4)
            )
        )
    }
)

ListView

Column 类似,但其支持滚动

基本参数如下

  • scrollDirection:列表滚动方向,可选值为 Axis.horizontal(默认值)、Axis.horizontal
  • controller:控制器,与列表滚动相关,如监听列表的滚动事件
  • physics:列表滚动到边缘后继续拖动的物理效果。当列表的长度没有填满屏幕时,会无法触发滑动效果,从而导致无法上拉和下拉,此时需要手动将该值设置为以下一项,建议设置为 AlwaysScrollableScrollPhysics
    • ClampingScrollPhysicsAndroid 平台默认效果
    • BouncingScrollPhysicsiOS 平台默认效果
    • AlwaysScrollableScrollPhysics:跟随各自平台效果
    • NeverScrollableScrollPhysics:禁用拖动效果
  • shrinkWrap:决定列表的长度是否仅包裹其内容的长度。当 ListView 嵌在一个无限长的容器中时,必须设置为 true
  • padding:内边距
  • itemExtent:子元素长度。当每一项的长度是固定时,可以指定该值,有助于提高性能
  • cacheExtent:预渲染区域长度。ListView 会在其可视区域的两边留一个 cacheExtent 长度的区域作为预渲染区域(对于 ListView.buildListView.separated 构造函数创建的列表,不在可视区域和预渲染区域内的子元素不会被创建或会被销毁)
  • children:子元素

ListView.builder()

用于生成大量列表的场景,会对长列表进行优化。基本用法示例如下

ListView.builder(
    itemCount: 10000,
    itemBuilder: (BuildContet context, index) {
        return Container(
            decoration: BoxDecoration(
                color: Colors.pink,
                border: Border.all()
            )
        )
    }
)

ListView.separated()

带分割线的 ListView

ListView.builder() 一样,不过多了一个 separatorBuilder 必填参数,用于定义分割线组件。示例如下

ListView.separated(
    itemCount: 10000,
    itemBuilder: (BuildContext context, index) {
        return Container(
            decoration: BoxDecoration(
                color: Colors.pink,
                border: Border.all(4)
            )
        )
    },
    separatorBuilder: (BuildContext context, index) {
        return Divider(
            height: 0.5,
            indent: 75,
            color: Colors.blur
        )
    }
)

下拉刷新

使用内置的 RefreshIndicator 组件。基本用法示例如下

RefreshIndicator(
    onRefresh: onRefresh,
    child: ListView.builder(
        itemCount: 100,
        itemBuilder: (BuildContext context, index) {
            return Container(
                decoration: BoxDecoration(
                    color: Colors.pink,
                    border: Border.all(4)
                )
            )
        }
    )
)

上拉加载

没有内置的组件,需要自己完成,需要用到前面提到的 controller 属性。基本代码示例如下

ListView.separated(
    controller: scrollController, // 监听事件
    itemCount: list.length + 1, // 元素加多 1 个,用于渲染加载提示
    separatorBuilder: (BuildContext context, index) {
        return Divider(height: .5, color: Colors.pink);
    },
    itemBuilder: (BuildContext context, index) {
        if (index < list.length) {
            return NewsCard(data: list[index]);
        } else {
            // 手动加入加载提示
            return renderBottom();
        }
    },
);

Stack

Stack 组件包裹的子元素将会堆叠在一起,排在后面的元素的会在上面,可通过 alignment 属性调整除第一个元素外的其他元素的堆叠位置

Positioned

Stack 通常搭配 Positioned 使用,Positioned 用于设置其子元素的位置

FractionallySizedBox

可以将一个组件的尺寸设置为其父级的百分比,虽然 CenterAlign 都有相应的缩放属性,但是它们两个容易受到父级约束的影响,因此尽量使用 FractionallySizedBox

FractionallySizedBox(
    widthFactor: 0.5, // 将子级的宽度设置为父级宽度的 50%
    heightFactor: 0.5, // 将子级的高度设置为父级高度的 50%
    child: Container(
        color: Colors.red
    )
)

Visibility

该组件可根据某个变量来设置其子组件是否显示

Visibility(
  visible: true,
  child: Text(
    'Your text here',
    style: TextStyle(fontSize: 16),
  ),
)

RefreshIndicator

该组件用于下拉刷新

一个很常见的情况是,下拉的图标已消失,但是数据还未返回,原因是 setStateonRefresh 是互斥的,此时需要通过 StreamBuilder 来实现

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


class DiyBuilder extends StatefulWidget {
  final Function() getData;
  final Widget Function(BuildContext context, dynamic data, int index) onData;

  const DiyBuilder({
    super.key,
    required this.getData,
    required this.onData,
  });

  
  State<DiyBuilder> createState() => _DiyBuilderState();
}

class _DiyBuilderState extends State<DiyBuilder> {
  var listData = [];
  var isLoading = false;
  var loadStatus = 'more';
  var pageNo = 1;
  var totalPages = 1;
  var isRefresh = false;

  final StreamController<List> _streamController = StreamController<List>();

  void _beforeData() {
    if (isLoading) {
      return;
    }
    isLoading = true;
  }

  void _setData(data) {
    if (isRefresh) {
      listData = data['records'];
    } else {
      listData = [...listData, ...data['records']];
    }
    totalPages = data['totalPages'];
    if (pageNo >= totalPages) {
      loadStatus = 'no';
    } else {
      loadStatus = 'more';
    }
    _streamController.sink.add(listData);
    isRefresh = false;
    isLoading = false;
  }

  Future<void> _getData() async {
    _beforeData();
    Map<String, dynamic> data = await widget.getData();
    _setData(data);
  }

  // 必须与 _getData() 分开写,否则下拉刷新的图标会立即消失
  Future<void> _onRefresh() async {
    _beforeData();
    isRefresh = true;
    pageNo = 1;
    Map<String, dynamic> data = await widget.getData();
    _setData(data);
  }

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

  
  void dispose() async {
    // 为避免退出页面时仍发送中的请求,需要使用 Future.wait 等待
    // 后续发现,即使不添加也不会导致报错,不知为何原因
    // 如确需取消 Future,建议使用 async 包(非 dart 自带的 async),示例见后面章节
    // await Future.wait([_getData(), _onRefresh()]);
    _streamController.close();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: _onRefresh,
      child: StreamBuilder(
        stream: _streamController.stream,
        builder: (context, snapshot) {
          final List? data = snapshot.data;
          // 判断数据是否存在
          if (snapshot.hasData && data != null) {
            return ListView.builder(
              ...
            );
          }
          return const Text('加载中...');
        },
      ),
    );
  }
}

Notification

Notification 用于实现组件之间的通信,不需要通过父子组件传递数据。当一个 Notification 被触发时,Flutter 会自动地沿着当前组件树向上传递通知,直到遇到处理这个通知的组件为止。用法示例如下

// 定义通知
class MyNotification extends Notification {
  final String message;
  MyNotification(this.message);
}

// 接收通知的组件
class MyWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        MyNotification('Hello').dispatch(context);
      },
      child: Container(
        color: Colors.white,
        child: Center(
          child: Text('Tap here'),
        ),
      ),
    );
  }
}

// 处理通知的组件
class MyListenerWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return NotificationListener<MyNotification>(
      onNotification: (notification) {
        // 处理通知
        print(notification.message);
        return true;
      },
      child: Container(
        color: Colors.white,
        child: Center(
          child: Text('Notification Listener'),
        ),
      ),
    );
  }
}

// 在父组件中组合 MyWidget 和 MyListenerWidget
class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            MyWidget(),
            MyListenerWidget(),
          ],
        ),
      ),
    );
  }
}

Form

Form 是一个内置的表单组件,通过给它的 key 属性配置一个值,然后可以通过该值调用其中的一些方法,如表单验证


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

  
  State<FormWidget> createState() => _FormWidgetState();
}

class _FormWidgetState extends State<FormWidget> {
  final _formKey = GlobalKey<FormState>();

  
  Widget build(BuildContext context) {
    return Form(
        key: _formKey,
        child: Column(
            TextFormField(),
            ElevatedButton(
              onPressed: () {
                if (_formKey.currentState!.validate()) {
                  DiyToast.showToast('提交成功');
                }
              },
              child: const Text('提交'),
            )
        ),
    )
  }
}

TextFormField

该组件通常搭配 Form 组件使用,注意它自带的验证信息会影响布局,因此用的时候要根据实际情况考虑是否自定义验证

TextFormField(
    focusNode: _focusNode,
    controller: _controller,
    decoration: InputDecoration(
        border: InputBorder.none,
        hintText: 'placeholder 文本',
        hiteStyle: TextStyle(),
        validator: (value) {
          if (value == null || value.isEmpty) {
            return '请输入手机号码';
          }
          return null;
        },
        errorStyle: TextStyle(), // 可通过将验证失败提示文本的大小设置为 0 来避免布局变换
    )
)

BottomNavigationBar

该组件用来实现底部的 tabbar 效果,通过 IndexedStack 来管理页面的堆栈,这样,每次切换选项时,只有当前页面会被重新构建,而其他页面会保持在堆栈中

import 'package:flutter/material.dart';
import './pages/order/main/index.dart';
import './pages/service/main/index.dart';
import './pages/home/main/index.dart';
import './pages/user/main/index.dart';

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

  
  State<TabbarWidget> createState() => _TabbarWidgetState();
}

class _TabbarWidgetState extends State<TabbarWidget> {
  int _selectedIndex = 0;
  static const List<Widget> _widgetOptions = <Widget>[
    HomePage(),
    OrderPage(),
    ServicePage(),
    UserPage(),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _selectedIndex,
        children: _widgetOptions,
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: '首页',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.list_alt),
            label: '订单',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.face),
            label: '服务',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: '我的',
          )
        ],
        currentIndex: _selectedIndex,
        selectedItemColor: Theme.of(context).primaryColor,
        unselectedItemColor: Colors.grey,
        onTap: _onItemTapped,
        type: BottomNavigationBarType.fixed,
      ),
    );
  }
}

SafeArea

用于处理屏幕上的安全区域。安全区域是指屏幕边缘周围的区域,用于避免内容与设备边缘重叠,例如刘海、屏幕凹口、状态栏和底部导航栏等

它会自动适应设备的安全区域,并将其内部的内容放置在安全区域内,以确保内容不会被遮挡或覆盖。它通常用作其他组件的父级组件,以确保这些组件在安全区域内进行布局

WillPopScope

WillPopScope 组件,用于拦截返回按钮,用法示例如下

class LoginPage extends StatelessWidget {
  const LoginPage({super.key});

  
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {

        // 返回 false 以阻止页面返回
        return false;
      },
      child: const Scaffold(
        body:  Center(
          child: Text('登录页面'),
        ),
      ),
    );
  }
}

SingleChildScrollView

当软键盘弹起导致页面超出范围时,使用改组件包裹页面其他组件即可

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Keyboard Overflow Example'),
        ),
        body: SingleChildScrollView(
          child: Padding(
            padding: EdgeInsets.all(16.0),
            child: Column(
              children: [
                // 页面内容
                TextField(
                  decoration: InputDecoration(
                    labelText: 'Input 1',
                  ),
                ),
                TextField(
                  decoration: InputDecoration(
                    labelText: 'Input 2',
                  ),
                ),
                // 添加更多的页面内容
              ],
            ),
          ),
        ),
      ),
    );
  }
}

TabBar

该组件用于实现常见的 tabs 切换效果。基础示例如下

import 'package:flutter/material.dart';

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

  
  State<TabWidget> createState() => _TabWidgetState();
}

class _TabWidgetState extends State<TabWidget> with TickerProviderStateMixin {
  late TabController _tabController;

  
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
  }

  
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        TabBar(
          controller: _tabController,
          tabs: [
            Tab(text: '首页'),
            Tab(text: '资讯'),
            Tab(text: '推荐'),
          ],
        ),
        Expanded(
          child: TabBarView(
            controller: _tabController,
            children: [
              Text('首页'),
              Text('资讯'),
              Text('推荐'),
            ],
          ),
        ),
      ],
    );
  }
}
Last Updated:
Contributors: af