Flutter 学习笔记-基础篇 Flutter 学习笔记-基础篇

Flutter 学习笔记-基础篇

如果你要获取与该笔记配套的源码,请点击这里

一、常用命令及快捷键

1. 常用命令

flutter doctor  //Flutter自检命令
flutter run         //运行项目到设备上

2. 常用快捷键

r 键:热加载(快速部署)。

p 键:显示网格,用于开发时调试UI。

o 键:切换Android和iOS的预览模式。

q 键:退出调试预览模式。

以上几个按键需要已经执行过flutter run之后才可以在终端中使用。

二、基本Widget组件

Flutter中一切皆组件(Widget)。所有的自定义组件、系统组件、自定义页面等其实都是Widget的子类。每个widget的构造函数都有一个key参数,这个参数的作用是什么呢?Key用于在widget的位置改变时保留其状态。比如,保留用户的滑动位置,或者在保留widget状态的情况下修改一个widget集合,如Row、Column等,这一篇博客详细d 讲解了Widget中的key。

1. 有状态无状态组件。

Flutter中如果要自定组件,一般都继承自有状态组件或无状态组件(小到组件大到页面),如果一个页面加载出来之后就不会再改变了那么就继承自StatelessWidget,如果加载之后还需要根据数据的变化而变化,那么就需要继承自StatefulWidget。

​ 1). StatelessWidget 无状态组件。

​ StatelessWidget组件是一个抽象类,继承该组件必须要实现Widget build(BuildContext context)方法。

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return Text("Hello Flutter!");
  }
}

​ 2). StatefulWidget 有状态组件。

​ StatefulWidget是一个抽象组件,继承该组件必须要实现State<StatefulWidget> createState()方法。该方法需要返回一个State类的实例。而State是一个抽象类,要继承State需要实现Widget build(BuildContext context)抽象方法,并制定泛型为拥有该State的Widget。

​ 如果要实现改变状态,需要使用State类中的setState方法。下面的栗子是点击按钮后按钮上面的数字动态加1,具体代码如下:

class TestPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(20),
      child: Custom(),
    );
  }
}

class Custom extends StatefulWidget{
  @override
  State<StatefulWidget> createState() {
    return _CustomState();
  }
}

class _CustomState extends State<Custom>{
  var myNumber = 0;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text("$myNumber"),
        SizedBox(height: 20),
        RaisedButton(
          onPressed: () {
            setState(() {
              myNumber++;
            });
          },
          child: Text('增加'),
        )
      ],
    );
  }
}

2. 装饰组件

​ 1). MaterialApp

​ MaterialApp是一个方便的Widget,他封装了应用程序实现 Material Design 所需要的一些Widget。一般作为一个页面的最顶层的Widget使用。

参数 类型 可选 默认值 说明
home Widget -- 页面的具体内容组件。
title String '' 页面标题。
color Color -- 页面颜色。
theme ThemeData -- 页面主题。
routes Map<String, WidgetBuilder> const <String, WidgetBuilder>{} 路由。

​ 2). Scaffold

​ Scaffold 是 Material Design 布局结构的基本实现。此类提供了用于显示 drawer、snackbar 和底部的 sheet 的参数。

参数 类型 可选 默认值 说明
appBar Widget -- 导航栏
body Widget --
drawer Widget --
bottomNavigationBar Widget --

​ 3). InkWell & GestureDetector 手势组件

​ 在Flutter中并不是所有的Widget都支持点击事件,但是如果我们想给某些我们自己的Widget设置点击事件时应该怎么办呢(例如我们列表中的item)?这时我们只需要使用InkWell将我们要设置点击事件的Widget包裹起来就可以了。

参数 类型 可选 默认值 说明
child Widget -- 子组件
onTap GestureTapCallback -- 单击事件回调
onDoubleTap GestureTapCallback -- 双击事件回调
onLongPress GestureLongPressCallback -- 长按事件回调

​ GestureDetector和InkWell用法基本一致,只是GestureDetector比InkWell多了更多回调设置,可以监听更多的事件。

3. Container 容器组件

参数 类型 可选 默认值 说明
constraints BoxConstraints -- 用不约束宽高(最大宽高、最小宽高等)
alignment AlignmentGeometry -- 对齐方式
decoration Decoration -- 设置边框装饰
padding EdgeInsetsGeometry -- 内边距
margin EdgeInsetsGeometry -- 外边距
transform Matrix4 -- 旋转、平移

4. Text 文本组件。

参数 类型 可选 默认值 说明
data String -- 用于设置现在界面上的文字
textAlign TextAlign -- center:居中 / left:居左 / right:居右 / justfy:两端对齐
textDirection TextDirection -- 文本方向,ltr:从左至右 rtl:从右至左
overflow TextOverflow -- 文字超出屏幕后的处理方式,clip:裁剪 fade:渐隐 ellipsis:省略号
style TextStyle -- 用于设置文字的样式
maxLines int -- 用于设置文字的最大显示行数
textScaleFactor double -- 字体显示倍率

5. ListView 列表组件。

参数 类型 可选 默认值 说明
itemCount Int 必填 -- 设置列表一共有多少个条目
itemBuilder Widget Function(BuildContext context, int index) 必填 -- 用于构建item(cell)视图的回调方法。
scrollDirection Axis Axis.vertical 滚动方向:horizontal-横向 vertical纵向。
reverse bool false 是否反向排列。
keyboardDismissBehavior ScrollViewKeyboardDismissBehavior ScrollViewKeyboardDismissBehavior.manual 滚动时键盘的处理方式。

静态列表

@override
Widget build(BuildContext context) {
  return ListView(
    children: [
      Text('我是一个条目'),
      Text('我是一个条目'),
      Text('我是一个条目'),
      Text('我是一个条目'),
      Text('我是一个条目'),
      Text('我是一个条目'),
    ],
  );
}

动态列表

@override
Widget build(BuildContext context) {
  return ListView.builder(
      itemCount: 5, 
      itemBuilder: (context, position){
        return ListTile(
          leading: item.avatarUrl,
          title: item.name,
          subtitle: item.desc,
        );
      }
  );
}

6. GridView

参数 类型 可选 默认值 说明
crossAxisSpacing double -- 水平间距
mainAxisSpacing double -- 垂直间距
scrollDirection Axis Axis.vertical 滚动方向:horizontal-横向 vertical纵向。
reverse bool false 是否反向排列。
childAspecet double -- item的宽高比例。
crossAxisCount int -- 设置列数
gridDelegate SliverGridDelegate 必填 -- builder专用。用于设置间距列数等。
itemBuilder Widget Function(BuildContext context, int index) 必填 -- builder专用。用于构建item(cell)视图的回调方法。

静态列表

@override
Widget build(BuildContext context) {
  return GridView.count(
      crossAxisCount: 2,
      children: [
        Text('我是一个条目'),
        Text('我是一个条目'),
        Text('我是一个条目'),
        Text('我是一个条目'),
        Text('我是一个条目'),
        Text('我是一个条目'),
        Text('我是一个条目'),
        Text('我是一个条目'),
        Text('我是一个条目'),
        Text('我是一个条目')
      ],
  );
}

动态列表

@override
Widget build(BuildContext context) {
  return GridView.builder(
      itemCount: 600,
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3
      ),
      itemBuilder: (context, position){
        return Text('我是第$position个条目');
      }
  );
}

7. Padding 内边距组件

参数 类型 可选 默认值 说明
padding EdgeInsetsGeometry 必填 -- 用于设置内边距,其实是设置child的外边距。
child Widget -- 子元素。

8. Row 水平布局组件

参数 类型 可选 默认值 说明
mainAxisAlignment MainAxisAlignment MainAxisAlignment.start 主轴的排序方式
crossAxisAlignment CrossAxisAlignment CrossAxisAlignment.center 次轴的排序方式
children List<Widget> const <Widget>[] 子组件

9. Column 垂直布局组件

参数 类型 可选 默认值 说明
mainAxisAlignment MainAxisAlignment MainAxisAlignment.start 主轴的排序方式
crossAxisAlignment CrossAxisAlignment CrossAxisAlignment.center 次轴的排序方式
children List<Widget> const <Widget>[] 子组件

10. Expanded 组件,类似Web中的Flex布局。

Expanded组件作为Column或Row组件的子组件使用。可设置当前组件占用父组件的比例。

参数 类型 可选 默认值 说明
flex Int 1 占父组件的比例(权重)
child Widget 必填 -- 子组件

11. SizedBox 占位组件

参数 类型 可选 默认值 说明
width double -- 宽度
height double -- 高度
child Widget 必填 -- 子组件

12. Stack 层叠组件

Stack组件可以单独使用,也可配合 Align 和 Positioned 实现定位布局。其实Stack有点类似于Android原生的帧布局。

参数 类型 可选 默认值 说明
alignment AlignmentGeometry AlignmentDirectional.topStart 所有子组件的显示位置
children List<Widget> const <Widget>[] 子组件
overflow Overflow Overflow.clip 子组件溢出后的处理方式
Stack(
      alignment: Alignment.center,
      children: [
        Align(
          alignment: Alignment.topCenter,
          child: Container(
            width: 100,
            height: 100,
            color: Colors.blue,
          ),
        ),
        Container(
          width: 100,
          height: 100,
          color: Colors.orange,
        ),
        Positioned(
          bottom: 10,
          child: Container(
            width: 100,
            height: 100,
            color: Colors.green,
          ),
        )
      ],
    );

13. Align

参数 类型 可选 默认值 说明
alignment AlignmentGeometry Alignment.center 所有子组件的显示位置
child Widget -- 子组件

14. Positioned

参数 类型 可选 默认值 说明
top double -- 距顶部的距离
bottom double -- 距底部的距离
left double -- 距左边的距离
right double -- 距右边的距离
child Widget -- 子组件
width double -- 宽度
height double -- 高度

15. AspectRatio

作用是可以设置子组件的宽高比。

参数 类型 可选 默认值 说明
aspectRatio double -- 设置宽高比例
child Widget -- 子组件

16. Card

参数 类型 可选 默认值 说明
margin EdgeInsetsGeometry -- 外边距
shape ShapeBorder -- 边框及阴影效果,还可以使用RoundedRectangleBorder对其设置圆角
clipBehavior Clip Clip.none 对子组件的裁剪方式
elevation doublle 1.0 阴影高度
child Widget -- 子组件

17. CircleAvatar 原型头像组件

参数 类型 可选 默认值 说明
backgroundImage ImageProvider -- 设置头像
onBackgroundImageError Function(dynamic exception, StackTrace stackTrace) -- 图片加载失败的回调

18. Wrap组件(流式布局组件)

参数 类型 可选 默认值 说明
spacing double 0.0 主轴的子组件之间的间隔
runSpacing double 0.0 次轴的子组件之间的间隔
direction Axis Axis.horizontal 子组件的排列方向,horizontal横向,vertical纵向。
alignment WrapAlignment WrapAlignment.start 主轴子组件的对齐方式。
runAlignment WrapAlignment WrapAlignment.start 次轴子组件的对齐方式。
textDirection TextDirection -- 文本的排列方向。

19. RaisedButton & MaterialButton & FlatButton & OutlineButton & IconButton & FloatingActiongButton 按钮组件。

Flutter中给我们预先定义好了一些按钮控件给我们用,常用的按钮如下

  • RaisedButton :凸起的按钮,其实就是Android中的Material Design风格的Button ,继承自MaterialButton

    如果要让RaisedButton支持图标,那么可以使用RaisedButton.icon()

  • FlatButton :扁平化的按钮,继承自MaterialButton

  • OutlineButton :带边框的按钮,继承自MaterialButton

  • IconButton :图标按钮,继承自StatelessWidget

  • FloatingActionButton : 浮动按钮,继承自StatelessWidget

按钮通常情况下是不能直接设置宽高的,如果要设置宽高可是在外层包一个Container组件。也可以在外层包一个Expanded组件使其可以自适应宽度。

常用属性如下

参数 类型 可选 默认值 说明
onPressed VoidCallback 必传 -- 按下按钮时触发的回调方法,传null表示按钮禁用,会显示禁用相关样式。
child Widget -- 子组件,不传就无法显示内容。
textColor Color -- 文本颜色
color Color -- 按钮的颜色
disabledColor Color -- 按钮禁用时的颜色
disabledTextColor Color -- 按钮禁用时的文本颜色
splashColor Color -- 点击按钮时水波纹的颜色
highlightColor Color -- 点击(长按)按钮后按钮的颜色
elevation double -- 阴影的范围,值越大阴影范围越大
padding EdgeInsetsGeometry -- 内边距
shape ShapeBorder -- 设置按钮的形状
minWidth double -- 最小宽度
height double -- 高度
materialTapTargetSize MaterialTapTargetSize MaterialTapTargetSize.padded 由于按钮按下后通常会有阴影效果,所以按钮默认都会有边距,如果不希望有边距可以通过该属性设置,MaterialTapTargetSize.padded:有边距。MaterialTapTargetSize.shrinkWrap:无边距。

20. Chip 碎片组件

该组件一般用作标签。

参数 类型 可选 默认值 说明
avatar Widget -- 一般用来设置左边的图标。
label Widget 必填 -- 一般用来设置文字。
deleteIcon Widget -- 删除图标,如果设置了onDeleted回调,即使该参数不设置也会显示默认图标。
deleteIconColor Color -- 删除图标的颜色。
deleteButtonTooltipMessage String delete 删除图标被按压时的提示文字。
shape ShapeBorder 默认为两边是半圆的形状 设置背景形状。
backgroundColor Color -- 背景颜色。
materialTapTargetSize MaterialTapTargetSize MaterialTapTargetSize.padded 由于按钮按下后通常会有阴影效果,所以按钮默认都会有边距,如果不希望有边距可以通过该属性设置,MaterialTapTargetSize.padded:有边距。MaterialTapTargetSize.shrinkWrap:无边距。

21. BottomNavigationBar 底部导航条组件

参数 类型 可选 默认值 说明
currentIndex int -- 当前选中的Tab的索引。
items List<BottomNavigationBarItem> 必填 -- 设置当前有多少个Item。
iconSize double 24 图标大小
fixedColor Color -- 选中后图标及字体的颜色。该属性通常不需要设置,会默认使用主体颜色。
type BottomNavigationBarType -- BottomNavigationBarType.fixed : 自动将所有item都显示到屏幕上。

22. AppBar 导航栏组件

参数 类型 可选 默认值 说明
title Widget -- 标题
centerTitle bool false 标题是否居中显示
actions List<Widget> -- 右边菜单
leading Widget -- 左边的导航图标

23. DefaultTabController 组件(实现类似于Android中的TabLayout效果)

要实现类似Android中的LabLayout效果还需要结合另外两个组件TabBarTabBarView一起使用,以下是这三个组件的具体参数介绍以及代码示例:

  1. DefaultTabController

    参数 类型 可选 默认值 说明
    length int 必填 -- 设置有多少个tab。
    initialIndex int 0 默认选中第几个tab,索引从0开始。
    child Widget 必填 -- child通常是一个Scaffold组件,但是需要有特俗的写法。下面会给出栗子。
    1. TabBar
    参数 类型 可选 默认值 说明
    isScrollable bool false 设置是否开启滚动,tab过多时建议开启,否则无法显示。
    tabs List<Widget> 必填 -- 设置tab,具体有多少个tab由集合的长度决定。
    controller TabController -- 用于自己定义控制器。
    indicatorColor Color -- 设置指示器的颜色,默认会根据主题色做出调整。
    indicatorWeight double 2.0 设置指示器的高度。
    indicatorPadding EdgeInsetsGeometry EdgeInsets.zero 底部指示器的padding,
    indicator Decoration -- 设置Tab的样式,例如边框等。
    indicatorSize TabBarIndicatorSize -- 指示器大小的计算方式,TabBarIndicatorSize.label:和文字等宽,TabBarIndicatorSize.tab:和tab等宽。
    labelColor Color -- 统一设置标签字体颜色,默认与指示器颜色一致。
    lablStyle TextStyle -- 统一设置选中时标签字体样式。
    unselectedLabelColor Color -- 设置未选中时标签字体颜色,默认与指示器颜色一致。
    unselectedLabelStyle TextStyle -- 设置未选中时标签字体样式。
    import 'package:flutter/material.dart';
    
    class MovieDetailPage extends StatelessWidget {
      final data;
    
      List<String> get _movies {
        return data['movies'];
      }
    
      int get _index {
        return data['index'];
      }
    
      MovieDetailPage(this.data);
    
      @override
      Widget build(BuildContext context) {
        return DefaultTabController(  // 1.要实现Android中的TabLayout需要使用DefaultTabController组件。
          initialIndex: this._index,
          length: _movies.length,
          child: Scaffold(    // 2.使用Scaffold作为DefaultTabController组件的child。
            appBar: AppBar(   // 3.设置appBar。
              title: Text('电影专区'),
              centerTitle: true,
              bottom: TabBar(  // 4.设置bottom属性为TabBar组件。
                isScrollable: true,  // 5.如果tab过多时需要将isScrollable属性设置为true,否者无法显示。默认为false。
                tabs: _movies.map((movie) => Tab(text: movie)).toList()  // 6.设置tab的样式,建议使用Tab组件。
              ),
            ),
            body: TabBarView(  // 7.设置body属性为TabBarView组件。
              children: _movies.map((movie) {   // 8.为TabBarView设置每个页面的具体内容(绘制每个页面)。
                return Align(
                    alignment: Alignment.center, child: Text('电影《$movie》的详情页面。'));
              }).toList(),
            ),
          ),
        );
      }
    }
    
    
    //调用
    Navigator.push(context, MaterialPageRoute(builder: (context) {
      return MovieDetailPage({
        'index': 0,
        'movies': [
          '天龙八部',
          '别拿村长不当干部',
          '长征',
          '白衣校花大长腿',
          '死侍',
          'X战警',
          '007大破天幕杀机',
          '信条',
          '射雕英雄传',
          '新白娘子传奇',
          '三体',
          '鹿鼎记',
          '我和僵尸有个约会',
          '奇异博士',
          '蜘蛛侠',
          '复仇者联盟-无限战争',
          '复仇者联盟-逆转无限'
        ]
      });
    }));
    

    他还有一种高级用法,可以自己监听生命周期,并监听滚动事件等。具体用法如下:

    import 'package:flutter/material.dart';
    
    class SuperiorTabControllerPage extends StatefulWidget{
      @override
      State<StatefulWidget> createState() {
        return _SuperiorTabControllerPageState();
      }
    }
    
    class _SuperiorTabControllerPageState extends State<SuperiorTabControllerPage> with SingleTickerProviderStateMixin{
    
      TabController _tabController;
    
      @override
      void initState() {
        super.initState();
        _tabController = TabController(
          length: 2,
          vsync: this  //1.固定写法
        );
        _tabController.addListener(() {
          print(this._tabController.index);
        });
      }
    
      @override
      void dispose() {
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('TabController的高级用法'),
            bottom: TabBar(
              controller: this._tabController,  //2.TabBar必须设置自己定义的TabController
              indicatorSize: TabBarIndicatorSize.label,
              tabs: [
                Tab(text: '科幻'),
                Tab(text: '悬疑')
              ],
            ),
          ),
          body: TabBarView(
            controller: this._tabController,  //3.TabBarView必须设置自己定义的TabController
            children: [
              Center(
                child: Text('科幻'),
              ),
              Center(
                child: Text('悬疑'),
              ),
            ],
          ),
        );
      }
    }
    
    
    //调用
    Navigator.push(context, MaterialPageRoute(
      builder: (context) => SuperiorTabControllerPage()
    ))
    

24. Divider 线条组件

该组件通常用于在一面中绘制一条线。

参数 类型 可选 默认值 说明
height double -- 线条的高度
color Color -- 线条的颜色

25. ListTile 条目组件

该组件可以快速绘制类似于个人中心页面中的条目,或则手机设置页面中的条目。

参数 类型 可选 默认值 说明
leading Widget -- 通常用于设置左侧的图标。
title Widget -- 通常用于设置标题。
subtitle Widget -- 通常用于设置子标题。
onTap GestureTapCallback -- 用于监听点击事件。

26. Drawer 抽屉组件

参数 类型 可选 默认值 说明
elevation double 16.0 设置阴影高度
child Widget 必填 -- 绘制抽屉中的内容

在使用Drawer组件的时候我们通常要为抽屉设置头,否则会有点难看。如果要设置头就需要用到一下两个组件:

  1. DrawerHeader

    参数 类型 可选 默认值 说明
    decoration Decoration -- 用于设置样式,通常使用BoxDecoration。
    margin EdgeInsetsGeometry -- 设置外边距
    padding EdgeInsetsGeometry -- 设置内边距
    child Widget 必填 -- 绘制内容视图
    1. UserAccountsDrawerHeader
    参数 类型 可选 默认值 说明
    accountName Widget 必填 -- 设置用户名
    accountEmail Widget 必填 -- 设置用户邮箱
    currentAccountPicture Widget -- 设置当前用户头像
    onDetailsPressed VoidCallback -- 用户信息被点击后的监听。
    arrowColor Color -- 当设置了onDetailsPressed后右侧会出现一个小的三角箭头,该参数用于设置箭头颜色。
    otherAccountsPictures List<Widget> -- 用于设置其他用户头像,设置后会在当前头像的右侧出现小的头像。

    如果需要用代码关闭抽屉可以使用下面的方式:

    Navigator.pop(context)
    

27. TextField 文本框组件

参数 类型 可选 默认值 说明
decoration InputDecoration InputDecoration() 设置边框、hint、lable、图标等
obscureText bool false 是否隐藏明文显示内容(密码模式)。
obscuringCharacter String -- 当obscureText为true时,用于设置显示的文字。
maxLines int 1 最大行数,当设置大于1时为多行输入模式。
minLines int -- 最小输入行数,主要是用来设置最小高度。
controller TextEditingController -- 用于设置默认显示内容以及获取编辑框中的内容。
onChanged ValueChanged<String> -- 监听内容的改变。
onEditingComplete VoidCallback -- 监听焦点的丢失(编辑完成)

28. CheckBox 多选框组件

参数 类型 可选 默认值 说明
value bool 必填 -- 设置是否选中
onChanged ValueChanged<bool> 必填 -- 监听选中状态的改变

29. CheckboxListTile组件

参数 类型 可选 默认值 说明
value bool 必填 -- 设置是否选中
onChanged ValueChanged<bool> 必填 -- 监听选中状态的改变
title Widget -- 通常用于设置标题。
subtitle Widget -- 通常用于设置子标题。
secondary Widget -- 配置图标或者图片。
selected bool false 选中的时候文字是否跟着改变。

30. Radio 单选框组件

参数 类型 可选 默认值 说明
value T 必填 -- 设置当前单选框所代表的值。
onChanged ValueChanged<T> 必填 -- 监听选中状态的改变,并将当前单选框所代表的值(T)回传。
groupValue T -- 为当前单选框分组。

31. RadioListTile组件

参数 类型 可选 默认值 说明
value T 必填 -- 设置当前单选框所代表的值。
onChanged ValueChanged<T> 必填 -- 监听选中状态的改变,并将当前单选框所代表的值(T)回传。
title Widget -- 通常用于设置标题。
subtitle Widget -- 通常用于设置子标题。
secondary Widget -- 配置图标或者图片。
selected bool false 选中的时候文字是否跟着改变。
groupValue T -- 为当前单选框分组。

32. Switch 开关组件

参数 类型 可选 默认值 说明
value bool 必填 -- 设置是否选中
onChanged ValueChanged<bool> 必填 -- 监听选中状态的改变

33. 日期组件以及日期和时间戳

  • 日期和时间戳

    日期转化成时间戳:

    print(DateTime.now().millisecondsSinceEpoch)
    //输出:1600925003761
    

    时间戳转换成日期:

    print(DateTime.fromMillisecondsSinceEpoch(1600925003761))
    //输出:2020-09-24 13:23:23:761
    
  • 使用系统的日期选择组件:

    showDatePicker(
      context: context,
      initialDate: DateTime.now(), //默认选中的日期
      firstDate: DateTime(2019),  //开始(最早)日期
      lastDate: DateTime(2050)  //结束(最晚)日期
    ).then((value) => setState(() => date = value));
    //showDatePicker方法返回的是Future<DateTime>类型,所以这里使用then方法接收结果。但也可以使用async结合await的方式。
    
  • 使用系统的时间选择组件:

    showTimePicker(context: context, initialTime: TimeOfDay.now())
      .then((value) => setState(() => time = value.format(context)));
    //showTimePicker方法返回的是Future<TimeOfDay>类型,所以这里使用then方法接收结果。但也可以使用async结合await的方式。
    

四、第三方组件库

虽然Flutter为我们提供了很多的Widget,但有时Flutter为我们提供的组件无法满足我们需求,这时我们就需要使用一些第三方的Widget组件。我们可以通过网站pub.dev来找到我们想要的Widget组件进行使用。

1. Toast 组件

  • 地址:https://pub.dev/packages/toast

  • 使用:

    1. 在pubspec.yaml文件中的dependencies节点下添加toast: ^版本号:

      dependencies:
        flutter:
          sdk: flutter
      
        #Toast
        toast: ^0.1.5  #例如这里添加0.1.5版本
      
    2. 在代码中使用:

      import 'package:toast/toast.dart'
        
      Toast.show('我是一个Toast', context, gravity: Toast.CENTER);
      

2. Swiper 轮播图组件

  • 地址:https://pub.dev/packages/flutter_swiper

  • 使用:

    1. 在pubspec.yaml文件中的dependencies节点下添加flutter_swiper: ^版本号:

      dependencies:
        flutter:
          sdk: flutter
      
        #轮播图组件
        flutter_swiper: ^1.1.6  #例如这里添加1.1.6版本
      
    2. 在代码中使用:

      import 'package:flutter_swiper/flutter_swiper.dart';
      
      Swiper(
        itemCount: images.length,  //设置一共有多少个item
        itemBuilder: (context, index) {  //构建没给item
          return Container(
            child: Image.network(images[index], fit: BoxFit.cover),
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(10),
            ),
            clipBehavior: Clip.antiAlias,
          );
        },
        pagination: SwiperPagination(),//显示指示器
        autoplay: true,  //是否自动播放
        viewportFraction: 0.8, //当前页的占比,如果小于1,那么剩下的部分将有左右两边填充。
        scale: 0.9, //每个Banner的缩放比例
        onTap: (i) => setState(()=> _currentIndex = i),  //条目点击监听
      )
      

3. DateFormat 日期格式化组件

  • 地址:https://pub.dev/packages/date_format

  • 在代码中使用:

    1. 在pubspec.yaml文件中的dependencies节点下添加date_format: ^版本号:

      dependencies:
        flutter:
          sdk: flutter
      
        #日期格式化
        date_format: ^1.0.9  #例如这里添加1.0.9版本
      
    2. 使用:

      由于该库使用起来稍微有点麻烦,所以我对他进行了封装(可能不是很完整,只是提供思路)。

      import 'package:date_format/date_format.dart' as df;
      
      String formatDate(DateTime date, DateType type){
        return df.formatDate(date, _getFormat(type));
      }
      
      _getFormat(DateType type){
        switch (type) {
          case DateType.yyyy_MM_dd:
            return ['yyyy', '-', 'mm', '-', 'dd'];
          case DateType.yyyy_MM_dd_HH:
            return ['yyyy', '-', 'mm', '-', 'dd', ' ', 'HH'];
          case DateType.yyyy_MM_dd_HH_mm:
            return ['yyyy', '-', 'mm', '-', 'dd', ' ', 'HH', ':', 'nn'];
          case DateType.yyyy_MM_dd_HH_mm_ss:
            return ['yyyy', '-', 'mm', '-', 'dd', ' ', 'HH', ':', 'nn', ":", 'ss'];
          case DateType.HH_mm_ss:
            return ['HH', ':', 'nn', ":", 'ss'];
          case DateType.HH_mm:
            return ['HH', ':', 'nn'];
        }
      }
      
      enum DateType{
        yyyy_MM_dd,
        yyyy_MM_dd_HH,
        yyyy_MM_dd_HH_mm,
        yyyy_MM_dd_HH_mm_ss,
        HH_mm,
        HH_mm_ss
      }
      
      
      //使用方式如下
      formatDate(date, DateType.yyyy_MM_dd)
      

五、Dialog 弹窗

如果要使用AlertDialog则需要使用showDialog(context, builder)方法。该方法有两个核心参数如下:

参数 类型 可选 默认值 说明
context BuildContext 必填 -- build方法中的上下文。
builder Widget Function(BuildContext context) 必填 -- 构建Dialog的回调函数。

第一个参数context比较简单,只要将Widget的build方法中的context或则State中的context成员传入即可。第二个参数builder则是一个Function。你需要定义一个Function并返回你创建好的Widget,这个Widget可以是以下几种:

1. AlertDialog

参数 类型 可选 默认值 说明
title Widget -- 设置标题
titlePadding EdgeInsetsGeometry -- 标题的内边距
titleTextStyle TextStyle -- 标题的字体样式
content Widget -- 设置内容
contentPadding EdgeInsetsGeometry EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0) 内容的内边距
contentTextStyle TextStyle -- 内容的字体样式
actions List<Widget> -- 设置按钮
showDialog(
        context: context,
        builder: (context){
          return AlertDialog(
            title: Text('提示'),
            content: Text('您觉的这个Demo对您的帮助大吗?'),
            actions: [
              FlatButton(
                child: Text('是的'),
                onPressed: () {
                  Navigator.pop(context);
                  Toast.show(context, '谢谢您的认可~');
                },
              ),
              FlatButton(
                child: Text('非常大'),
                onPressed: () {
                  Navigator.pop(context);
                  Toast.show(context, '您真是一个好人呐~');
                },
              )
            ],
          );
        }
    );

有些情况下我们需要获取Dialog操作后的返回值,这时可以分为以下几步:

  1. 自定义函数上加上async关键字。
  2. showDialog方法前面加上await关键字并定义一个变量接收该方法的返回值。
  3. Navigator.pop()退出弹窗时传入要返回的值。
  4. 拿到showDialog方法的返回结果做相应处理。
_showAlertDialog() async{ //1.加上async关键字
    var result = await showDialog(  //2.加上await关键字并用变量接收
        context: context,
        builder: (context){
          return AlertDialog(
            title: Text('提示'),
            content: Text('您觉的这个Demo对您的帮助大吗?'),
            actions: [
              FlatButton(
                child: Text('是的'),
                onPressed: () {
                  Navigator.pop(context, '您点击了按钮:是的');  //3.退出弹出时传入要返回的值。
                  Toast.show(context, '谢谢您的认可~');
                },
              ),
              FlatButton(
                child: Text('非常大'),
                onPressed: () {
                  Navigator.pop(context, '您点击了按钮:非常大');  //3.退出弹出时传入要返回的值。
                  Toast.show(context, '您真是一个好人呐~');
                },
              )
            ],
          );
        }
    );
    print(result);  //4.拿到showDialog方法的返回结果做相应处理。
  }

2. SimpleDialog

参数 类型 可选 默认值 说明
title Widget -- 设置标题
titlePadding EdgeInsetsGeometry EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0) 标题的内边距
titleTextStyle TextStyle -- 标题的字体样式
children List<Widget> -- 设置内容,通常使用SimpleDialogOption作为元素。
contentPadding EdgeInsetsGeometry EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0) 内容的内边距
backgroundColor Color -- 设置背景颜色
_showSimpleDialog() async{
    var result = await showDialog(
        context: context,
        builder: (context){
          return SimpleDialog(
            title: Center(
              child: Text('请您选择'),
            ),
            children:[
              SimpleDialogOption(
                child: Text('第一个选项'),
                onPressed: (){
                  Navigator.pop(context, '您点击了: 第一个选项');
                },
              ),
              Divider(height: 0),
              SimpleDialogOption(
                child: Text('第二个选项'),
                onPressed: (){
                  Navigator.pop(context, '您点击了: 第二个选项');
                },
              ),
              Divider(height: 0),
              SimpleDialogOption(
                child: Text('第三个选项'),
                onPressed: (){
                  Navigator.pop(context, '您点击了: 第三个选项');
                },
              )
            ],
          );
        }
    );
    print(result??'您取消了选择');
  }

3. BottomSheet

如果要使用BottomSheet则需要使用showModalBottomSheet(context, builder)方法。该方法有两个核心参数如下:

参数 类型 可选 默认值 说明
context BuildContext 必填 -- build方法中的上下文。
builder Widget Function(BuildContext context) 必填 -- 构建你的自定义布局。
_showBottomSheet() async{
    var result = await showModalBottomSheet(
        context: context,
        isScrollControlled: true,
        builder: (context) {
          return Container(
            height: 290,
            child: Column(
              children: [
                ListTile(
                  title: Text('第一个条目'),
                  onTap: () => Navigator.pop(context, '第一个条目'),
                ),
                Divider(height: 0),
                ListTile(
                  title: Text('第二个条目'),
                  onTap: () => Navigator.pop(context, '第二个条目'),
                ),
                Divider(height: 0),
                ListTile(
                  title: Text('第三个条目'),
                  onTap: () => Navigator.pop(context, '第三个条目'),
                ),
                Divider(height: 0),
                ListTile(
                  title: Text('第四个条目'),
                  onTap: () => Navigator.pop(context, '第四个条目'),
                ),
                Divider(height: 0),
                ListTile(
                  title: Text('第五个条目'),
                  onTap: () => Navigator.pop(context, '第五个条目'),
                ),
              ],
            ),
          );
        }
    );
    print('您点击了:$result');
  }

4. 自定义Dialog

在Flutter中,如果要自定义Dialog,需要以下几个步骤:

  • 声明一个类并继承自Dialog。
  • 重写Dialog中的Widget build(BuildContext context)方法。
  • Widget build(BuildContext context)方法中返回的Widget必须以Material组件作为根节点。
  • 通常情况下我们的Dialog都是有透明背景的,就是Dialog弹出后仍然能看到一部分原来的页面,所以我们需要给Material组件设置type属性为:MaterialType.transparency

下面的栗子就是自定义一个Dialog

class CustomDialog extends Dialog {

  @override
  Widget build(BuildContext context) {
    return Material(
      type: MaterialType.transparency,
      child: Stack(  //从这里开始就可以写我们自己想要的界面了。
        alignment: Alignment.center,
        children: [
          Container(
            margin: EdgeInsets.only(left: 40, right: 40),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Container(
                  decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.only(
                          topLeft: Radius.circular(8),
                          topRight: Radius.circular(8))),
                  child: Column(
                    children: [
                      SizedBox(height: 12),
                      Text('提示',
                          style: TextStyle(
                              fontWeight: FontWeight.bold, fontSize: 15)),
                      SizedBox(height: 12),
                      Divider(height: 0),
                    ],
                  ),
                ),
                Container(
                  padding: EdgeInsets.all(12),
                  color: Colors.white,
                  constraints: BoxConstraints(minHeight: 100),
                  child: Center(
                    child: Text('这是一个自定义弹出,你知道了吗?'),
                  ),
                ),
                Divider(height: 0),
                Row(
                  children: [
                    Expanded(
                      child: Container(
                        height: 50,
                        child: FlatButton(
                          color: Colors.white,
                          shape: RoundedRectangleBorder(
                              borderRadius: BorderRadius.only(
                                  bottomLeft: Radius.circular(8))),
                          child: Text('不知道'),
                          materialTapTargetSize:
                          MaterialTapTargetSize.shrinkWrap,
                          onPressed: () {
                            Navigator.pop(context, '我看你该挨揍了。');
                          },
                        ),
                      ),
                    ),
                    VerticalDivider(width: 0.1),
                    Expanded(
                      child: Container(
                        height: 50,
                        child: FlatButton(
                          color: Colors.white,
                          shape: RoundedRectangleBorder(
                              borderRadius: BorderRadius.only(
                                  bottomRight: Radius.circular(8))),
                          child: Text('知道了',
                              style: TextStyle(color: Colors.blue)),
                          materialTapTargetSize:
                          MaterialTapTargetSize.shrinkWrap,
                          onPressed: () {
                            Navigator.pop(context, '不错,你很聪明。');
                          },
                        ),
                      ),
                    )
                  ],
                )
              ],
            ),
          )
        ],
      ),
    );
  }
}

使用上面的自定义Dialog和普通的Dialog没有什么区别,下面是使用时的代码:

 _showCustomDialog() async {
    var result = await showDialog(
        context: context,
        builder: (context) {
          return CustomDialog();
        });
    print(result);
  }

如果要实现打开弹窗后的一段时间之后自动关闭弹窗则可以结合Dart中的Timer定时器实现:

Timer.periodic(Duration(seconds: 5), (timer) {
  timer.cancel();
  //do something,such as dismiss the dialog.
});

六、注解

1. @override 重写

该注解用于表示一个成员是继承自父类。

2. @protected

该注解只能应用于一个类中的成员,表示该成员只能被子类调用或扩展,也可以在混合类中直接或间接的调用。

3. @mustCallSuper

该注解应用在方法上,表示该方法如果被子类重写了,那么子类则必须调用super。

七、路由

Flutter中路由的核心类为Navigator,如果要打开新的页面则需要调用push方法,如果要退出页面则需要调用pop方法。

1. 普通路由

Navigator.push(context, MaterialPageRoute(
  builder:(context){
    return SecondPage();   //这里返回目标页面。
  }
));

2. 普通路由传值

普通路由的传值方式其实就是在构造要跳转的页面时将参数传入。

Navigator.push(context, MaterialPageRoute(
  builder:(context){
    return SecondPage(title: '第二个页面');   //这里构建要跳转的页面时直接传入参数。
  }
));

class SecondPage extends StatelessWidget{

  final String title;
  SecondPage({this.title = ""});  //这里接受外面传入的参数

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(this.title),  //这里使用外面传入的参数
      ),
      body: Align(
        alignment: Alignment.center,
        child: Text("我是${this.title}"),  //这里使用外面传入的参数
      ),
    );
  }
}

3. 命名路由

命名路由的使用分以下两步:

  1. 在主入口中的MaterialApp中配置routes,routes接受的数据类型时Map,Map的key是路由命名,值是初始具体跳转到某个页面的回调。

    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: Tabs(),
          routes: {  //使用routes参数配置命名路由。
            '/second': (context) => SecondPage()  //这里定义了一个命名路由‘/second’并制定跳转到SecondPage页面。
          },
        );
      }
    }
    
  1. 利用pushNamed方法使用已经定义好的路由。

    Navigator.pushNamed(context, '/second'); //这里的‘/second’就是我们已经定义好的路由,必须和已经定义好的名称保持一致。
    

4. 命名路由传值

命名路由的传值略微有点复杂,具体代码如下:

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {

  //将我们所有的路由定义为成员变量。
  final routes = {
    '/second': (context, {arguments}) => SecondPage(title: arguments['title'])
  };

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: Tabs(),
        onGenerateRoute: (RouteSettings settings){  //利用onGenerateRoute自己处理传参及跳转。
          final String name = settings.name;  //获取路由名称
          final Function pageContentBuilder = this.routes[name]; //根据名称获取具体跳转的方法
          if(pageContentBuilder != null) { //判断方法是否为空,如果空则抛出异常。
            return MaterialPageRoute(
                builder: (context) {
                  if(settings.arguments != null) {  //如果有参数则调用我们自定义的方法传入参数
                    return pageContentBuilder(context, arguments: settings.arguments);
                  }else {
                    return pageContentBuilder(context);  //没有参数时则忽略arguments
                  }
                }
            );
          }else {
            throw ArgumentError('route "$name" not implements!');
          }
        },
    );
  }
}

路由已经定义好了,那么调用的方式如下:

Navigator.pushNamed(context, '/second', arguments: {
  'title': '第二个页面'
});

代码抽离:

  1. 入口文件

    import 'package:flutter/material.dart';
    import 'package:flutter_demo_one/pages/route.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
            theme: ThemeData(
              primarySwatch: Colors.blue,
            ),
            initialRoute: '/',
            onGenerateRoute: generateRoute,
        );
      }
    }
    
  2. Route文件

    import 'package:flutter/material.dart';
    import 'package:flutter_demo_one/pages/hospital/hospitalDetailPage.dart';
    
    import 'pages/main/tabs.dart';
    
    final routes = {
      '/': (context) => Tabs(),
      '/hospital': (context, {arguments}) => HospitalDetailPage(arguments)
    };
    
    Function generateRoute = (RouteSettings settings){
      final String name = settings.name;
      final Function pageContentBuilder = routes[name];
      if(pageContentBuilder != null) {
        return MaterialPageRoute(
            builder: (context) {
              if(settings.arguments != null) {
                return pageContentBuilder(context, arguments: settings.arguments);
              }else {
                return pageContentBuilder(context);
              }
            }
        );
      }else {
        throw ArgumentError('route "$name" not implements!');
      }
    };
    

5. 替换路由

替换路由其实就是用要打开的新的页面来替换当前页面,例如:A是第一页面(主页面),A正常打开了B页面,而B又用替换路由打开了C,那么此时的页面栈中就只有AC两个页面,如果在C页面中点击返回按钮后( Navigator.pop() )就会直接显示A而不会显示B。

注意:使用替换路由时,被打开的页面是否有返回按钮(左上角的返回箭头)取决于当前页面是否有返回按钮。

Navigator.pushReplacementNamed(context, '/testPage2', arguments: {
    'title':'页面标题'
});

八、 网络请求

1. Json 与 Map 相互转换。

  • Json转Map

    final String _json = '{"username":"张三","age":23}'; //json字符串数据
    
    print(json.decode(_json)['username'])  //输出:张三
    
  • Map转Json

    final Map _map = {
      "username":"张三",
      "age": 23
    };
    print(context, json.encode(_map))  //输出:{"username":"张三","age":23}
    

2. 使用http库进行网络请求

  • 地址:https://pub.dev/packages/http

  • 使用:

    1. 在pubspec.yaml文件中的dependencies节点下添加http: ^版本号:

      dependencies:
        http: ^0.12.2
      
    2. 在代码中使用:

      get请求

      import 'package:http/http.dart';
      
      _onGetData(apiUrl) async{
        var result = await get(apiUrl);
        if(result.statusCode == 200) {
          print(result.body);
        }else {
          print('接口请求错误!');
        }
      }
      

      post请求

      _doLogin(context, username, psw, {url = ''}) async {
        var result = await post(url, body: {'username':username, 'psw':psw});
        if (result.statusCode == 200) {
          print(result.body);
        } else {
          print('接口请求错误!');
        }
      }
      

3. dio 库请求

​ dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等...

  • 地址:https://pub.dev/packages/dio github地址:https://github.com/flutterchina/dio

  • 使用:

    1. 在pubspec.yaml文件中的dependencies节点下添加dio: ^版本号:

      dependencies:
        dio: ^3.0.7
      
    2. 简单使用

      import 'package:dio/dio.dart';
      void getHttp() async {
        try {
          Response response = await Dio().get("http://www.baidu.com");
          print(response);
        } catch (e) {
          print(e);
        }
      }
      
    3. 发起一个 GET 请求 :

      Response response;
      Dio dio = Dio();
      response = await dio.get("/test?id=12&name=wendu")
      print(response.data.toString());
      // 请求参数也可以通过对象传递,上面的代码等同于:
      response = await dio.get("/test", queryParameters: {"id": 12, "name": "wendu"});
      print(response.data.toString());
      
    4. 发起一个 POST 请求:

      response = await dio.post("/test", data: {"id": 12, "name": "wendu"});
      
    5. 发起多个并发请求:

      response = await Future.wait([dio.post("/info"), dio.get("/token")]);
      
    6. 下载文件:

      response = await dio.download("https://www.google.com/", "./xx.html");
      
    7. 以流的方式接收响应数据:

      Response<ResponseBody> rs = await Dio().get<ResponseBody>(url,
        options: Options(responseType: ResponseType.stream), //设置接收类型为stream
      );
      print(rs.data.stream); //响应流
      
    8. 以二进制数组的方式接收响应数据:

      Response<List<int>> rs = await Dio().get<List<int>>(url,
       options: Options(responseType: ResponseType.bytes), //设置接收类型为bytes
      );
      print(rs.data); //二进制数组
      
    9. 发送 FormData:

      FormData formData = FormData.from({
          "name": "wendux",
          "age": 25,
        });
      response = await dio.post("/info", data: formData);
      
    10. 通过FormData上传多个文件:

      FormData.fromMap({
          "name": "wendux",
          "age": 25,
          "file": await MultipartFile.fromFile("./text.txt",filename: "upload.txt"),
          "files": [
            await MultipartFile.fromFile("./text1.txt", filename: "text1.txt"),
            await MultipartFile.fromFile("./text2.txt", filename: "text2.txt"),
          ]
       });
      response = await dio.post("/info", data: formData);
      
    11. 监听发送(上传)数据进度:

      response = await dio.post(
        "http://www.dtworkroom.com/doris/1/2.0.0/test",
        data: {"aa": "bb" * 22},
        onSendProgress: (int sent, int total) {
          print("$sent $total");
        },
      );
      
    12. 以流的形式提交二进制数据:

      // 二进制数据
      List<int> postData = <int>[...];
      await dio.post(
        url,
        data: Stream.fromIterable(postData.map((e) => [e])), //创建一个Stream<List<int>>
        options: Options(
          headers: {
            Headers.contentLengthHeader: postData.length, // 设置content-length
          },
        ),
      );
      

    注意:如果要监听提交进度,则必须设置content-length,反之则是可选的。

九、其他

1. 国际化

  • 第一步:找到pubspec.yaml文件,配置flutter_localizations(国际化)。

    dependencies:
      flutter:
        sdk: flutter
    
      #国际化配置
      flutter_localizations:
        sdk: flutter
    
  • 第二步:导入国际化的包flutter_localizations(AS可以自动导包)。

    import 'package:flutter_localizations/flutter_localizations.dart';
    
  • 第三步:在main入口中的MaterialApp组件中配置国际化:

    import 'package:flutter/material.dart';
    import 'package:flutter_demo_one/route.dart';
    import 'package:flutter_localizations/flutter_localizations.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme: ThemeData(
            primarySwatch: Colors.blue
          ),
          initialRoute: '/',
          onGenerateRoute: generateRoute,
          
          //以下是国际化配置,配置下面localizationsDelegates和supportedLocales两个属性即可实现国际化。
          localizationsDelegates: [
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate
          ],
          supportedLocales: [
            const Locale('zh', 'CH'),
            const Locale('en', 'US')
          ],
        );
      }
    }
    

2. 沉浸式状态栏

只需要在入口处build方法中return前加入下面一行代码,将状态栏的颜色设置为透明色即可:

SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(statusBarColor: Colors.transparent));
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章