关于

这是一篇学习笔记博客,会记录个人flutter日常开发中的一些知识点,长期更新。如果某个知识点比较复杂,会另外写一篇博客链接到此。

常用工具

1.强制手机竖屏

在应用程序根MaterialApp之前,Widget build(BuildContext context)方法中,添加如下代码(ios可能不起作用,还未测试)

SystemChrome.setPreferredOrientations([
      DeviceOrientation.portraitUp,
      DeviceOrientation.portraitDown,
    ]);

2.监听页面的生命周期

项目中经常会有一些需求,需要监听页面打开关闭等,在android使用onResume,onPause等方法实现,但是在flutter没有默认的生命周期方法,可以使用RouteAware+RouteObserver实现。
首先要给根MaterialApp的navigatorObservers属性赋值(比如routeObserver),然后State的实现类需要混合RouteAware类,然后在State的didChangeDependencies方法下订阅路由状态,然后就可以在RouteAware的四个方法下监听页面变化。
假如有3个页面,分别是A、B、C,跳转逻辑由A->B->C。
didPopNext:在C页面关闭后,B页面调起该方法,类似onResume();
didPush: 当由A打开B页面时,B页面调起该方法,类似onCreate();
didPop: 当B页面关闭时,B页面调起该方法,类似onDestroy();
didPushNext: 当从B页面打开C页面时,该方法被调起,类似onPause()。
部分关键代码如下

//路由状态观察者初始化
final RouteObserver<Route<dynamic>> routeObserver = RouteObserver();
//MaterialApp赋值
navigatorObservers: [routeObserver],
//state 实现类混合RouteAware类
class BaseState<T extends StatefulWidget> extends State with RouteAware
//订阅路由状态
@override
  void didChangeDependencies() {
    routeObserver.subscribe(this, ModalRoute.of(context)); //订阅
    super.didChangeDependencies();
  }

3.国际化

使用Intl实现。参考链接

4.路由统一管理和路由跳转动画

在应用程序的根MaterialApp,设置onGenerateRoute: MyRouter.generateRoute属性,其中MyRoute代码如下

///路由管理类,管理所有路由跳转
class MyRouter {

///应用中所有界面
  static const String home = 'home'; //主界面
  static const String play = 'play'; //游戏界面
  static const String pause = 'pause'; //游戏暂停界面

  static Route<dynamic> generateRoute(RouteSettings settings) {

///设置onGenerateRoute属性后,所有路由跳转都会走到此,路由传值也在此设置,例如下面的PlayRoute

    Map arguments = settings.arguments;
    switch (settings.name) {
      //根据名称跳转相应页面
      case home:
        return NoAnimRouter(child: HomeRoute());
      case pause:
        return NoAnimRouter(child: PauseRoute());
      case play:
        return ScaleRouter(
            child: PlayRoute(
          sudokuBean: arguments[arg_key_sudoku],
          handle: arguments[arg_key_handle],
          handleStack: arguments[arg_key_handleStack],
          time: arguments[arg_key_time],
          noteMap: arguments[arg_key_note],
          inputList: arguments[arg_key_inputlist],
        ));
      default:
        return MaterialPageRoute(
            builder: (_) => Scaffold(
                  body: Center(
                    child: Text('No route defined for ${settings.name}'),
                  ),
                ));
    }
  }
}

常用的路由跳转动画,代码如下(代码来源,RESPECT)

import 'package:flutter/material.dart';
///路由动画
//缩放路由动画
class ScaleRouter<T> extends PageRouteBuilder<T> {
  final Widget child;
  final int durationMs;
  final Curve curve;
  ScaleRouter(
      {this.child, this.durationMs = 500, this.curve = Curves.fastOutSlowIn})
      : super(
          pageBuilder: (context, animation, secondaryAnimation) => child,
          transitionDuration: Duration(milliseconds: durationMs),
          transitionsBuilder: (context, a1, a2, child) => ScaleTransition(
            scale: Tween(begin: 0.0, end: 1.0)
                .animate(CurvedAnimation(parent: a1, curve: curve)),
            child: child,
          ),
        );
}
//渐变透明路由动画
class FadeRouter<T> extends PageRouteBuilder<T> {
  final Widget child;
  final int durationMs;
  final Curve curve;
  final RouteSettings settings;
  FadeRouter(
      {this.child,
      this.durationMs = 500,
      this.curve = Curves.fastOutSlowIn,
      this.settings})
      : super(
            pageBuilder: (context, animation, secondaryAnimation) => child,
            settings: settings,
            transitionDuration: Duration(milliseconds: durationMs),
            transitionsBuilder: (context, a1, a2, child) => FadeTransition(
                  opacity: Tween(begin: 0.1, end: 1.0).animate(CurvedAnimation(
                    parent: a1,
                    curve: curve,
                  )),
                  child: child,
                ));
}
//旋转路由动画
class RotateRouter<T> extends PageRouteBuilder<T> {
  final Widget child;
  final int durationMs;
  final Curve curve;
  RotateRouter(
      {this.child, this.durationMs = 500, this.curve = Curves.fastOutSlowIn})
      : super(
            pageBuilder: (context, animation, secondaryAnimation) => child,
            transitionDuration: Duration(milliseconds: durationMs),
            transitionsBuilder: (context, a1, a2, child) => RotationTransition(
                  turns: Tween(begin: 0.1, end: 1.0).animate(CurvedAnimation(
                    parent: a1,
                    curve: curve,
                  )),
                  child: child,
                ));
}
//右--->左
class Right2LeftRouter<T> extends PageRouteBuilder<T> {
  final Widget child;
  final int durationMs;
  final Curve curve;
  Right2LeftRouter(
      {this.child, this.durationMs = 500, this.curve = Curves.fastOutSlowIn})
      : super(
            transitionDuration: Duration(milliseconds: durationMs),
            pageBuilder: (ctx, a1, a2) => child,
            transitionsBuilder: (
              ctx,
              a1,
              a2,
              child,
            ) =>
                SlideTransition(
                  child: child,
                  position: Tween<Offset>(
                    begin: Offset(1.0, 0.0),
                    end: Offset(0.0, 0.0),
                  ).animate(CurvedAnimation(parent: a1, curve: curve)),
                ));
}
//左--->右
class Left2RightRouter<T> extends PageRouteBuilder<T> {
  final Widget child;
  final int durationMs;
  final Curve curve;
  List<int> mapper;
  Left2RightRouter(
      {this.child, this.durationMs = 500, this.curve = Curves.fastOutSlowIn})
      : assert(true),
        super(
            transitionDuration: Duration(milliseconds: durationMs),
            pageBuilder: (ctx, a1, a2) {
              return child;
            },
            transitionsBuilder: (
              ctx,
              a1,
              a2,
              child,
            ) {
              return SlideTransition(
                  position: Tween<Offset>(
                    begin: Offset(-1.0, 0.0),
                    end: Offset(0.0, 0.0),
                  ).animate(CurvedAnimation(parent: a1, curve: curve)),
                  child: child);
            });
}
//上--->下
class Top2BottomRouter<T> extends PageRouteBuilder<T> {
  final Widget child;
  final int durationMs;
  final Curve curve;
  Top2BottomRouter(
      {this.child, this.durationMs = 500, this.curve = Curves.fastOutSlowIn})
      : super(
            transitionDuration: Duration(milliseconds: durationMs),
            pageBuilder: (ctx, a1, a2) {
              return child;
            },
            transitionsBuilder: (
              ctx,
              a1,
              a2,
              child,
            ) {
              return SlideTransition(
                  position: Tween<Offset>(
                    begin: Offset(0.0, -1.0),
                    end: Offset(0.0, 0.0),
                  ).animate(CurvedAnimation(parent: a1, curve: curve)),
                  child: child);
            });
}
//下--->上
class Bottom2TopRouter<T> extends PageRouteBuilder<T> {
  final Widget child;
  final int durationMs;
  final Curve curve;
  Bottom2TopRouter(
      {this.child, this.durationMs = 500, this.curve = Curves.fastOutSlowIn})
      : super(
            transitionDuration: Duration(milliseconds: durationMs),
            pageBuilder: (ctx, a1, a2) => child,
            transitionsBuilder: (
              ctx,
              a1,
              a2,
              child,
            ) {
              return SlideTransition(
                  position: Tween<Offset>(
                    begin: Offset(0.0, 1.0),
                    end: Offset(0.0, 0.0),
                  ).animate(CurvedAnimation(parent: a1, curve: curve)),
                  child: child);
            });
}
//缩放+透明+旋转路由动画
class ScaleFadeRotateRouter<T> extends PageRouteBuilder<T> {
  final Widget child;
  final int durationMs;
  final Curve curve;
  ScaleFadeRotateRouter(
      {this.child, this.durationMs = 1000, this.curve = Curves.fastOutSlowIn})
      : super(
            transitionDuration: Duration(milliseconds: durationMs),
            pageBuilder: (ctx, a1, a2) => child, //页面
            transitionsBuilder: (
              ctx,
              a1,
              a2,
              Widget child,
            ) =>
                RotationTransition(
                  //旋转动画
                  turns: Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
                    parent: a1,
                    curve: curve,
                  )),
                  child: ScaleTransition(
                    //缩放动画
                    scale: Tween(begin: 0.0, end: 1.0)
                        .animate(CurvedAnimation(parent: a1, curve: curve)),
                    child: FadeTransition(
                      opacity: //透明度动画
                          Tween(begin: 0.5, end: 1.0).animate(
                              CurvedAnimation(parent: a1, curve: curve)),
                      child: child,
                    ),
                  ),
                ));
}
//无动画
class NoAnimRouter<T> extends PageRouteBuilder<T> {
  final Widget child;
  NoAnimRouter({this.child})
      : super(
            opaque: false,
            pageBuilder: (context, animation, secondaryAnimation) => child,
            transitionDuration: Duration(milliseconds: 0),
            transitionsBuilder:
                (context, animation, secondaryAnimation, child) => child);
}

5.屏幕适配

使用的插件是flutter_screenutil,适配原理和android的今日头条适配方案一致,按宽度适配,填入设计稿的设备屏幕尺寸,相关代码如下,在应用的第一个界面的Widget build(BuildContext context)方法下初始化即可,

///初始化,设置适配尺寸 (填入设计稿中设备的屏幕尺寸)
ScreenUtil.init(context, designSize: Size(375, 667), allowFontScaling: true);
///使用
import 'package:flutter_screenutil/flutter_screenutil.dart';
height: 300.w
fontSize: 35.sp

6.手机唯一标识uuid

使用的插件是device_info,相关代码如下

///设备唯一标识
Future<String> getUniqueId() async {
  DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
  if (Platform.isIOS) {
    IosDeviceInfo iosDeviceInfo = await deviceInfo.iosInfo;
    logD("ios唯一设备码:" + iosDeviceInfo.identifierForVendor);
    return 'IOS_' + iosDeviceInfo.identifierForVendor; // unique ID on iOS
  } else if (Platform.isAndroid) {
    AndroidDeviceInfo androidDeviceInfo = await deviceInfo.androidInfo;
    logD("android唯一设备码:" + androidDeviceInfo.androidId);
    return 'AND_' + androidDeviceInfo.androidId; // unique ID on Android
  } else {
    logD("Platform not support." + Platform.operatingSystem);
    return "Platform not support." + Platform.operatingSystem;
  }
}

7.log

使用的插件是logger,相关代码如下

final logger = Logger(
  printer: PrettyPrinter(),
);
logD(dynamic d) {
    logger.d(d);
}

8.sp

使用的插件是shared_preferences,做了一些简单封装,代码如下

///sp实现类,所有sp相关的,都统一在此写,包括put和get
import 'package:shared_preferences/shared_preferences.dart';

SharedPreferences _sp;

Future<bool> spInit() async {
  _sp ??= await SharedPreferences.getInstance();
  return Future<bool>.value(true);
}

//get的简单封装,增加默认返回值
dynamic _spGetDef(String key, dynamic def) {
  final dynamic res = _sp?.get(key);
  if (res == null) {
    return def;
  } else
    return res;
}

//put的简单封装
Future<bool> _spPutString(String key, String value) async {
  await spInit();
  return _sp?.setString(key, value);
}

Future<bool> _spPutInt(String key, int value) async {
  await spInit();
  return _sp?.setInt(key, value);
}

Future<bool> _spPutDouble(String key, double value) async {
  await spInit();
  return _sp?.setDouble(key, value);
}

Future<bool> _spPutBool(String key, bool value) async {
  await spInit();
  return _sp?.setBool(key, value);
}

//测试
const String _key_test = '_key_test';

Future<bool> spTestPut(String s) async {
  return _spPutString(_key_test, s);
}

String spTestGet() {
  return _spGetDef(_key_test, '');
}

widget相关

1.类似android上的viewpager+tabLayout实现效果

在flutter上可以使用PageView+bottomNavigationBar实现。
PageView默认使用的回弹效果和android上的一样,可以使用physics这个属性去修改,如果使用ClampingScrollPhysics,则page不再可以滑动,也不会有回弹效果;如果使用BouncingScrollPhysics,回弹效果和ios类似,也可以使用其他实现类,具体效果自测。pageSnapping 属性控制滚动方式,默认true的情况下,PageView都是一页一页翻的,如果设置成false,这个PageView就可以一点点滚动,就是不用整页滚动过去,滚动到一半的时候也能停下来,具体效果自测。

2.view点击效果

使用InkWell实现,borderRadius属性修改点击效果的圆角。enableFeedback: false属性关闭反馈(开启反馈的情况下,部分android手机点击后会有点击音效)

3.LinearLayout权重

android中,使用LinearLayout的weight实现权重,在flutter可以使用Row(或者Column)+Expanded实现,Expanded的flex属性即为权重。

4.监听back按钮

在Widget build(BuildContext context)方法下,使用WillPopScope包装Scaffold,然后相关代码在onWillPop属性下写。

Future<bool> _finish() {
    ///do something
    return Future.value(true);
  }

 return WillPopScope(
      onWillPop: _finish,
      child: Scaffold());

5.保存page状态

使用PageView或者TabBarView时,当page页面发生切换时,默认会销毁,再切换回来会重绘。如果需要保存page的状态,让他不会销毁,可以让State实现类混合AutomaticKeepAliveClientMixin,实现wantKeepAlive方法并返回true。

6.修改控件大小

有一些控件(比如CupertinoSwitch),大小是固定的,不能直接通过设置宽高来控制他的大小,可以使用缩放的方式实现,代码如下

 child: Transform.scale(
                scale: 0.88,
                child: CupertinoSwitch(
                    activeColor: app_theme_color,
                    value: on,
                    onChanged: (b) {
                      callback(b);
                    }),
              ),

7.dialog相关

barrierDismissible控制是否可以点击阴影取消dialog。
使用WillPopScope包装AlertDialog,可以控制返回键是否取消dialog。
想自定义圆角,可以使用AlertDialog的shape属性+ClipRRect裁剪content,代码如下

AlertDialog(
            shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.all(Radius.circular(10.w))),
content: ClipRRect(
                borderRadius: BorderRadius.circular(10.w),

监听dialog的取消,showDialog(或者showCupertinoDialog)方法是异步的,会返回Future,使用 await showDialog(...); testBlock; 当dialog取消后会执行 testBlock

8.Text多余的黄线问题

当使用Text出现多余的黄线时,可以将style中的inherit参数设置为false解决
比如使用hero动画包装Text,动画时会出现黄色的下划线,可以使用以上方法解决

dart语法相关

1.json和Model类互转

只需要Model类实现两个如下的方法,然后就可以直接使用json

///实现方法
Point.fromJson(Map<String, dynamic> json)
      : x = json['x'],
        y = json['y'];

  Map toJson() {
    Map map = new Map();
    map["x"] = this.x;
    map["y"] = this.y;
    return map;
  }

  import 'dart:convert';
  ///Model类转json字符串
  String str = json.encode(bean);

  ///json字符串转Model类
  Bean bean = Bean.fromJson(json.decode(str));

android平台相关

ios平台相关

其他

1.AES加密

使用插件encrypt实现,代码如下

import 'package:encrypt/encrypt.dart' as my_encrypt;
String aesDecoded(String s) {
  //加密的key和偏移量,不可改动,要和对json加密时使用一样
  final key = my_encrypt.Key.fromUtf8('f4k9f5w7f8g4er26'); //加密key
  final iv = my_encrypt.IV.fromUtf8('5e8y6w45ju8w9jq8'); //偏移量
  //设置cbc模式
  final encrypter = my_encrypt.Encrypter(my_encrypt.AES(key, mode: my_encrypt.AESMode.cbc));
  //encrypter.encrypt(s, iv: iv);//加密
  final decrypted = encrypter.decrypt(my_encrypt.Encrypted.from64(s), iv: iv);
  return decrypted;
}

对应的java AES加解密代码如下


import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

private static final String KEY = "f4k9f5w7f8g4er26";// 密匙
    private static final String OFFSET = "5e8y6w45ju8w9jq8"; // 偏移量
    private static final String ENCODING = "UTF-8"; // 编码
    private static final String ALGORITHM = "AES";//算法
    private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; // 默认的加密算法

    /**
     * 加密
     */
    public static String encrypt(String data) throws Exception {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        SecretKeySpec skeySpec = new SecretKeySpec(KEY.getBytes("ASCII"), ALGORITHM);
        IvParameterSpec iv = new IvParameterSpec(OFFSET.getBytes());//使用CBC模式,需要一个向量iv,可增加加密算法的强度
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
        byte[] encrypted = cipher.doFinal(data.getBytes(ENCODING));
        return new BASE64Encoder().encode(encrypted);//此处使用BASE64做转码。
        //当数据比较大时,生成的string会多出换行符,待优化
    }

    /**
     * 解密
     */
    public static String decrypt(String data) throws Exception {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        SecretKeySpec skeySpec = new SecretKeySpec(KEY.getBytes("ASCII"), ALGORITHM);
        IvParameterSpec iv = new IvParameterSpec(OFFSET.getBytes());//使用CBC模式,需要一个向量iv,可增加加密算法的强度
        cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
        byte[] buffer = new BASE64Decoder().decodeBuffer(data);
        byte[] encrypted = cipher.doFinal(buffer);
        return new String(encrypted, ENCODING);//此处使用BASE64做转码。
    }

2.实际开发中用到的一些工具类型网站

Flutter组件介绍-老孟
Flutter开发中的一些Tips
pub查找-官方
Json 2 Dart-json生成bean
Flutter绘制学习-掘金小册,需购买

3.奇奇怪怪的问题

3.1 在根界面调用pop(退出应用)时,state的deactivate方法和dispose方法不会执行。