Flutter基础知识
最近准备为组里面的小伙伴分享flutter相关的知识,因此对flutter进行了一些整理,主要包括widget、路由、状态管理,以及一些常见的需求处理方案。
<!--more-->
1. Widget
参考
- Flutter Widget框架概述
- Widget目录 Flutter中,通过widget构建UI,其定义与React中组件基本类似。
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
类似的字体图标,使用方式也十分简单
从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.....'),
),
),
);
}
}