Flutter基础知识
最近准备为组里面的小伙伴分享flutter相关的知识,因此对flutter进行了一些整理,主要包括widget、路由、状态管理,以及一些常见的需求处理方案。
准备工作
安装
网上有大把教程进行安装,这里只是简单描述一下步骤就行了
- 下载对应环境的flutter SDK,解压到本地
- 配置flutter到环境变量,确保flutter命令可以使用
- 运行
flutter doctor
检测开发环境 - 欧了~
Dart语法
flutter采用Dart作为开发语言,关于Dart的相关语法,可以先看一下这篇教程。
Dart是一门很有趣的语言,在VSCode中安装flutter插件后,就可以体验代码高亮、语法补全等功能了。如果使用Android Studio,还需要安装Dart插件,配置flutter的SDK路径等工作。
貌似Dart还可以编译成JavaScript的代码,这个后面再研究下~
真机调试
纯粹的UI开发的话,模拟器就够用了,但在某些场景下,需要真机调试
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包安装在手机上即可。
Android真机调试
Android真机的安装要比iOS简单一点
- 通过数据线连接电脑,windows貌似需要安装USB驱动
- 手机开启“开发者模式”和“USB调试”选项
- 提示“是否允许USB调试”,选择“是”
真机安装后,就可以愉快地使用啦~
UI开发
Widget
参考
widget 部件是每个Flutter应用程序的基本构建块,通过组合不同的 Widget,来实现我们用户交互界面,这跟Vue和React中的Components十分相似。
组件可分为StatelessWidget无状态组件和StatefulWidget有状态组件两大类,
- StatelessWidget,只用于展示信息,无用户交互行为
- StatefulWidget,可以通过改变状态(数据)使得UI发生变化,可以包含用户交互
flutter的部件也是有生命周期的,大致生命周期可以分为
- initState : 初始化widget的时候调用,只会调用一次。
- build : 初始化之后开始绘制界面,当setState触发的时候会再次被调用
- didUpdateWidget : 当触发setState时,会被调用
- dispose : 页面销毁的时候调用
无状态部件和有状态部件的区别在于,StatelessWidget组件只会build绘制一次,而StatefulWidget主要状态发生改变,就会重新调用build方法,重新进行绘制。
样式
Flutter中的布局样式与CSS中部分概念比较相似,如margin、padding、border等,尤其是CSS中的flex布局,在Flutter得到了有力的支持。
尽管概念比较相似,但是在flutter中为widget添加样式与CSS添加样式还是有很大区别的,而在Flutter中
- 不是所有 Widget 都可以添加任意的样式属性,有的部件只有布局样式,有的部件只有展示样式
- 由于布局、样式和逻辑都一起书写到widget上,部件的嵌套可能就比较深~论两个空格缩进的重要性
- 由于Dart语言的关系,基本上所有的样式属性都不在支持以字符串的形式书写,而是必须创建特定类的实例或是使用 Flutter 中预先定义好的常量
屏幕适配
在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.sp
、200.h
之类的快捷尺寸
字体图标
在flutter中,也可以使用类似于iconfont
类似的字体图标,使用方式也十分简单
从iconfont选择对应的图标,然后下载字体文件,并将下载包内的
*.ttf
字体放在flutter项目资源目录下配置
pubspec.yaml
下的fonts项,设置fontFaimly和字体路径然后就可以在代码中使用iconfont字体创建IconData对象了,其中,如
0xe6bb
这样的十六进制数据在iconfont官网下载时切换到Unicode编码获取到dartIcon(IconData(0xe65b, fontFamily: 'iconfont'),color: Colors.blue,size: 89.0)
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()
));
}
路由
路由管理,就是管理页面之间如何跳转,其原理是:维护一个路由栈,路由入栈(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可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画。
页面之间的数据传递
在实例化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);
}
命名路由
MaterialApp
构造函数接收一个routes
构造参数,作为命名路由映射表
MaterialApp(
// ...
//注册路由表
routes:{
"new_page":(context)=>NewRoute(),
} ,
);
当有了路由表之后,除了手动传入一个Route对象外,还可以通过传入命名路由的名字来实现页面的跳转
Navigator.pushNamed(context, "new_page");
由于路由表是提前注册的,因此无法通过构造参数的形式动态传递路由参数。
弹窗
需要注意的是flutter中的弹窗页相当于是一个新的页面,因此关闭弹窗也是通过Navigator.pop()来实现的
showDialog(
context: context,
child: AlertDialog(
actions: <Widget>[
FlatButton(
child: const Text('取消'),
onPressed: () {
Navigator.of(context).pop(false);
},
),
],
);
);
状态管理
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),
)
本地存储
应用往往需要在本地持久化存储一些数据,在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");
网络请求
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";
};
};
}
与Native交互
Flutter使用了一个灵活的系统,允许用户调用特定平台的API,无论在Android上的Java或Kotlin代码中,还是iOS上的ObjectiveC或Swift代码中均可用。
Flutter与原生之间的通信依赖灵活的消息传递方式:
- 应用的Flutter部分通过平台通道(platform channel)将消息发送到其应用程序的所在的宿主(iOS或Android)应用(原生应用)。
- 宿主监听平台通道,并接收该消息。然后它会调用该平台的API,并将响应发送回客户端,即应用程序的Flutter部分。
开发原生插件需要具备相关平台的开发知识,社区提供了一些跟平台相关的插件,如fluwx
(调用微信SDK)等.
与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
常见问题
flutter命令被阻塞
在使用flutter doctor
或者打包iOS真机包的时候遇见提示
Waiting for another flutter command to release the startup lock
在使用了flutter命令或者程序异常退出后,可能会出现这种提示,这时需要等待其他flutter命令执行完毕,或者手动删除<YOUR FLUTTER FOLDER>/bin/cache/lockfile
文件
本地图片资源加载失败
原来本地图片需要在pubsepc.yaml
中进行声明
flutter:
assets:
- assets/img/ic_main_tab_company_pre.png
- assets/img/ic_main_tab_my_pre.png
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。