Flutter基础知识

最近准备为组里面的小伙伴分享flutter相关的知识,因此对flutter进行了一些整理,主要包括widget、路由、状态管理,以及一些常见的需求处理方案。

<!--more-->

1. 准备工作

1.1. 安装

网上有大把教程进行安装,这里只是简单描述一下步骤就行了

  • 下载对应环境的flutter SDK,解压到本地
  • 配置flutter到环境变量,确保flutter命令可以使用
  • 运行flutter doctor检测开发环境
  • 欧了~

1.2. Dart语法

flutter采用Dart作为开发语言,关于Dart的相关语法,可以先看一下这篇教程

Dart是一门很有趣的语言,在VSCode中安装flutter插件后,就可以体验代码高亮、语法补全等功能了。如果使用Android Studio,还需要安装Dart插件,配置flutter的SDK路径等工作。

貌似Dart还可以编译成JavaScript的代码,这个后面再研究下~

2. 真机调试

纯粹的UI开发的话,模拟器就够用了,但在某些场景下,需要真机调试

2.1. iOS真机调试

首先参考官方文档进行安装,需要配置环境变量啥的,以及XCode。

brew update

brew install --HEAD libimobiledevice

brew install ideviceinstaller ios-deploy cocoapods

pod setup

然后进行下面流程:

  • 切换到项目目录,运行open ios/Runner.xcworkspace
  • 此时会打开xcode的runner,配置好开发证书,使用数据线连接iPhone真机
    • 如果弹出“要信任此电脑吗?”的弹框,请选择“信任”
  • 然后左上角,选择运行目标为Device->iPhone,点击运行,进行安装
  • 稍等片刻,就能在手机上看见安装的app了~
    • 个人账号需要在手机“设置”->“通用” -> “描述文件与设备管理”中信任开发者,然后才能打开程序

真机安装遇见的一个问题是: ListView滑动感觉比较卡顿

这个貌似是是因为安装的debug的缘故,安装release包就可以体验如丝般顺滑的体验。

flutter build ios --release

然后重新open ios/Runner.xcworkspace将release包安装在手机上即可。

2.2. Android真机调试

Android真机的安装要比iOS简单一点

  • 通过数据线连接电脑,windows貌似需要安装USB驱动
  • 手机开启“开发者模式”和“USB调试”选项
    • 提示“是否允许USB调试”,选择“是”

真机安装后,就可以愉快地使用啦~

3. UI开发

3.1. Widget

参考

widget 部件是每个Flutter应用程序的基本构建块,通过组合不同的 Widget,来实现我们用户交互界面,这跟Vue和React中的Components十分相似。

组件可分为StatelessWidget无状态组件和StatefulWidget有状态组件两大类,

  • StatelessWidget,只用于展示信息,无用户交互行为
  • StatefulWidget,可以通过改变状态(数据)使得UI发生变化,可以包含用户交互

flutter的部件也是有生命周期的,大致生命周期可以分为

  • initState : 初始化widget的时候调用,只会调用一次。
  • build : 初始化之后开始绘制界面,当setState触发的时候会再次被调用
  • didUpdateWidget : 当触发setState时,会被调用
  • dispose : 页面销毁的时候调用

无状态部件和有状态部件的区别在于,StatelessWidget组件只会build绘制一次,而StatefulWidget主要状态发生改变,就会重新调用build方法,重新进行绘制。

3.2. 样式

Flutter中的布局样式与CSS中部分概念比较相似,如margin、padding、border等,尤其是CSS中的flex布局,在Flutter得到了有力的支持。

尽管概念比较相似,但是在flutter中为widget添加样式与CSS添加样式还是有很大区别的,而在Flutter中

  • 不是所有 Widget 都可以添加任意的样式属性,有的部件只有布局样式,有的部件只有展示样式
  • 由于布局、样式和逻辑都一起书写到widget上,部件的嵌套可能就比较深~论两个空格缩进的重要性
  • 由于Dart语言的关系,基本上所有的样式属性都不在支持以字符串的形式书写,而是必须创建特定类的实例或是使用 Flutter 中预先定义好的常量

3.3. 屏幕适配

在web移动端现在最常见的屏幕适配方式是rem,在移动端和小程序有诸如rpx、dp等单位,在flutter中貌似需要手动处理屏幕适配,其原理与rem基本类型

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

class Adapt {
  static MediaQueryData mediaQuery = MediaQueryData.fromWindow(window);
  static double _width = mediaQuery.size.width;
  static double _height = mediaQuery.size.height;
  static double _topbarH = mediaQuery.padding.top;
  static double _botbarH = mediaQuery.padding.bottom;
  static double _pixelRatio = mediaQuery.devicePixelRatio;
  static var _ratio;

  static init(int number) {
    int uiwidth = number is int ? number : 750;
    mediaQuery = MediaQueryData.fromWindow(window);

    _ratio = _width / uiwidth;
  }

  static px(number) {
    return number * _ratio;
  }
}

num rem(num px){
    return Adpa.px(px)
}

然后在布局中通过rem函数处理尺寸单位

Container(
    width: rem(100),
    widht: rem(50)
)

上面代码在debug模式下可以正常运行,但是在release模式下,往往会出现问题(相关issue),其原因在于:mediaQuery是在初始化中赋值的,在release模式下,代码初始化时,获取到的mediaQuery.size.width为空,导致计算的radio一直为0。

一种解决办法是通过轮询来判断MediaQueryData是否已经成功获取就绪

void main() async {
  // release模式下mediaQuery获取到的值为空,因此需要等待其返回正确结果时才渲染页面
  Timer queryTimer;
  queryTimer = new Timer.periodic(new Duration(milliseconds: 50), (timer) {
    var queryData = MediaQueryData.fromWindow(window);
    if (queryData.size.width != 0) {
      queryTimer.cancel();
      runApp(MaterialApp(
          // ...
      );
    } else {
      print("waiting for MediaQueryData.fromWindow");
    }
  });
}

flutter_screenutil这个库提供了非常便捷的屏幕实配单位,借助dart 新增的exntension特性,可以实现诸如100.sp200.h之类的快捷尺寸

3.4. 字体图标

参考:如何在 Flutter 中使用 IconFont

在flutter中,也可以使用类似于iconfont类似的字体图标,使用方式也十分简单

  • iconfont选择对应的图标,然后下载字体文件,并将下载包内的*.ttf字体放在flutter项目资源目录下

  • 配置pubspec.yaml下的fonts项,设置fontFaimly和字体路径

  • 然后就可以在代码中使用iconfont字体创建IconData对象了,其中,如0xe6bb这样的十六进制数据在iconfont官网下载时切换到Unicode编码获取到

    Icon(IconData(0xe65b, fontFamily: 'iconfont'),color: Colors.blue,size: 89.0)

3.5. UI调试

在Android Studio或IDEA中,可以使用内置的Flutter inspector来实现布局调试。详情可参考Flutter Widget Inspecto-官方文档,使用方式与Chrome开发者工具比较相似

此外可以通过rendering.dart包来调试布局,开启debugPaintSizeEnabled后可以在布局页面上看见很多箭头网格,了解大致的布局嵌套(虽然这个功能不太好用~)

import 'package:flutter/rendering.dart' show debugPaintSizeEnabled;

void main(){
    debugPaintSizeEnabled = true; // 开启调试功能
    runApp(new MaterialApp(
        title: 'Fun',
        home: new FunApp()
    ));
}

4. 路由

路由管理,就是管理页面之间如何跳转,其原理是:维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,路由重定向(redirect)替换栈顶的页面为新页面。

在Flutter中的路由管理主要是指如何通过Navigator对象来管理路由栈。

// 跳转到登录页面
Navigator.push(
        context, 
        MaterialPageRoute(builder: (context) => LoginPage()));

// 重定向到结果页面
Navigator.pushReplacement(
        context, 
        MaterialPageRoute(builder: (context) => LoginPage()));
// 返回上页面
Navigator.pop(context, [result]);

其中MaterialPageRoute是Material组件库的一个路由Widget,继承至PageRoute抽象类,MaterialPageRoute可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画。

4.1. 页面之间的数据传递

在实例化Route对象时,可以在builder函数中传入对应Widget的构造参数,然后在对应的组件中,就可以通过Widget的构造参数获得路由参数

MaterialPageRoute(builder: (context) => DetailPage({id:1, utm_resource: '_ad'}))

Navigator.push方法返回的是一个Futher对象,可以接收到Navigator.pop(contenxt, result)的第二个参数,因此可以实现从新页面返回数据给上一个页面的逻辑

_setting() async {
    var result = await Navigator.push(
        context, MaterialPageRoute(builder: (context) => SettintPage());

    print(result);
}

4.2. 命名路由

MaterialApp构造函数接收一个routes构造参数,作为命名路由映射表

MaterialApp(
  // ...
  //注册路由表
  routes:{
   "new_page":(context)=>NewRoute(),
  } ,
);

当有了路由表之后,除了手动传入一个Route对象外,还可以通过传入命名路由的名字来实现页面的跳转

Navigator.pushNamed(context, "new_page");

由于路由表是提前注册的,因此无法通过构造参数的形式动态传递路由参数。

4.3. 弹窗

需要注意的是flutter中的弹窗页相当于是一个新的页面,因此关闭弹窗也是通过Navigator.pop()来实现的

showDialog(
    context: context,
    child: AlertDialog(
        actions: <Widget>[
            FlatButton(
                child: const Text('取消'),
                onPressed: () {
                    Navigator.of(context).pop(false);
                },
            ),
        ],
    );
);

5. 状态管理

StatefulWidget自身就有个一个State实例来管理组件自身的状态,但如果需要在多个组件之间共享数据,就需要有一个状态管理的方案

参考:

redux-dart使用Dart编写的redux版本,下面是官方教程的示例

import 'package:redux/redux.dart';

// action type
enum Actions {
  increment,
  decrement,
}

// reducer
int counterReducer(int state, action) {
  if (action == Actions.increment) {
    return state + 1;
  } else if (action == Actions.decrement) {
    return state - 1;
  }

  return state;
}
// 中间件
loggingMiddleware(Store<int> store, action, NextDispatcher next) {
  print('${new DateTime.now()}: $action');

  next(action);
}

// store,全局唯一的store,可以自定义泛型,传入复杂的数据结构
final store = new Store<int>(
  counterReducer,
  initialState: 0,
  middleware: [loggingMiddleware],
);
// 然后在其他组件中引入即可
void initState() {
    super.initState();

    store.onChange.listen((state) {
        print("store state change to: $state");
        setState(() {
        _counter = store.state;
        });
    });
}
// 在UI中dispatch 动作 actionType
FloatingActionButton(
    onPressed: (){
         store.dispatch(Actions.increment);
    },
    tooltip: 'Increment',
    child: Icon(Icons.add),
)

6. 本地存储

应用往往需要在本地持久化存储一些数据,在web中可以通过LocalStorage,在flutter中可以通过shared_preferences实现相同的功能。

一般地,在APP中,为了节省内存资源,文件操作、网络请求等操作都会使用单例类。

import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';

class SPUtil {
  static SharedPreferences _prefs;

  static SPUtil _instance;
  // 实现单例
  static Future<SPUtil> getInstance() async {
    if (_instance == null) {
      _instance = new SPUtil._();
      await _instance._init();
    }
    return _instance;
  }

  SPUtil._();

  static Object _lock = new Object();

  _init() async {
    _prefs = await SharedPreferences.getInstance();
  }

  getSP() {
    return _prefs;
  }
}

由于读取本地数据是异步的,因此需要放在回调或await中处理

await spUtil = SPUtil.getInstance();
SharedPreferences sp =  spUtil.getSP();
var prefs = sp.getSP();

// 调用相关接口存储或读取数据
prefs.setString("uid", "xs_d131");

7. 网络请求

Dart IO库中提供了Http请求的一些类,我们可以直接使用HttpClient来发起请求,此外,社区还提供了一个比较好用的网络请求库dio

import 'package:dio/dio.dart';

class DioFactory {
  static Dio _dio;

  static DioFactory _instance;
  // 实现单例
  static DioFactory getInstance() {
    if (_instance == null) {
      _instance = new DioFactory._();
      _instance._init();
    }
    return _instance;
  }

  DioFactory._();

  _init() {
        // 基础配置信息
    Options opt = Options(
            baseUrl: "https://test.com/api/",
            connectTimeout: 5000,
            receiveTimeout: 3000);

    _dio = Dio(opt);

    _dio.interceptor.request.onSend = (Options options) {
        // optios.data['from'] = 'app'
        // options.headers["XX-Token"] = userInfo.appToken;
        // print(options.data);
        // print(options.baseUrl + options.path);
        // 在请求被发送之前做一些事情
        return options;
        };
    _dio.interceptor.response.onSuccess = (Response response) {
        // 在返回响应数据之前做一些预处理
        return response; // continue
    };
    _dio.interceptor.response.onError = (DioError e) {
        // 当请求失败时做一些预处理
        return DioError; //continue
    };
  }

  getDio() {
    return _dio;
  }
}

需要注意的是,如果需要使用fiddler、charles等代理进行抓包,需要对dio进行代理配置

if (isDebug) {
    print("debug模式下启动代理配置用于抓包");
    _dio.onHttpClientCreate = (client) {
        client.findProxy = (uri) {
            return "PROXY 192.168.1.4:8887";
        };
    };
}

8. 与Native交互

Flutter使用了一个灵活的系统,允许用户调用特定平台的API,无论在Android上的Java或Kotlin代码中,还是iOS上的ObjectiveC或Swift代码中均可用。

Flutter与原生之间的通信依赖灵活的消息传递方式:

  • 应用的Flutter部分通过平台通道(platform channel)将消息发送到其应用程序的所在的宿主(iOS或Android)应用(原生应用)。
  • 宿主监听平台通道,并接收该消息。然后它会调用该平台的API,并将响应发送回客户端,即应用程序的Flutter部分。

开发原生插件需要具备相关平台的开发知识,社区提供了一些跟平台相关的插件,如fluwx(调用微信SDK)等.

9. 与Web交互

flutter本身不支持webview,可以借助flutter_webview_plugin插件,通过原生实现在flutter中使用webview的功能

import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';

class CommonWebView extends StatefulWidget {
  CommonWebView({Key key, this.title, this.url}) : super(key: key);
  final String title;
  final String url;

  @override
  State<StatefulWidget> createState() => _CommonWebViewPageState();
}

class _CommonWebViewPageState extends State<CommonWebView> {
  @override
  Widget build(BuildContext context) {
    return WebviewScaffold(
      url: widget.url,
      appBar: AppBar(
        title: Text(widget.title),
      ),
      scrollBar: true,
      withZoom: true,
      withLocalStorage: true,
      initialChild: Container(
        color: Colors.grey,
        child: const Center(
          child: Text('Waiting.....'),
        ),
      ),
    );
  }
}

flutter也可以向web注入一些功能;以及调用web提供的一些钩子,可以参考在webview_flutter中封装JSBridge

10. 常见问题

10.1. flutter命令被阻塞

在使用flutter doctor或者打包iOS真机包的时候遇见提示

Waiting for another flutter command to release the startup lock

在使用了flutter命令或者程序异常退出后,可能会出现这种提示,这时需要等待其他flutter命令执行完毕,或者手动删除<YOUR FLUTTER FOLDER>/bin/cache/lockfile文件

10.2. 本地图片资源加载失败

原来本地图片需要在pubsepc.yaml中进行声明

flutter:

  assets:

    - assets/img/ic_main_tab_company_pre.png

    - assets/img/ic_main_tab_my_pre.png

参考:在Flutter中添加资源和图片