Flutter 实战进阶

2019-03-19 16:02:25

Flutter 在实际开发中遇到的一些问题及解决方案,作为笔记记录。

1、container width、height 100%

FractionallySizedBox(
      widthFactor: 1,
      heightFactor: 1,
      child: ,
)
double width = MediaQuery.of(context).size.width
double height = MediaQuery.of(context).size.height

2、沉浸式背景图片

背景图片铺满Appbar及状态栏

Widget build(BuildContext context) {
    return new Stack(
      children: <Widget>[
        Container(
          child: Image.network('https://www.bing.com/az/hprichbg/rb/Punakaiki_DE-DE0884339574_1920x1080.jpg'),
          color: Colors.lightGreen,
        ),
        Scaffold(
          backgroundColor: Colors.transparent,
          appBar: AppBar(
            backgroundColor: Colors.transparent,
            title: Text('Coin'),
          ),
          body: MyCoinPage(),
        )
      ],
    );
  }

3、APPBar no shadow

AppBar默认底部有阴影。去掉只需要配置参数即可:

AppBar(
    elevation: 0.0,
)

4、自定义字体

flutter:
  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  fonts: 
    - family: Kphi
      fonts: 
        - asset: assets/fonts/Komet-Pro-Heavy-Italic.otf

5、Tab切换动画

Tab切换需加入SingleTickerProviderStateMixin

通过 with SingleTickerProviderStateMixin 实现Tab页的切换动画效果。因为初始化animationController的时候需要一个TickerProvider类型的参数Vsync参数,所以我们混入了TickerProvider的子类SingleTickerProviderStateMixin

6、 逐帧动画 FrameAnimationImage

Flutter 逐帧动画 FrameAnimationImage。 逐帧动画: 依次顺序循环播放多张图片组成一个动画。

/// Flutter FrameAnimation 逐帧动画
import 'package:flutter/material.dart';

class FrameAnimationImage extends StatefulWidget {
  final List<String> assetList;
  final double width;
  final double height;
  final int interval;

  FrameAnimationImage(
      {this.assetList, this.width, this.height, this.interval = 200});

  @override
  State<StatefulWidget> createState() {
    return _FrameAnimationImageState();
  }
}

class _FrameAnimationImageState extends State<FrameAnimationImage>
    with SingleTickerProviderStateMixin {
  // 动画控制
  Animation<double> _animation;
  AnimationController _controller;
  int interval = 200;

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

    if (widget.interval != null) {
      interval = widget.interval;
    }
    final int imageCount = widget.assetList.length;
    final int maxTime = interval * imageCount;

    // 启动动画controller
    _controller = new AnimationController(
        duration: Duration(milliseconds: maxTime), vsync: this);
    _controller.addStatusListener((AnimationStatus status) {
      if (status == AnimationStatus.completed) {
        _controller.forward(from: 0.0); // 完成后重新开始
      }
    });

    _animation = new Tween<double>(begin: 0, end: imageCount.toDouble())
        .animate(_controller)
          ..addListener(() {
            setState(() {
              // the state that has changed here is the animation object’s value
            });
          });

    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    int ix = _animation.value.floor() % widget.assetList.length;

    List<Widget> images = [];
    // 把所有图片都加载进内容,否则每一帧加载时会卡顿
    for (int i = 0; i < widget.assetList.length; ++i) {
      if (i != ix) {
        images.add(Image.asset(
          widget.assetList[i],
          width: 0,
          height: 0,
        ));
      }
    }

    images.add(Image.asset(
      widget.assetList[ix],
      width: widget.width,
      height: widget.height,
    ));

    return Stack(alignment: AlignmentDirectional.center, children: images);
  }
}

7、 时间格式化库 intl

8、 时间格式化库intltextDirection冲突

报错如下:

The getter 'rtl' isn't defined for the class 'TextDirection'

参考: Stack Overflow

解决方案:

import 'package:intl/intl.dart' as intl;

9、 convert string to date

dart将时间字符串转为date库:

参考: Stack Overflow

var parsedDate = DateTime.parse('1974-03-20 00:00:00.000');
print('dateTime: $parsedDate');

10、 tabcontroller.addListener

Tab切换时,可以通过tabcontroller.addListener监听切换事件。在实际开发中发现如下问题,可以根据需求选择不同的判断方式:

_tabController = new TabController(length: 2, vsync: this);
_tabController.addListener(() {
  // _tabController.addListener...
  if (_tabController.indexIsChanging) {
    // _tabController.indexIsChanging...
  }
});

当点击切换时:

  • _tabController.addListener 触发两次
  • _tabController.indexIsChanging 触发一次

当滑动切换时:

  • _tabController.addListener 触发一次
  • _tabController.indexIsChanging 不触发

11、 FutureBuilder => Future widget

参考: FutureBuilder 延迟加载组件

@override
  Widget build(BuildContext context) {
    return new FutureBuilder(
      future: getTextFromFile(),
      initialData: "Loading text..",
      builder: (BuildContext context, AsyncSnapshot<String> text) {
        return new SingleChildScrollView(
          padding: new EdgeInsets.all(8.0),
          child: new Text(
            text.data,
            style: new TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 19.0,
            ),
          ));
      });
  }

  Future<String> getFileData(String path) async {
    return await new Future(() => "test text");
  }

  Future<String> getTextFromFile() async {
    return getFileData("test.txt");
  }
}

12、获取页面生命周期

使用 with WidgetsBindingObserver后,可以对页面的生命周期进行监听。

class _LifecycleWatcherState extends State<LifecycleWatcher> with WidgetsBindingObserver {
  AppLifecycleState _lastLifecycleState;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    print('current state: $state');
    setState(() {
      _lastLifecycleState = state;
    });
  }
  
}

13、页面重构

1、TabView + pull_to_refresh 如何优雅共存
当页面中有Tab切换,且每个Tab页都是可以下拉刷新、上拉加载的组件时。如果每个TabView使用的是同一个组件,只是根据不同的标识显示不同的内容。在实际开发中发现,这么做虽然可以减少部分代码量,但是出现的问题是,Tab切换时每次都等于是重新渲染公共的组件,导致比较卡顿,且切换时会发生多个TabView间的数据混乱等一些列问题。而且多个TabView公用一个scrollController的话也会出很多问题。

正确的做法是,每个TabView应该是独立组件,包括互相独立的scrollController。只不过其中的一些子组件是可以抽出来共用的,这是没问题的。

2、listView显示占位图。

空列表的一般需要显示占位图,开始的做法是,判断列表为空时,直接返回一个占位图的Container,代替ListView。这样做之后,发现数据为空时就不能下拉刷新了,因为下拉刷新和ListView绑定在一起。于是改为下述方案,当数据为空时任然可以下拉刷新,实时更新数据:

child: new SmartRefresher(
headerConfig: const RefreshConfig(completeDuration: 300),
enablePullDown: true,
enablePullUp: true,
onRefresh: _onRefresh,
headerBuilder: LoadingIndicator.buildHeader,
footerBuilder: LoadingIndicator.buildFooter,
controller: _refreshController,
child: (!loading && _payList.length == 0)
    ? ListView.builder(
        itemCount: 1,
        itemBuilder: (context, i) {
          return _emptyTip();
        },
      )
    : _listView(),
),

14、网络图片加载组件

默认的图片加载组件 networkImage,当图片链接错误、为空时,会报exception导致Crash。FadeInImage同理。

参考: GitHub

试了好几款组件都没有做错误处理,最终找到一个可用的组件:Flutter_image ,同时配合FadeInImage设置默认占位图,完美。

adeInImage(
  fit: fit,
  placeholder: AssetImage(placeHolderImage),
  image: NetworkImageWithRetry(imgUrl),
),

15、主题颜色 ThemeData

16、网络图片加载失败Exception

使用 flutter_imageNetworkImageWithRetry 代替 NetworkImage 后,当图片404或者错误时,确实没有报Exception奔溃,但是安卓端却显示crash。

换了很多其他的插件都不好使,最终查明,NetworkImageWithRetry虽然可以catch exception不让应用奔溃,但是会触发Flutter.onError,而安卓端在检测到onError之后,当做crash处理,直接将页面关闭....

方案: 修改组件源码,图片加载失败时,不要触发Flutter.onError。

17、ActionSheet 点击隐藏

点击 ActionSheet 选择某项后,隐藏之。

Navigator.of(context, rootNavigator: true).pop("Discard");

18、阻止软键盘顶起页面

当软键盘弹起时,会将页面高度缩减,导致页面中的内容随着父级高度变化而变化。即软键盘弹起时将内容顶起,导致诸多问题。

解决方案:

return Scaffold(
  appBar: AppBar(
    title: new Text("通讯录"),
  ),
	resizeToAvoidBottomPadding: false, //输入框抵住键盘 内容不随键盘滚动
);

19、dart正则

RegExp regExp = new RegExp(
  r'(ludis)',
  caseSensitive: false, // 是否大小写敏感
  multiLine: false, // 待匹配文字是否是多行的
);

// 正则
var d = regExp.allMatches(text);
d.forEach((f) => print(f.group(0)));
print('match result, ${d.toString()}, ${d.toList()}');

20、键盘确认按钮样式

键盘的确认按钮会显示为“搜索”,点击后触发onSubmitted事件。

TextFormField( 
  ...
  textInputAction: TextInputAction.done,
  ...
)

21、Map类型错误

json转换时报错如下:

'_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, dynamic>'

原因: 类型为Map的其实际类型为Map<dynamic, dynamic>而非Map<String, dynamic>

解决方法: 进行转换

Map json; // Map<dynamic, dynamic>
Map<String, dynamic> = new Map<String, dynamic>.from(json); // Map<String, dynamic>

22、GestureDetector 点击区域小

使用GestureDetector包裹Container,发现在Container内容为空的区域点击时,捕捉不到onTap点击事件。

参考: GitHub

解决方法:

GestureDetector(
    behavior: HitTestBehavior.opaque,
    onTap: (){},
    child: Container()
)

23、TextField优化:

TextField decoration 可使用以下两种:

  • 使用 InputDecoration.collapsed() 。其高度不能和父级同高,导致可点击区域变小,用户体验不好。优势在于InputDecoration.collapsed()为无边框的输入框。
  • 使用InputDecoration。可使输入框高度和父级同高,扩大可点击区域。去掉边框即可。

解决方法:

InputDecoration(
    border: InputBorder.none, // 去除输入框边框
)

24、Flutter hide keyboard

调用方法隐藏键盘。

FocusScope.of(context).requestFocus(new FocusNode());

25、去除TextFiled水纹效果 ripple effect

参考: how-to-disable-default-widget-splash-effect-in-flutter

解决方案:

  1. 使用自定义主体,覆盖默认主题
  2. 给组件包裹Theme容器,设置 splashFactoryColors.transparent。✅
child: new Theme(
  data: new ThemeData(splashFactory: Colors.transparent),
  child: new TextField(...),
),

Dart 数组、字符串常用方法:

何人在此

发现现在有些年轻人,看书,看剧,看很多东西,听很多东西。日常表述观点惯用:某某说过,我特别赞同/喜欢某某在哪哪说的什么。遇到事自己没主意时,不先思考下,首先想到的就是去问周边同龄的朋友,或者去网上搜索,然后筛选些自己看着舒服的答案直接拿过来用。这些好像没什么不对。然而自始至终都没有太多的自我思考,大多是嗟来之食。人生来就是一张白纸。就中青少而言白纸又分为多种。一种,就是开头说的这种。在感觉到自己的空白后,在慌张下开始”学习“,这确实是好事,然而吃相太不讲究,遇到的各种观点,只要看着舒服的,拿来就吃,饥不择食,快速的填充自己的世界观。找到自己觉得不错的观点就狼吞虎咽,收藏起来。到头来,说起什么话题,遇到什么问题,确能迅速从藏品中拿出来一个观点回应,来作为行为的指引,实则是别人的观点。而这些观点一般都是供大众消费用的,类鸡汤文。看似很有见地,但却是真真儿的是受人操控,活在大流中的傀儡。这种人我称之为会吸星大法的,大家都知道,在所有武侠小说中,吸星大法都是极为厉害的武功,能让人快速“变强”