Flutter基础知识

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

<!--more-->

1. Widget

参考

1.1. 组件分类

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

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

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

StatefulWidget有生命周期的,大致可以分为

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

1.2. 样式

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

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

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

1.3. 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()
    ));
}

2. 路由

路由管理,就是管理页面之间如何跳转,其原理是:维护一个路由栈,路由入栈(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可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画。

2.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);
}

2.2. 命名路由

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

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

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

Navigator.pushNamed(context, "new_page");

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

2.3. 弹窗

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

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

3. 状态管理

参考:

在React中,常见的状态管理方式有下面几种

3.1. ScopedModel

// todo

3.2. BLoC

// todo 参考

3.3. redux

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),
)

4. 常见需求

下面整理在flutter中一些常见的需求

4.1. 屏幕适配

在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");
    }
  });
}

4.2. 字体图标

参考:如何在 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)

4.3. 本地存储

应用往往需要在本地持久化存储一些数据,在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");

4.4. 网络请求

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";
        };
    };
}

4.5. 与Native交互

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

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

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

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

4.6. webview

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.....'),
        ),
      ),
    );
  }
}
Preact技术栈:router和redux 2018年五月面试发现的一些问题