Flutter1.0入门基础
注:原课程视频是基于Flutter1的;
目标:
- 开发入门:工具、环境搭建、入门必备
- 开发技巧:导航框架、常用功能
- 开发流程:网络、数据存储、列表、Flutter与Native混编
- 工程封装:模块开发、AI结合
- 项目上手:包和插件的开发、屏幕适配和兼容、打包与发布、Flutter的升级和适配;
导航框架实现:Scaffold+PageView
服务端交互:http
页面路由跳转:Navigator
监听列表滚动:NotificationListener
自定义组件
封装Native代码:NativeModules
Native实现智能语音
FLutter与native通信:Channel通信
各种插件和组件的使用;
滚动渐变
智能语音
富文本展示
混合开发(与ios android h5)
瀑布流布局
加载刷新
自定义webview
开发工具需安装:Flutter Plugin、Dart Plugin、Dart DevTools
布局:
声明式布局
Layout Widgets
导航:
Navigator
MaterialPageRoute
PageRouteBuilder
列表:
ListView
GridView
ExpansionTile(可折叠列表)
图片:
Image
静态图片
Native图片
网络图片
图片缓存
Icon
Placeholder
Flutter组件:
Flutter插件:
flutter_webview_plugin
cupertino_icons
page_view_indicator
flutter_swiper
http
flutter_staggered_grid_view
flutter_statusbar_manager
...
Native插件:
百度AI智能语音
...
自定义组件:
loading_container
asr_manager
webview
search_bar
网络和存储:
http
shared_preferences
JSON解析和模型转换
Future(异步编程)
FutureBuilder(异步UI)
动画:
基础动画
AnimateWidget
AnimatedBuilder
Animation
AnimationController
Tween
CurvedAnimation
Hero动画(实现页面间跳转动画)
高级:
Native Modules:智能AI语音
Flutter混合开发:
+ Android
+ iOS
+ H5
通信:
BasicMessageChannel
MethodChannel
EventChannel
全面屏/折叠屏适配:
iOS、Android全面屏适配
折叠屏适配
Flutter更新升级
打包发布
IDE:推荐AndroidStudio;
环境搭建
- 确认系统要求
- 设置Flutter镜像
- 获取Flutter SDK
- iOS开发环境配置
- Android开发环境设置
命令行工具:
bash curl git 2.x mkdir rm unzip which
设置镜像:
- 编辑当前用户主目录
.bash_profile
文件 export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
注:此镜像为临时镜像,并不能保证一直有用,可以参考官网进行配置;
下载Flutter SDK:
- 官网下载,解压到主目录(或其他目录)flutter目录;
- 添加环境变量:
export PATH=/Users/huaqiang/flutter/bin:$PATH
flutter doctor
:第一次运行会很慢
# 相关环境变量
# Android 环境变量
export ANDROID_HOME=/Users/huaqiang/Library/Android/sdk
# Android 模拟器路径
export PATH=${PATH}:${ANDROID_HOME}/emulator
# Android tools 路径
export PATH=${PATH}:${ANDROID_HOME}/tools
# Android 平台工具路径
export PATH=${PATH}:${ANDROID_HOME}/platform-tools
# Android NDK路径
export ANDROID_NDK_HOME=/Users/huaqiang/Library/Android/ndk/android-ndk-r10e
# Flutter 镜像
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
# Flutter环境变量
export PATH=/Users/huaqiang/flutter/bin:$PATH
iOS开发环境设置:
- 下载安装xcode
- 配置xcode使命令行工具使用新安装的xcode版本:
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
(也可以指定不同版本的xcode) - xcode许可协议通过:
sudo xcodebuild -license
iOS模拟器:
- 使用xcode启动,或通过命令:
open -a Simulator
创建Flutter项目:
flutter create my_app
cd my_app/
- ios:iOS端宿主项目
- android:Android端宿主项目
pubspec.yaml
:Flutter依赖包的配置文件- lib:flutter Dart代码
- 运行项目(命令行方式):
flutter run
r
:热加载R
:热重启q
:退出h
:请求帮助
iOS的真机调试:
# 安装Homebrew
# 更新Homebrew
brew update
# 将Flutter应用安装到iOS设备需要一些工具
brew install --HEAD usbmuxd
brew link usbmuxd
brew install --HEAD libimobiledevice
brew install ideviceinstaller ios-deploy cocoapods
pod setup
# 出现问题 可执行brew doctor按照说明进行解决
# 使用xcode打开项目 配置项目签名等信息,也可以通过命令打开项目
open ios/Runner.xcworkspace
安卓开发环境设置:
- 下载Android Studio,
https://developer.android.com/studio
,https://developer.android.google.cn/studio
,https://developer.android.google.cn/studio/install
- 执行Android Studio安装向导,这将安装最新的Android SDK,Android SDK平台工具和Android SDK构建工具;
运行Android模拟器:
- 启动硬件加速
- Tools-> Androids-> AVD Manager,选择Create Virtual Device
- 在Emulated Performance下,选择Hardware - GLES2.0 以启动硬件加速(windows上,可以为Android SDK 启动 HAXM)
- 在Android Virtual Device Manager中,点击工具栏Run
- 通过flutter run运行启动项目
在配置了emulator相关环境变量之后,也可以通过命令启动模拟器:emulator -avd a81<模拟器名>
运行Android真机:
- 开启 开发人员选项和USB调试;
- 链接USB,进行授权;
- 运行
flutter devices
验证是否链接设备; - 运行
flutter run
;
安装Flutter和Dart插件:
- 打开Android Studio
- Preferences -> Plugins(macOS上操作)
- File-> Settings-> Plugins(Windows & Linux上操作)
- 选择Browse repositories,搜索Flutter plugin、Dart插件,重启IDE;
环境准备好之后,项目创建和运行既可以用命令行方式,也可以使用IDE进行;关于项目的运行,可以使用flutter视角运行,也可以使用ios/android视角进行运行;
Flutter的快速上手
- Dart基础
- 声明时UI
- Flutter基础
- 项目结构、资源、依赖和本地化
- 视图(Views)
- 布局和列表
- 状态管理
- 路由与导航
- 主题和文字处理
- 线程和异步UI、Futures、async、await
- 手势检测和触摸事件处理
- 硬件调用,第三方交互
- 表单输入和富文本
Dart
Dart中,每个app都必须有一个顶级main()函数,作为程序入口;
Dart语法类似js,只不过增加类类型声明,也是强类型语言,支持类型推断:
var name = null
String name = 'hello'
// 只有布尔类型的true才是true,并不是非零即为真;
// 运算符(左边为null时->阻断 赋默认值)
?. ??
// 函数声明
返回值类型 函数名(){}
// Futures 相当于 ES6中的Promise
HttpRequest.request(url).then((value){
print(json.decode(value.responseText)['origin']);
}).catchError((error) => print(error));
// async 和 await
// js中async返回一个Promise,await用于等待Promise
// Dart中,async函数返回一个Future,函数主体稍后执行;await用于等待Future
_getIPAddress() async {
var response = awart HttpRequest.request(url)
...
}
声明式UI
- 之前熟悉的是手动构建全功能UI实体,UI响应时使用方法对其进行变更;
- 为了减轻开发人员在各种UI状态之间转换的负担,Flutter支持描述UI状态,但不关心UI如何过渡到框架;
- 声明式UI中,视图(Widgets)的配置是不可变的,并且是轻量级的蓝图;如果UI需要变更,Widgets会在自身上触发重建,比较常见的方式是通过Flutter中的StatefulWidgets调用setState(),并构造一个新的Widget子树;
return ViewB(
color:red,
child:ViewC(...)
)
- 框架使用
RenderObjects
管理传统UI对象(如维护布局的状态);RenderObjects
在帧之间保持不变;- Flutter的轻量级Widget会在状态改变时,改变
RenderObjects
,其余部分会由Flutter框架进行处理;
Flutter入门
- 如何创建Flutter项目、如何运行;
- 如何导入Widget、HelloWorld、Widget树、重用Widget;
flutter create <projectname>
flutter run -d 'iPhone X'// 指定设备
// 使用Material Design,需导入material.dart包
import 'package:flutter/material.dart';
// 使用iOS样式的widget,导入Cupertino库
import 'package:flutter/cupertino.dart';
// 更基本的窗口widget,请导入widget库
import 'package:flutter/widgets.dart';
// 也可以导入自己编写的widget;
import 'package:flutter/my_widgets.dart';
具体导入包,也是按需导入的,Dart只会导入应用中使用的Widget;
注:该视频是基于Flutter1的,目前版本相关语法可能会有改变;
HelloWorld:
import 'package:flutter/material.dart';
void main(){
runApp(
// 根widget
Center(
child: Text(
'Hello world',
textDirection: TextDirection.ltr,
)
)
)
}
Widget树:
- Flutter中,几乎所有的东西都是Widget;它是用户界面构成的基本块;
- 子widget会继承父widget的属性;应用程序本身也是一个组件,即根widget;
常用widget:
- 结构元素:按钮、菜单
- 文体元素:字体、颜色、主题
- 布局:填充、对齐
import 'package:flutter/maretial.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 根widget
return MeterialApp(
title: 'Welcome to Flutter',
home: Scaffold(// Scaffold 译为 脚手架
appBar: AppBar(
title: Text('Flutter'),
),
body: Center(
child: Text('Hello world'),
)
)
)
}
}
重用Widget:
// 自定义一个widget
class HQCustomCard extends StatelessWidget {
HQCustomCard({
@required this.index,
@required this.actionOnPress
});
final index;
final Function onPress;
@override
Widget build(BuildContext context) {
return Card(
child: Column(
children:<Widget>[
Text('Card $index'),
FlatButton(
child: const Text('Press'),
onPressed: this.actionOnPress
)
]
)
)
}
}
// 使用
HQCustomCard(
index: 1,
onPress:() {
print('something!');
}
)
项目结构、资源、依赖和本地化
- 项目文件结构
projectname
android
build // 构建输出目录
ios
lib // Dart源文件
src
...
main.dart // 入口文件
test // 测试文件
pubspec.yaml // 类似 vue的package.json 依赖项的配置文件
- 图片资源和分辨率处理
- 图片资源均作为assets处理,放在 assets 文件夹;
- 该文件夹中可以放置任意类型文件,而不仅仅是图片,如json;
注:assets文件夹是放在哪个目录的?
答:Assets文件夹可以被放置任何目录,Flutter并未预先定义该文件结构,只需要在pubspec.yaml中声明assets的位置,flutter会进行识别;
// 放置资源后,在pubspec.yaml中声明assets
assets:
- my-assets/data.json
// 使用示例
import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;
Future<String> loadAsset() async {
// 使用bulndle加载资源
return await rootBundle.loadString('my-assets/data.json');
}
对于图片,类似iOS,1x、2x、3x:
- ldpi 0.75x
- mdpi 1.0x
- hdpi 1.5x
- xhdpi 2.0x
- xxhdpi 3.0x
- xxxhdpi 4.0x
// 目录存放:
assets
images/my_icon.png
images/2.0x/my_icon.png
images/3.0x/my_icon.png
// pubspec.yaml中声明
assets:
- images/my_icon.png
// 访问,可借助AssetImage
return AssetImage("images/my_icon.png");
// 也可以通过Image widget直接使用:
return Image.asset("images/my_icon.png");
- 归解档strings资源、多语言处理
- 不像iOS拥有一个Localizable.strings文件,Flutter目前没有专门的字符串资源系统;
- 目前最佳做法是将strings资源作为静态字段保存在类中;
class Strings {
static String welcomeMessage = 'something';
}
// 使用
Text(Strings.welcomeMessage)
// 默认Flutter只支持英语,要支持其他语言,需引用 flutter_localizations包;
// 可能也需要引入 intl 包来支持其他的 i10n 机制,比如日期/时间格式化;
dependencies:
# ...
flutter_localizations:
sdk: flutter
intl: "^0.15.6"
// 要使用 flutter_localizations 包, 还需要在app widget中指定
// localizationsDelegates 和 supportedLocals
import 'package:flutter_localizations/flutter_localizations.dart';
MaterialApp(
// 这些代理包含实际的本地化值
localizationsDelegates: [
// App app-specific localization delegate[s] here
GlobalMeterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
// 定义支持的地区
supportedLocales: [
const Locale('en', 'US') // English
const Locale('he', 'IL') // Hebrew
// ... other locals the app supports
],
// ...
)
// 当初始化时,WidgetsApp或MaterialApp会使用指定代理创建一个 Localizations widget;
// Localization widget 可以随时从当前上下文中访问设备的地点,或者使用 Window.locale;
// 要访问本地化文件,使用Localizations.of() 方法访问提供代理的特定本地化类;
// 如需翻译,使用 intl_translation 包来取出翻译副本到 arb 文件中;把他们引入App中,并用 intl 来使用它们;(不使用 intl包也可以实现本地化)
- 添加Flutter项目依赖(PubSite)
- Android 在Gradle文件夹 添加依赖项
- iOS 在Podfile中添加;
- RN中,会在package.json中管理依赖
- Flutter使用Dart构建系统和Pub包管理器来处理依赖;
pub get
安装依赖
pubspec.yaml是Flutter 依赖项配置文件;Android 的Gradle和iOS的Podfile,只用来管理平台相关的依赖项;
视图Views
什么是View:
- 概念上类似原生的UIView,但是Widget组成的View有些不同;
- Widget具有不同的生命周期;
- Widget是不可变的,状态一旦改变,Flutter会创建一个新的Widget实例树;
- iOS的视图,在调用
setNeedsDisplay
之前都不会重绘; - Widget很轻巧,部分原因是它的不变性;因为它本身不是视图,并且不会直接绘制任何东西,而是对UI及语义的描述;
Flutter Widgets核心布局小部件,如Container Column Row Center;
如何更新Widgets:
- iOS中,直接调用UIView实例的方法来进行UI控制;
- 由于Widget不可变,Flutter会通过操纵Widget的状态来更新他们;
- 对于
StatelessWidgets
适用于描述界面不依赖对象中配置信息的部件; - 如果需要根据网络请求结果或交互之后更新UI,则必须使用
StatefulWidgets
,并通知框架Widget已更新; StatefulWidgets
具有一个State
对象,该对象存储状态数据并将其传递到树重建中;
Widget在build之外会改变的,即是有状态的;无状态组件、有状态组件可以相互包含;
import 'package:flutter/material.dart';
void main(){
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title:'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
// placeholder
String textToShow = "Flutter";
void _updateText(){
setState(() {
textToShow = 'Hello';
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('App'),
),
body: Center(child: Text(textToShow)),
floatingActionButton:FloatingActionButton(
onPressed: _updateText,
tooltip: 'Update Text',
child: Icon(Icons.update),
)
)
}
}
如何布局:
- Android 是 xml;iOS是xib约束布局;Flutter通过编写widget树来声明布局;
@override
Widget build(BuildContext context) {
return Scafford(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(
child:MaterialButton(
onPressed:() {},
child: Text('Hello'),
padding: EdgeInsets.only(left: 10.0, right: 10.0),
)
)
)
}
布局中添加或删除组件:
- Android addChild removeChild
- iOS addSubview() removeFromSuperView()
- Flutter:传入一个函数或表达式,返回值是一个给父项的Widget;并通过布尔值控制该Widget的创建;
import 'package:flutter/material.dart';
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "",
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State<SampleAppPage> {
// placeholder
bool toggle = true;
void _toggle(){
setState(() {
toggle = !toggle;
});
}
_getToggleChild() {
if (toggle) {
return Text('one');
}else {
return MaterialButton(
onPressed: () {},
child: Text('two')
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('App'),
),
body: Center(child: _getToggleChild),
floatingActionButton:FloatingActionButton(
onPressed: _toggle,
tooltip: 'Update Text',
child: Icon(Icons.update),
)
)
}
}
如何对Widget做动画:
- Android 通过XML创建动画或调用view.animate()
- iOS,通过 animateWithDuration:animations:方法给view添加动画;
- Flutter,使用动画库包裹widgets;
AnimationController:
- 这是一个可以暂定、寻找、停止、反转动画的Animation类型;
- 需要一个Ticker在vsync发生时发送信号,在每帧运行时创建一个介于0~1的线性插值(interpolation);
- 可创建多个Animation附加给一个controller;
CurvedAnimation可以实现一个interpolated
曲线:
- controller是动画的主人,而CurvedAnimation计算曲线,并替代controller默认的线性模式;
当构件widget树时,可以吧Animation指定给一个widget的动画属性,比如FadeTransition的opacity属性,并告诉控制器开始动画;
import 'package:flutter/material.dart';
void main() {
runApp(FadeAppTest());
}
class FadeAppTest entends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp {
title: 'Fade demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyFadeTest(title: 'Fade'),
}
}
}
class MyFadeTest extends StatefulWidget {
MyFadeTest({Key key, this.title}) : super(key: key);
final String title;
@override
_MyFadeTest createState() => _MyFadeTest();
}
class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
AnimationController controller;
CurveAnimation curve;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
curve = CurveAnimation(parent: controller, curve: Curves.easeIn);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Container(
// 按钮淡入动画
opacity: curve,
child: FlutterLogo(
size:100.0,
)
)
),
floatingActionButton: FloatingActionButton(
tooltip: 'Fade',
child:Icon(Icons.brush),
onPressed: () {
controller.forward();
}
)
)
}
}
如何绘图(Canvas draw/paint):
- Android Canvas与Drawable
- iOS CoreGraphics
- Flutter,提供的底层渲染引擎Skia,通过
CustomPaint
和CustomPainter
两个类帮助绘制画布;
// 画图
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(home: DemoApp()));
class DemoApp extends StatelessWidget {
Widget build(BuildContext context) => Scaffold(body: Signature());
}
class Signature extends StatefulWidget {
SignatureState createState() => SignatureState();
}
class SignatureState extends State<Signature> {
List<Offset> _points = <Offset>[];
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(() {
RenderBox referenceBox = context.findRenderobject();
Offset localPosition = referenceBox.globalToLocal(details.globalPosition);
_points = List.from(_points)..add(localPosition);
});
},
onPanEnd: (DragEndDetails details) => _points.add(null),
child: CunstomPaint(painter: SignaturePainter(_points), size: Size.infinite),
);
}
}
class SignaturePainter extends CustomPainter {
SignaturePainter(this.points);
final List<Offset> points;
void paint(Canvas canvas, Size size) {
var paint = Paint()
..color = Colors.black
..StrokeCap = StrokeCap.round
..strokeWidth = 5.0;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null)
canvas.drawLine(points[i], points[i + 1], paint);
}
}
bool shouldRepaint(SignaturePainter other) => other.points != points;
}
使用CustomPaint
widget在绘制阶段绘制:
- 实现抽象类
CustomPainter
,并将其传递给CustomPaint
的painter
属性; CustomPaint
子类必须实现paint
和shouldRepaint
方法;
// 绘制图形
import 'package:flutter/material.dart';
import 'package:flutter_app/navigator/tab_navigator.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MatreialApp(
title:'',
theme: new ThemeData.fallback(),
home:_MyCanvas(),
)
}
}
class MyCanvasPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint();
paint.color = Colors.amber;
canvas.drawCircle(Offset(100.0, 200.0), 40.0, paint);
Paint paintRect = Paint();
paintRect.color = Colors.lightBlue;
Rect rect = Rect.fromPoints(Offset(150.0,300.0), Offset(300.0,400.0));
canvas.drawRect(rect, paintRect);
}
bool shouldRepaint(MyCanvasPainter oldDelegate) => false;
bool shouldRebuildSemantics(MyCanvasPainter oldDelegate) => false;
}
class _MyCanvas extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomPaint(
painter: MyCanvasPainter(),
),
);
}
}
自定义widgets:
- Android 中通过继承View,重载方法;
- iOS通过编写UIView子类,重载方法;
- Flutter中,推荐通过组合方式自定义widget,而不是扩展它;
// 自定义一个button
class CustomButton extends StatelessWidget {
final String label;
CustomButton(this.label);
@override
Wideget build(BuildContext context) {
return new RaisedButton(
onPressed: () {},
child: new Text(label)
);
}
}
// 使用
...
new Center(
child: new CustomButton('Hellow');
)
...
设置widget属性,如透明度:
- Android,使用view的setAlpha方法;
- iOS,设置opacity或alpha属性;
- Flutter要改变透明度,可以给widget包裹一个
Opacity
widget;
Opacity (
opacity: 0.5,
child: Text('透明度50%')
)
布局与列表
Android中的LinearLayout相当于?
- Android中使用
LinearLayout
来使你的空间呈水平或垂直排列; - Flutter中,使用Row或Column widget来实现相同的结果(关于主轴 交叉轴的概念参考flex);
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Row1'),
Text('Row2'),
Text('Row3'),
Text('Row4'),
],
)
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Row1'),
Text('Row2'),
Text('Row3'),
Text('Row4'),
],
)
}
Android中的RelativeLayout相当于?
- Android 中,RelativeLayout用于使widget相对于彼此的位置排列;
- Flutter中可以通过Column、Row、Stack组合实现 RelativeLayout的效果;
- 可以为widget构造函数指定相对于父组件的布局规则;
定义布局属性:
- Flutter中,布局主要由专门用于提供布局的小部件定义,并结合空间widget及其样式属性;
Row
、Column
widget控制一个数组中条目,排列方向;Container
widget控制一个布局的样式和属性;Center
widget负责居中它的子widget;- Flutter核心widget库还提供了其他布局小部件,如
Padding
,Align
,Stack
等
Center(
child: Column(
children:<Widget>[
Container(
color: Colors.red,
width: 100.0,
height:100.0,
),
Container(
color: Colors.blue,
width: 100.0,
height:100.0,
),
Container(
color: Colors.green,
width: 100.0,
height:100.0,
),
]
)
)
分层布局:
- Flutter使用
Stack
widget控制widget在同一层;子widgets可以完全或者部分覆盖基础widgets; - Stack控件将其子项相对于其框的边缘定位;
Stack(
alignment: const Alignment(0.6,0.6),
children:<Widget>[
CircleAvatar(
backgroundImage: NetworkImage(
"https://wwwwww",
),
),
Container(
decoration: BoxDecoration(
color: Colors.black45,
),
child: Text('Flutter'),
)
]
)
设置布局样式:
- Flutter使用布局组件,Padding Center Column Row等widget;
- 组件接收用于样式布局的的构造参数;如Text widget的TextStyle属性;
var textStyle = TextStyle(fontSize: 32.0, color: Colors.cyan, fontWeight: FontWeight.w600);
...
Center(
child: Column(
children:<Widget>[
Text(
'Sample',
style: textStyle,
),
Padding(
padding: EdgeInsets.all(20.0),
child: Icon(
Icons.lightbulb_outline,
size:48.0,
color: Colors.redAccent
)
)
]
)
)
ScrollView在Flutter中等价于?
- Flutter ListView,表现可以和ScrollView基本一致;
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Text('one'),
Text('two')
]
)
}
Flutter的列表组件:
- 对比iOS的UITableView和UICollectionView,Flutter中就是ListView;
ListView(children: _getListData()),
...
_getListData(){
List<Widget> widgets = []
for (int i = 0; i < 100; i++) {
widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text("Row $i")));
}
return widgets;
}
如何动态更新ListView:
- iOS, reloadData()通知tableView刷新;
- Android,变更数据后,通过notifyDataSetChanged来更新列表;
- Flutter中,通过
setState()
更新widget列表,如果只改变了某一元素,是不会有效果的,因为ListView组件树并未发生变化;全部替换的话,则会起作用,因为ListView组件树也发生了变化;
// 列表操作示例
...
class _SampleAppPageState extends State<SampleAppPage> {
List widgets = [];
@override
void initState() {
super.initState();
for (int i = 0l i < 100; i++){
widgets.add(getRow(i));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView(children: widgets),
);
}
Widget getRow(int i) {
return GestureDetector(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row $i"),
),
onTap: () {
setState(() {
widgets = List.from(widgets);
widgets.add(getRow(widgets.length + 1));
print('row $i');
})
}
)
}
}
一个更高效的做法是:使用ListView.Builder来构建动态列表;
ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
// 和iOS的 cellForRowAtIndexPath很像
return getRow(position);
},
);
...
Widget getRow(int i) {
return GestureDetector(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row $i"),
),
onTap: () {
setState(() {
widgets.add(getRow(widgets.length + 1));
print('row $i');
})
}
)
}
状态管理
StatelessWidget:
- 不需要状态更改的widget,没有要管理的内部状态;
- 当描述的用户界面部分不依赖于对象本身的配置信息以及widget的BuildContext时,无状态widget很有用;
AboutDialog
、CircleAvator
、Text
都是StatelessWidget的子类;
// StatelessWidget:
import 'package:flutter/maretial.dart';
void main() => runApp(MyStatelessWidget(text:'haha'));
class MyStatelessWidget extends StatelessWidget {
final String text;
MyStatelessWidget({Key key, this.text}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Text(
text,
textDirection: TextDirection.ltr,
)
)
}
}
StatelessWidget的build方法被调用的三种情况:
- widget插入树中;
- widget父级更改配置;
- widget依赖的 InheritedWidget 发生变化时;
StatefulWidget:
- 可变状态widget,使用setState方法管理状态改变;
- 调用setState方法通知Flutter框架,有状态变化,Flutter会重新运行build方法;
Checkbox Radio Slider InkWell Form TextField
都是有状态widget,也是StatefulWidget的子类;- 声明一个StatefulWidget时,需要一个
createState()
方法,此方法创建管理widget状态的状态对象_MyStatefulWidgetState
,示例如下;
class MyStatefulWidget extends StatefulWidget {
final String title;
MyStatefulWidget({Key key, this.title}) : super(key: key);
@override
// 创建State对象,框架调用createState来构建widget
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
// _MyStatefulWidgetState类 实现 widget的 build()方法;
// 当状态改变时,使用新值调用setState;
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
bool showtext = true;
bool toggleState = true;
Timer t2;
void toggleBlinkState() {
setState(() {
toggleState = !toggleState;
});
var twenty = const Duration(milliseconds: 1000);
if(toggleState == false) {
t2 = Timer.periodic(twenty, (Timer t) {
toggleShowText();
});
}else {
t2.cancel();
}
}
void toggleShowText() {
setState(() {
showtext = !showtext;
})
}
@override
Widget buidl(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: <Widget>[
(showtext?(Text('waefawef')):(Container())),
Padding(
padding: EdgeInsets.only(top: 70.0),
child: RaisedButton(
onPressed: toggleBlinkState,
child: (toggleState?(Text('Blink')):(Text(Stop Blinking)))
)
)
]
)
)
)
}
}
状态管理的三种主要方式:
- 每个widget管理自己的状态
- 父widget管理widget状态(优选)
- 混合管理;
路由和导航
Android的Intent:
- Android中在Activity间切换时,会用到Intents,Flutter没有这个概念,需要的话通过整合Native来触发Intents;
- Intent在调用外部组件如相机、文件选择也有用,Flutter需要借助第三方插件实现相应功能;
Flutter实现页面跳转(导航):
- 实现切屏,可以访问路由以绘制新的Widget;
- 管理多个屏幕的两个核心概念:Route和Navigator;
- Route是屏幕或页面的抽象;
- Navigator是管理Route的Widget;可以通过push和pop Route实现页面切换;
和Android类似,可以在AndroidManifest.xml中声明Activites;在Flutter中,我们可以将具有指定Route的Map传递到顶层MaterialApp实例,但这不是必须的;
Flutter中页面导航的两种选择:
- 具体指定一个由路由名构成的Map(MaterialApp)
- 直接跳转到一个路由(WidgetApp)
void main() {
runApp(MaterialApp(
home: MyAppHome(),
router: <String, WidgetBuilder> {
'/a': (BuildContext context) => MyPage(titie: 'page A'),
'/b': (BuildContext context) => MyPage(titie: 'page B'),
'/c': (BuildContext context) => MyPage(titie: 'page C'),
},
));
}
// 使用路由名字
Navigator.of(context).pushNamed('/b');
// 使用Navigator的push方法 直接跳转
// MaterialPageRoute widget是一个模板路由 会根据平台自适应替换页面
Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) => UsualNavscreen()));
获取路由跳转返回结果:
- Navigator类可用于获取route push到栈中返回结果;
- 使用 await等待路由返回结果;
// 进入页面
Map coordinate = await navigator.of(context).pushNamed('/location');
// 退出页面
Navigator.of(context).pop({"lat":100, "long":100});
Flutter如何处理来自外部应用程序的Intents(Android):
- 通过直接与Android层通信,请求共享数据,来处理来自Android的Intents;
- 在AndroidManifest.xml中注册想要处理的Intent;
- 在MainActivity中,处理intent;
- 等待Flutter请求数据;
- 通过MethodChannel发送;
// android 部分
package com.example.shared;
import android.content.Intent;
import android.os.Bundle;
import java.nio.ByteBuffer;
import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.ActivityLifecycleListener;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.GeneratedPluginRegistrant;
public class MainActivity extends FlutterActivity {
private String sharedText;
@override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
Intent intent = getIntent();
String action = intent.getAction();
String type = intent.getType;
if (Intent.ACTION_SEND.equals(action) && type != null) {
if ("text/plain".equals(type)) {
handleSendText(intent);
}
}
new MethodChannel(getFlutterView(), "app.channel.shared.data").setMethodCallHander(
new MethodCallHandler() {
@override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
if (call.method.contentEquals("getSharedText")) {
result.success(sharedText);
sharedText = null;
}
}
}
);
}
void handleSendText(Intent intent) {
sharedText - intent.getStringExtra(Intent.EXTRA_TEXT);
}
}
// flutter
...
class _SampleAppPageState extends State<SampleAppPage> {
static const platform = const MethodChannel('app.channel.shared.data');
String dataShared = 'No data';
@override
void initState() {
super.initState();
getSharedText();
}
@override
Widget build(BuildContext context) {
ruturn Scaffold(body: Center(child: Text(dataShared)));
}
getSharedText() async {
var sharedData = await platform.invokeMethod("getSharedText");
if (sharedData != null) {
setState(() {
dataShared = sharedData;
})
}
}
}
如何跳转其他App:
- iOS中,通过特定的URL Scheme,对于系统级App,这个scheme取决于App;
- 在Flutter中实现这个功能,可以创建一个原生平台的整合层,或者使用现有的plugin(如 url_launcher);
线程和异步UI
如何编写异步代码:
- Dart有一个单线程执行模型:
- 支持Isolate(一种在另一个线程运行代码的方式)
- 一个事件循环
- 支持异步编程
- 除非自己创建一个Isolate,否则你的Dart始终运行在主UI线程,并由 event loop驱动;
- Flutter中 event loop和 iOS中 main loop相似:Looper是附加在主线程上的;
- 可以使用Dart提供的异步工具,如 async/await 实现异步操作;
loadData() async {
String dataURL = 'https://xxx';
http.Response respinse = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
})
}
如何把工作放到后台执行:
- Flutter是单线程并且跑着一个event loop,因此不必担心线程管理或生成后台线程;
- I/O操作,磁盘或网络请求 都可以安全的使用 async/await来完成;
- 如果需要让CPU执行计算密集型任务,你需要使用Isolate利用多核CPU来避免阻塞event loop;
Isolate是分离的运行线程,并且不和主线程的内存堆共享内存;
// Isolate的使用
import 'dart:isolate';
...
loadData() async {
// 打开ReceivePort以接收传入信息
ReceivePort receivePort = ReceivePort();
// 创建并生成与当前Isolate共享相同代码的Isolate
await Isolate.spawn(dataLoader, receivePort.sendPort);
// 流的第一个元素
SendPort sendPort = await receivedPort.first;
// 流的第一个元素被收到后监听会关闭,所以需要重新打开一个ReceivePort以接收传入的消息
ReceivePort response = ReceivePort();
// 通过此发送端口向其对应的“ReceivePort”发送异步消息
sendPort.send(
["https://xxxxx", response.sendPort]
);
// 获取端口送来的数据
List msg = await response.first;
setState(() {
widgets = msg;
});
}
// isolate的入口函数 该函数会在新的Isolate中调用,Isolate.spawn的message参数会作为调用它的唯一参数
static dataLoader(SendPort sendPort) async {
// 打开ReceivePort以接收传入的消息
ReceivePort port = ReceivePort();
// 通知其他的isolates, 本isolate 所监听的端口
sendPort.send(port.sendPort);
// 获取其他端口发送的异步消息 msg ->
// 等价于 List msg = await port.first;
await for (var msg in port) {
String data = msg[0];
SendPort replyTo = msg[1];
String dataURL = data;
http.Response response = await http.get(dataURL);
// 其对应的ReceivePort 发送解析出来的json数据
replyTo.send(json.decode(response.body));
}
}
上边,dataLoader()是一个运行于独立线程上的Isolate;在Isoalte里,你可以执行CPU密集型任务,如解析一个很大的json、或处理一些复杂的计算;
如何进行网络请求:
- 使用
http
package,在pubspec.yaml中添加如下依赖;
// pubspec.yaml
dependencies:
...
http: ^0.12.0+1
如何为长时间运行的任务添加一个进度指示器:
- 进度条widget,
ProgressIndicator
; - 可以通过一个布尔值flag来控制是否展示进度;在任务开始时,告诉Flutter更新状态,并在结束后隐藏;
手势检测 及 触摸事件处理
如何给Flutter widget添加一个点击事件的监听:
- 对于支持点击事件的widget可以直接传递参数函数,如
RaisedButton
的onPressed
; - 如果widget本身不支持,则需要在外包裹一个
GestureDetector
,对应的属性是onTap
;
如何处理widget上的其他手势:
- 使用
GestureDetector
可以监听多种手势;- 点击:
- onTapDown(e) 轻触手势开始
- onTapUp 轻触手势停止
- onTap
- onTapCancel 比如点击了一个按钮 但没松开 最终划出去了
- 双击:
- onDoubleTap
- 长按:
- onLongPress
- 垂直拖动:
- onVerticalDragStart
- onVerticalDragUpdate
- onVerticalDragEnd
- 水平拖动:
- onHorizontalDragStart
- onHorizontalDragUpdate
- onHorizontalDragEnd
- 点击:
主题 和 文字处理
如何在Text widget上设置自定义字体;
- iOS需要在项目中引入ttf文件,并在info.plist中设置引用;
- Android 需要创建一个Font资源并将其传递到TextView的FontFamily参数中;
- Flutter中设置字体,只需在文件夹中放置字体文件,并在pubspec.yaml中引入。就像添加图片那样;
// pubspec.yaml
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
// 使用
Text(
'hello',
style: TextStyle(fontFamily: 'MyCustomFont'),
)
如何在Text上定义样式:
- Test widget的style属性,接收一个TextStyle对象,可以指定很多参数;
color
decoration
decorationColor
decorationStyle
fontFamily
fontSize
fontStyle
fontWeight
hashCode
height
inherit
letterSpacing
textBaseline
wordSpacing
如何给App设置主题:
- 声明一个顶级组件,
MaterialApp
作为App入口;它是一个更高级的组件封装; - 也可以使用WidgetApp,它提供能了许多相似功能,但不如MaterialApp强大;
- 如果想实现iOS设计风格的界面,可以使用 Cupertino library;
通用的颜色和样式,可以通过给 MaterialApp widget传递一个 ThemeData对象;
MaterialApp(
title:'',
theme: ThemeData(
priarySwatch: Colors.blueGrey,
textSelectionColor: Colors.red
),
home: Scaffold(
appBar: AppBar(),
body: Text("hahaha"),
),
)
表单输入和富文本
如何获取用户输入:
- 使用
TextField
/TextFormField
,然后通过TextEditingController
来获得用户输入;
class _MyFormState extends State<MyForm> {
final myController = TextEditingController();
@override
void dispose() {
myController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar:...
body: Padding(
padding: conse EdageInsets.all(16.0),
child: TextField(
// 指定一个controller
controller: myController,
)
),
floatingActionButton: FloatingActionButton(
onPressed: () {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
// 通过myController.text获取输入
content: Text(myController.text),
);
},
);
},
tooltip:'',
child: Icon(Icons.text_fields),
)
)
}
}
如何设置输入框的提示文字:
- 通过Text widget的装饰构造器参数 设置一个
InputDecoration
来展示“小提示”,或是占位符文字;
body: Center(
child: TextField(
decoration: InputDecoration(hintText: 'placeholder'),
)
)
如何显示验证错误信息:
InputDecoration
除了可以添加提示信息,还可以添加错误信息提示;
String _errorText;
...
TextField(
onSubmitted: (String text) {
setState(() {
if (!isEmail(text)) {
_errorText = "Error";
}else {
_errorText = null;
}
})
},
decoration: InputDecoration(hintText: "", errorText: _getErrorText())
)
...
_getErrirText(){
return _errorText;
}
bool isEmail(String emailString) {
String emailRegexp =
r'正则';
RegExp regExp = RegExp(emailRegexp);
return regExp.hasMatch(emailString)
}
富文本展示:
- 使用
RickText
widget;
List<TextSpan> spans = []
...
RickText(
text: TextSpan(
children: spans,
),
);
硬件调用、第三方服务、平台交互、通知
如何调用硬件和第三方服务:
- 一般使用插件,一下是几款常用插件,如果插件不满足也可以自己开发
geolocator
:访问位置信息的GPS插件image_picker
:访问相机 相册Shared Preferences plugin
:本地存储,iOS的UserDefaults、Android的Shared Preferences;SQFlite
:访问数据的插件flutter_facebook_login
:唤起Facebook第三方登录firebase_messaging
:用于推送通知,在插件库中可以搜索到更多的关于firebase plugin;
如何构建和继承Native SDK/模块:
- 如果有Flutter或其他社区缺失的功能,可以利用Flutter插件架构:发出消息让接收者进行处理并将结果返回给您,在此情况下,接收者将是iOS或Android 的SDK或功能模块;
Flutter必备基础知识
构建Flutter项目
学习项目参考:
https://github.com/flutter/flutter/tree/master/examples
https://github.com/nisrulz/flutter-examples
https://github.com/iampawan/FlutterExampleApps
- flutter sdk 安装目录下的 examples文件夹;
多看项目,多参考;
图片控件开发
Image Widget:
- 加载网络图片
- 加载静态图片
- 加载本地图片
- 设置PlaceHolder
- 配置图片缓存
- 加载Icon
Image构造函数:
new Image
从ImageProvider获取图像new Image.asset
使用key从AssetsBundle获取图像new Image.network
从网络URL获取图像new Image.file
从本地文件获取图像new Image.memory
从内存获取图像
如果是多套UI资源图片,请使用new Image.asset
并通过AssetImage
指定图像;
// 网络图片
Image.network('url')
// 静态图片 并处理不同分辨率
// 1.在pubspec.yaml文件中声明图片资源路径;
// 2.使用AssetImage访问图片;或 使用Image.asset加载;
// pubspec.yaml
assets:
- images/my_icon.png
// 使用AssetImage
Image(
height:100,
width:100,
image: AssetImage("images/my_icon.png"),
)
// 使用Image.asset
Image.asset(
"images/my_icon.png",
height:100,
width:100,
)
// 加载本地图片(如手机SD卡)
import 'dart:io';
Image.file(File('/sd/Download/Stack.png'))
// 加载相对路径的本地图片
// 1.在pubspec.yaml中添加 path_provider 插件,处理路径前缀;
// 2.编写代码
import 'dart.io'
import 'package:path_provider/path_provider.dart';
// Image.file(File('/sd/Download/Stack.png'))
FutureBuilder(
future: _getLocalFile('Download/Stack.png'),
builder: (BuildContext context, AsyncSnapshot<File> snapshot) {
return snapshot.data != null ? Image.file(snapshot.data) : Container();
}
)
// 获得SDCard路径
Future<File> _getLocalFile(String filename) async {
String dir = (await getExternalStorageDictionary()).path;
File f = new File('$dir/$filename');
return f;
}
// 设置PlaceHolder:需要借助 FadeInImage widget,他能从内存,本地资源中加载PlaceHolder
// 从内存加载的话:需要安装从内存读取数据的插件 transparent_image
import 'package:transparent_image/transparent_image.dart';
...
FadeInImage.memoryNetWork(
placeholder: kTransparentImage,
image:'url'
)
// 再从本地资源中加载PlaceHolder
assets:
- assets/loading.gif
FadeInImage.assetNetwork(
placeholder: 'assets/loading.gif',
image: 'url'
)
// 配置网络图片缓存:需要借助 cached_network_image 插件,来从网络上加载图片,并将其缓存到本地
import 'package:cached_network_image/cached_network_image.dart';
CachedNetworkImage(
placeholder: CircularProgressIndcator(),
imageUrl:'url'
)
加载Icon:
- 使用
Icon
widget,其构造函数如下: - material_fonts
const Icon(
this.icon, // IconData(可以是自己构造的 也可以是 Flutter提供的 material_fonts)
{
Key key,
this.size,
this.color,
this.semanticLabel, // 标志位
this.textDirection // 绘制方向
}
)
// 使用内置图标
new Icon(Icon.adnroid, size: 100.0)
// 使用自定义Icon:需要构造一个IconData
const IconData(
this.codePoint, // 必填参数 fonticon对应的16进制 Unicode
{
this.fontFamily, // 字体库系列
this.fontPackage, // 字体所在包 不填仅在程序包中查找
this.matchTextDirection: false, // 是否按照绘制方向显示
}
)
// 首选 类似字体的使用 配置先配置icon
fonts:
- family: devio
fonts:
- asset: fonts/devio.ttf
// 其次 是使用
new Icon(
new IconData(0xf5566, fontFamily: "devio"),
size: 100.0,
color: Colors.blue
)
动画Animation
- Flutter中有哪些类型的动画
- 如何使用动画库的基础类给widget添加动画
- 如何为动画添加监听器
- 什么时候使用AnimatedWidget与AnimatedBuilder
- 如何使用Hero动画;
Flutter中有哪些类型的动画:
- 两类:基于tween 和 基于物理的;
- tween动画:定义开始和结束点,时间线 以及 定义转换时间和速度的曲线,再由框架计算如何从开始点过渡到结束点;
- 基于物理的动画:模拟真实物体行为;
如何使用动画库的基础类给widget添加动画:
- Animation:核心库,生成指导动画的值;
- CurvedAnimation:Animation的子类,将过程抽象为一个非线性曲线;
- AnimationController:Animation的子类,用来管理Animation;
- Tween:在正在执行动画的对象所使用的数值范围之间生成值;
Animation:
- Animation本身与UI渲染无关;他是个抽象类,比较常用的是
Animation<double>
; - Animation对象是在一段时间内依次生成一个区间之间值的类;其输出可以是线性的、曲线的、一个步进函数或 任何其他可设计的映射;
- 设置Animation控制方式,动画可以反向运行,甚至在中间切换方向;
- Animation可以生成
Animation<double>
外的其他类型,如Animation<Color>
或Animation<Size>
; - Animation对象是有状态的,可以通过访问其value属性获取动画的当前值;
CurvedAnimation:
final CurvedAnimation: curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
// Curves.easeIn是已经定义好的,也可以自行创建
class ShakeCurve extends Curve {
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
AnimationController:
- 特殊的Animation对象,屏幕刷新一帧就会生成一个新值;
- 默认情况,AnimationController在给定时间内会线性的生成从0.0~1.0的数字;
// vsync参数 会防止widget显示区域外动画消耗不必要的资源,可以将stateful对象作为vsync的值;
final AnimationController controller = new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
- AnimationController派生自
Animation<double>
,可以在需要Animation对象的地方使用; - AnimationController具有控制动画的其他方法:
forward()
启动动画reverse({double from})
倒放动画reset()
重置动画stop({bool canceled = true})
停止动画
Tween:
- AnimationController对象的范围是从0~1(可能会超出);如果需要不同的范围或不同的数据类型,这可以使用Tween(不同范围、不同数据类型);
- Tween是无状态的,需要begin和end值;它的唯一职责就是定义从输入范围到输出范围的映射;
- Tween继承自
Animatable<T>
,Animatable输出的比一定是double值;
final Tween doubleTween = new Tween<double>(begin:-200.0, end: 0.0);
final Tween colorTween = new ColorTween(begin: Colors.transparent, end: Colors.black54);
- Tween对象不存储任何状态;它提供了
evaluate(Animation<double> animation)
方法将映射函数应用于动画的当前值;Animation对象的当前值可以通过value()
方法取到;
Tween.animate:
- 可调用Tween对象的
animate()
方法,传入一个控制器对象;该方法返回的是一个Animation对象;
final AnimationController controller = new AnimationController(duration: const Duration(millisecounds: 500), vsync: this);
final Animation curve = new CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = new IntTween(begin:0, end:255).animate(curve);
为widget添加动画:
// 图片放大
import 'package:flutter/animation.dart';
import 'packaget:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTockerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
AnimationStatus animationState;
double animationValue;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)..addListener(() {
setState((){
// setState方法必须存在触发页面重新渲染 动画在能有效
animationValue = animation.value;
});
})..addStatusListener((AnimationStatus state) {
setState(() {
animationState = state;
});
})
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(top: 50),
child: Column(
children: <Widget>[
GestrueDetector(
onTap:(){
controller.reset();
controller.forward();
},
child: Text('Start', textDirection: TextDirection.ltr),
),
Text('State:' + animationState.toString(), textDirection: TextDirection.ltr),
Text('Value:' + animationValue.toString(), textDirection: TextDirection.ltr),
Container(
height: animation.value,
width: animation.value,
child: FlutterLogo(),
)
]
)
)
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
// 除了上述实现 也可以使用 AnimatedWidget来实现
为动画添加监听器:
- 需要知道动画指定进度和状态,通过Animation的
addListener
与addStatusListener
方法添加监听器;addListener
动画的值变化addStatusListener
动画状态发生变化
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse()
}else if (status == AnimationStatus.dismissed) {
controller.formard();
}
})
..addListener(() {
setState(() {
})
};
controller.forward();
使用AnimatedWidget与AnimatedBuilder简化和重构对动画的使用:
class AnimatedLogo extends AnimatedWidget {
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
// 动画值变化 就会调用build
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return new Center(
child: new Container(
margin: new EdgeInsets.symmetric(vertical: 10.0),
height: animation.value,
width: animation.value,
child: new FlutterLogon(),
)
)
}
}
class LoginApp extends StatefulWidget {
_LogoAppState createState() => new _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTockerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this
);
animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
controller.forward();
}
Widget build(BuildContext context) {
return new AnimatedLogo(animation: animation);
}
dispose() {
controller.dispose();
super.dispose();
}
}
void main(){
runApp(new LogoApp());
}
AnimatedBuilder:
- 用于构建动画的通用widget,AnimatedBuilder:对于希望将动画作为更大构建函数的一部分包含在更负载的widget时很有用;
- 可以理解为:AnimatedBuilder是拆分动画的一个工具类,将动画和widget分离;
AnimatedBuilder(
animation: animation,
builder: (context, child) => Container(
height: animation.value,
width: animation.value,
child: child,
)
child: child
)
Hero动画:
- 通过两个页面都有的元素,如图片(hero),实现页面到页面之间的过渡(淡入淡出);
import 'package:flutter/material.dart';
import 'package:flutter/schedule.dart' show timeDilation;
class PhotoHero extends StatelessWidget {
const PhotoHero({Key key, this.photo, this.onTap, this.width }) : super(key: key);
final String photo;
final VoidCallBack onTap;
final double width;
Widget build(BuildContext context) {
return SizeBox(
width: width,
child: Hero(
tag: photo,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Image.network(
photo,
fit: BoxFit.contain,
)
)
)
)
)
}
}
class HeroAnimation extends StatelessWidget {
Widget build(BuildContext context) {
// speed
timeDilation = 10.0;
return Scaffold(
appBar:...
body: Center(
child: PhotoHero(
photo: 'url',
width: 300.0,
opTap: () {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return Scaffold(
appBar...
body: Container(
color: Colors.lightBlueAccent,
padding: const EdgeInsets.all(16.0),
alignment: Alignment.topLeft,
child: PhotoHero(
photo: 'url',
width: 100.0,
opTap: () {
Navigator.of(context).pop();
}
)
)
)
}
))
}
)
)
)
}
}
void main() {
runApp(MaterialApp(home: HeroAnimation()));
}
Hero原型参数:
tag
关联两个Hero动画的标识;createRectTween
(可选)定义Hero的边界,从开始到结束 过渡过程中该如何变化;child
定义动画所呈现的widget;
实现径向hero动画:
- 原型变方形,方形变原型;
- 利用
createRectTween
;
Flutter调试技巧
Debug模式运行:
- 断点调试
- Variables视窗、Watches视窗:变量列表
- Frames视窗:函数调用栈
- Flutter控制台 console 打印输出
各自平台的log信息使用相应的IDE打开app调试即可;
Flutter基础实践
注:以对widget的介绍和使用为主要内容,不做长篇幅代码段的整理;
App导航框架 与 常用功能实现
首页Tab导航框架:
- Scaffold:BottomNavigationBar
- PageView:PageController
Scaffold是一个实现了基本的material design的布局结构:
class Scaffold extends StatefulWidget {
const Scaffold({
Key key,
thos.appBar,
this.body,
this.floatingActionButton,
this.floatingActionButtonLocation,
this.floatingActionButtonAnimator,
this.persistentFooterButtons,
this.drawer,
this.endDrawer,
this.bottomNavigationBar,
this.bottomSheet,
this.backgroundColor,
this.resizeToAvoidBottomPadding = true,
this.primary = true,
}) : assert(primaru != null), super(key:key);
}
AppBar、TabBarView、BottomNavigationBar、AppBar、TabBarView、BottomNavigationBarItem、DrawHeader;
PageView是一个可以完成页面之间滚动的widget:
class PageView entends StatefulWidget {
PageView({
Key key,
this.scrollDorection = Axis.horizontal,
this.reverse = false,
PageController controller,//pageView的控制类
this.physics,
this.pageSnapping = true,
this.onPageChanged,
List<Widget> children = const <Widget>[],
}): controller = controller ?? _defaultPageController,
childrenDelegate = SliverChildListDelegate(children),
super(key :key);
}
Banner可以使用轮播组件:flutter_swiper
Container(
height:160,
child: Swiper(
itemCount: _imageUrls.length,
autoplay: true,
itemBuilder: (BuildContext context, int index) {
return Image.network(
_imageUrls[index],
fit: BoxFit.fill,
);
},
pagination: SwiperPageination(),//指示器
),
)
去掉顶部非安全区的留白:
MediaQuery.removePadding(removeTop: true, context: context, child: ListView...)
监听ListView列表滚动:
_onScroll(offset){
print(offset);
}
NotificationListener(onNotification:(scrollNotification){
if (scrollNotification is ScrollUpdateNotofication &&
scrollNotification.depth == 0 //仅第0个组件ListView被监听(因为可能会有更深一层的ListView滑动被监听 所以这里要进行过滤)
){
// 调用自定义函数
_onScroll(scrollNotification.metrics.pixels);
}
}, child:ListView )
在ListView上放一个自定义的appBar,实现ListView向上滚动时隐藏appBar:
- 堆叠组件
Stack
;
Stack(
children: <Widget>[
MediaQuery.removePadding(
removeTop: true,
context: context,
child: NotificationListener(
if...
),
child: ListView...
),
Opacity(
opacity: 1,// 根据监听值进行设置
child: Container(
height: 80,
decoration: BoxDecoration(color: Colors.white),
child: Center(
child: Padding(padding: EdgeInset.only(top: 20),
child: Text('首页'),
)
)
)
)
]
)
Flutter进阶提升:网络编程与数据存储技术
HTTP:
- 引入和安装http插件;
Future、FutureBuilder:
- Future是与异步操作一起工作的核心Dart类;用于表示未来某个时间可能会出现的可用值或错误;(类似于ES6的Promise,提供then和catchError的链式调用);
- Future是
dart:async
包的一个类,使用它时需要导入包; - Future有两种状态:
- pending 执行中
- completed 执行成功/失败
Future用法:
- future.then
- async await
- future.whenComplete
- future.timeout
// 示例1:捕获异常
import 'dart:async';
Future<String> testFuture() {
return Future.value('success');
// or
return Future.error('error');
}
testFuture().then((s){
}, onError: (e){
}).catchError((e){
// catchError与onError 同时存在时 只会调用onError
}).whenComplete(() {
print('done')
});
// 示例2:异步调用
import 'dart:async';
test() async {
int result = await Future.delayed(Duration(milliseconds:2000), () {
return 123;
})
print('t3' + DateTime.now().toString());
}
print('t1' + DateTime.now().toString());
test()
print('t1' + DateTime.now().toString());
// 示例3:设置超时时间
import 'dart:async';
Future.delayed(Duration(milliseconds:2000), () {
return Future.value(1);
}).timeout(new Duration(seconds: 5)).then((s){
}).catchError((e){
});
FutureBuilder:
- 是一个将异步操作和异步UI更新结合在一起的类;
- 在创建FutureBuilder对象时,将Future对象作为要处理的异步计算模式传递;在builder函数中,检查connectionState的值,并使用 AsyncSnapshot中的数据或错误返回不同的窗口小部件;
构造方法解析:
- future:future对象表示FutureBuilder构造器当前连接的异步;
- initialData:表示一个非空的Future完成前的初始化数据;
- builder:AsyncWidgetBuilder类型的回调函数,是一个基于异步交互构建widget的函数;
- builder函数接收两个参数:
BuildContext context
、AsyncSnapshot<T> snapshot
,返回一个widget; - snapshot包含异步计算信息,具有以下属性:
- connectionState:枚举值,表示异步计算的连接状态,四个值分别为
none waiting active done
; - data:异步计算接收的最新数据
- error:异步计算接收的最新错误对象;
- hasData和hasError:分别检查它是否包含非空数据值或错误值;
- connectionState:枚举值,表示异步计算的连接状态,四个值分别为
- builder函数接收两个参数:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
...
class _MyAppState extends State<MyApp> {
String showResult = '';
Future<CommonModel> fetchPost() async {
final response = await http.get('url');
Utf8Decoder utf8decoder = Utf8Decoder();
var result = json.decode(utf8Decoder.convert(response.bodyBytes));
return CommonModel.fromJson(result);
}
@override
Widget build(BuildContext context) {
...
body: FutureBuilder<CommonModel>(
future: fetchPost(),
builder: (BuildContext context, AsyncSnapshot<CommonModel> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text('Input an URL to start');
case ConnectionState.waiting:
return new Center(child: new CircularProgressIndeicator());
case ConnectionState.active:
return new Text('');
case ContextionState.done:
if (snapshot.hasError) {
return new Text(
'${shapshot.error}',
style: TextStyle(color: Colors.red),
);
}else {
return new Column(children: <Widget>[
Text('Icon: ${snapshot.data.icon}'),
Text('Title: ${snapshot.data.title}')
])
}
}
}
)
...
}
}
将Response转换为Dart object:
// Model类 允许通过json创建Model对象
class CommonModel {
final String icon;
final String title;
final String url;
final String statusBarColor;
final bool hideAppBar;
CommonModel({
this.icon,
this.title,
this,url,
this.statusBarColor,
this.hideAppBar
});
// 工厂构造函数
factory CommonModel.fromJson(Map<String, dynamic> json) {
return CommonModel(
icon: json['icon'],
title: json['title'],
url: json['url'],
statusBarColor: json['statusBarColor'],
hideAppBar: json['hideAppBar'],
)
}
}
JSON解析和模型转换:
- 使用
dart:convert
package将Future<Post>
响应的内容转化为一个json Map; - 再使用上面工厂构造函数将json Map转化为一个CommonModel对象;
Future<CommonModel> fetchPost() async {
final response = await http.get('');
final result = json.decode(response.body);
return new CommonModel.fromJson(result);
}
fetchPost().then(
(CommonModel value) {
setState(
() {
resault = value;
}
)
}
)
JSON序列化方式:
- 小型项目:手动序列化;(更通用)
- 大型项目:借助插件
json_serializable
和built_value
简单json解析:
String jsonStr = '{...}';
Map<String, dynamic> map = JSON.decode(jsonStr);
复杂json解析:
// 嵌套结构的json解析
class TravelTabModel {
final String url;
final List<TravelTab> tabs;
TravelTabModel({this.url, this.tabs});
factory TravelTabModel.fromJson(Map<String, dynamic> json) {
String url = json['url'];
List<TravelTab> tabs = (json['tabs'] as List).map((i)=> TravelTab.fromJson(i));
return TravelTabModel(url:url, tabs:tabs);
}
}
class TravelTab {
// 也可以修改为final修饰的方式(参上)
String labelName;
String groupChannelCode;
TravelTab({this.labelName, this.groupChannelCode});
TravelTab.fromJson(Map<String, dynamic> json) {
labelName = json['labelName'];
groupChannelCode = json['groupChannelCode'];
}
}
也可以通过在线的json转dart工具进行代码转换;
shared_preferences本地存储:
shared_preferences
本地存储插件;- 简单 异步 持久化的key-value存储,iOS是基于NSUserDefault的;
- 在pubspec.yaml添加依赖:
dependencies:shared_preferences: ^0.5.1+1
- 执行命令安装:
flutter packages get
- 需要时导入包:
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shared_preferences/shared_preferences.dart';
final prefs = await SharedPreferences.getInstance();
prefs.setInt('counter',counter);
final counter = prefs.getInt('counter') ?? 0;
prefs.remove('counter');
shared_preferences常用API:
- setString
- setBool
- setDouble
- setInt
- setStringList
- get (获取的是动态类型dynamic)
- getBool
- getDouble
- getInt
- getKeys
- getString
- getStringList
列表组件
ListView相关知识点:
- ListView、ListView点击事件、ListView动态更新、ListView.Builder构建列表;
ListView水平滚动和垂直滚动:
body: ListView(
// scrollDirection: Axis.horizontal, //默认为纵向可滑动 主轴设置为水平轴 则横向可滑动
children: _buildList()
)
List<Widget> _buildList() {
return CITY_NAMES.map((city)=> _item(city)).toList();
}
Widget _item(String city) {
return Container(
height:80,
margin: EdgeInsets.only(bottom: 5),
alignment: Alignment.center,
decoration: BoxDecoration(color: Color.teal),
child: Text(
city,
style: TextStyle(color: Colors.white, fontSize: 20),
),
)
}
ExpansionTile实现可展开列表:
// 构造函数
const ExpansionTile({
Key key,
this.leading,// 标题左侧 要展示的widget
@required this.title,// 标题widget
this.backgroundColor,// 北京色
this.onExpansionChanged,//列表 展开收起的回调函数
this.children = const <Widget>[],//列表展开时展示的widgets
this.trailing,//标题右侧需要展示的widget
this.initiallyExpanded = false,//是否默认展开
})
// 示例
List<Widget> _buildList() {
List<Widget> widgets = [];
CITY_NAMES.keys.forEach((key) {
widgets.add(_item(key, CITY_NAMES[key]));
});
return widgets;
}
Widget _item(String city, List<String> subCities) {
return ExpansitonTitle(
title: Text(
city,
style: TextStyle(color: Color.black54, fontSize:20),
),
children: subCities.map((subCity)=>_buildSub(subCity)).toList(),
)
}
Widget _buildSub(String subCity) {
// 可伸缩的box 是为了让宽度撑满
return FractionallySizedBox(
widthFactor: 1,
child: Container(
height: 50,
margin: EdgeInsets.only(bottom: 5),
decoration: BoxDecoration(color: Color.lightBlueAccent),
child: Text(subCity),
)
)
}
GridView实现网格列表:
- 通常使用GridView.count构造函数来创建一个GridView;
body: GridView.count (
crossAxisCount: 2,//设置显示多少列
children: _buildList(),
)
...
下拉刷新和上拉加载:
RefreshIndicator
:下拉竖线widget;
body: RefreshIndicator(
onRefresh: _handleRefresh,
child: ListView(...)
)
Future<Null> _handleRefresh() async {
await Future.delayed(Duration(seconds:2));
setState((){
cityNames = cityNames.reversed.toList();
});
return null;
}
- 上拉加载需要借助
ScrollController
,列表支持设置controller参数,通过ScrollController监听列表滚动的位置,来实现加载更多的功能;
class _MyAppState extends State<MyApp> {
ScrollController _scrollController = ScrollController();
@override
void initState() {
_scrollController.addListener((){
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent){
_loadData();
}
});
super.initState();
}
@override
void dispise() {
_scrollController.dispose();
super.dispose();
}
@override
Widget buid(BuildContext context) {
...
body: RefreshIndicator(
onRefresh: _handleRefresh,
child: ListView(
controller: _scrollController,
children: _buildList()
)
)
...
}
}
_loadData() async {
await Future.delayed(Duration(milliseconds: 200));
setState((){
// copy
List<String> list = List<String>.from(cityNames);
// 继续追加
list.addAll(cityNames);
cityNames = list;
})
}
...
Flutter进阶提升: Flutter、Native混合开发
混合方式:
- 页面间独立:一个页面时原生的、一个页面时Flutter的;
- 页面内嵌入:原生页面嵌入flutter组件,或flutter页面嵌入原生View;
集成步骤:
- 创建Flutter module(Flutter module会有用于调试的宿主iOS、Android工程)
- 在原生中添加Flutter module依赖
- 在原生中调用Flutter module
- 编写Dart代码;(lib目录)
- 运行项目
- 热重启、热加载
- 结合原生混合开发的Flutter项目需要开启热加载、热重启时,首先需要打开一个模拟器或链接一台设备,然后关闭App,在flutter_module根目录运行命令
flutter attach <模拟器ID>
; - 运行项目,使用Flutter开发的模块,使用r或R即可完成热重启和热加载;
- 结合原生混合开发的Flutter项目需要开启热加载、热重启时,首先需要打开一个模拟器或链接一台设备,然后关闭App,在flutter_module根目录运行命令
- 调试Dart(混合开发的项目)
- AndroidStudio,关闭App,点击原调试按钮旁的
Flutter attach
按钮,开启调试dart代码;
- AndroidStudio,关闭App,点击原调试按钮旁的
- 发布应用
创建Flutter module:
// 假设Native项目如:xxx/flutter_hybrid/Native项目
cd xxx/flutter_hybrid/
flutter create -t module flutter_module
// 这会切换到Android/iOS的上一级目录 并创建一个flutter模块
// flutter_module中文件:
// .android .ios分别为flutter_module的宿主工程
// lib 是 flutter_module的Dart部分代码
// pubspec.yaml 是flutter_module项目依赖配置文件
// flutter_module在不增加额外配置时,是可以独立运行的;
与ios的混合开发:
- 为已经存在的iOS应用添加Flutter module依赖;
- 需要配置CocoaPods;
- 初始化Podfile:
pod init
- 然后添加依赖,内容如下;
- 初始化Podfile:
target 'FlutterHybridiOS' do
flutter_application_path='../flutter_module/'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
target 'FlutterHybridiOSTests' do
inherit! :search_paths
end
target 'FlutterHybridiOSUITests' do
inherit! :search_paths
end
end
- 在iOS项目的根目录运行:
pod install
,安装依赖; - 如果在FLutter中引入其他插件,也需要重新运行一次
pop install
以在iOS项目中刷新依赖项; - 关闭Bitcode;
- 添加build phase以构建Dart代码:
- Targets->New Run Script Phase;
- 创建一个 build phase,Run Script,添加如下内容;
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" buiObject-cld
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
- 将
Run Script
移动到紧挨着Target Dependencies phase
下面;之后就可以通过commend+B构建项目了;
在OC中调用Flutter module
- 方式1:直接使用FlutterViewController
- 方式2:使用FlutterEngine
// 使用FlutterViewController
#import <Flutter/Flutter.h>
#import "AppDelegate.h"
#import "ViewController.h"
#import <FlutterpluginRegistrant/GeneratedPluginRegistrant.h>
FlutterViewController * flutterViewController = [FlutterViewController new];
// 如果我们创建的FlutterViewController 中使用了flutter插件 则还需要注册插件管理器
GeneratedPluginRegistrant.register(with: flutterViewController);
// 这里传递的字符串 可以是一个json结构 主要用于传参
[flutterViewController setInitialRoute:@"route1"];
[self presentViewController:flutterViewController animated:true completion:nil];
import 'dart:ui'
import 'package:flutter/material.dart';
// window.defaultRouteName 用于接收 flutter初始化的参数
void main() => runApp(_widgetForRoute(window.defaultRouteName));
Widget _widgetForRoute(String route) {
switch (route) {
case 'route1':
return SomeWidget(...);
case 'route2':
return SomeWidget(...);
default:
return Center(
child: Text('Unknown route: $route', textDirection: TextDirection.ltr);
)
}
}
// 使用FlutterEngine
// AppDelegate.h
#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>
@interface AppDelegate: FlutterAppDelegate
@property (nonatomic, strong) FlutterEngine * flutterEngine;
@end
// AppDelegate.m
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h>
#import "AppDelegate.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
[self.flutterEngine runWithEntrypoint:nil];
[GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
return ...
}
// 使用
FlutterEngine * flutterEngine = [(AppDelegate *)[UIApplication sharedApplication] delegate] flutterEngine];
FlutterViewController * flutterViewController = [[FlutterViewController alloc] initWithEngine:flutterEngine];
// native在初始化flutter模块时 传递参数
// 但是基于FlutterEngine方式的 Route参数传递 会失效(bug)
[flutterViewController setInitialRoute:@"route1"];
[self presentViewController:flutterViewController animated:true completion:nil];
单独运行Flutter module编写Dart部分代码即可;
Flutter与Native通信
场景:
- 如flutter调用原生的某些功能,如相册或其他复杂计算;
方式:
- 原生向Flutter传递初始化参数
- 原生主动向Flutter传递参数
- Flutter主动向Native传递参数
- Flutter与Native双向传递参数
通信机制:channel
- 支持数据类型如下;
Dart | Android | iOS |
---|---|---|
null | null | nil |
bool | Boolean | NSNumber numberWithBool |
int | Integer | NSNumber numberWithInt |
int(64) | Long | NSNumber numberWithLong |
double | Double | NSNumber numberWithDouble |
String | String | NSString |
Uint8List | byte[] | FlutterStandardTypedData typedDataWithBytes |
Int32List | int[] | FlutterStandardTypedData typedDataWithInt32 |
Int64List | long[] | FlutterStandardTypedData typedDataWithInt64 |
Float64List | double[] | FlutterStandardTypedData typedDataWithFloat64 |
List | ArrayList | NSArray |
Map | HashMap | NSDictionary |
channel类型:
- BasicMessageChannel:
- 用于传递字符串和半结构化信息
- 持续通信,收到消息后可回复此消息
- 双向通信
- MethodChannel
- 用于传递方法调用(method invocation)
- 一次性通信,如调用拍照
- Dart调用Native方法
- EventChannel
- 用于数据流通信(event streams)
- 持续通信
- 但是无法回复;
- 通常用于Native向Dart的通信;如手机电量、各种传感器数据等;
信息的发起方 可以是Dart,也可以是Native;
Dart端:BasicMessageChannel:
// 构造方法
const BasicMessageChannel(this.name, this.codec);
// String name 指Channel的名字 要和Native保持一致
// MessageCodec<T> codec 消息的编解码器,要和Native端保持一致,有四种类型的实现;
void setMessageHandler(Future<T> handler(T message))
// 注册处理器,用于接收Native端发来的消息
Future<T> send(T message)
// Dart用于向Native发送消息的方法,返回的Future<T>是消息发出后收到的Native回复的回调函数;
// 使用
import 'package:flutter/services.dart';
...
static const BasicMessageChannel _basicMessageChannel = const BasicMessageChannel('BasicMessageChannelPlugin', StringCodec());
...
_basicMessageChannel.setMessageHandler((String message) => Future<String>((){
setState((){
showMessage = message;
});
// 返回值是 回复给Native的
return "收到Native消息:" + message;
}));
String response;
try{
response = await _basicMessageChannel.send(value);
}on PlatformException catch(e){
print(e);
}
...
Dart端:MethodChannel
// 构造方法
const MethodChannel(this.name, [this.codec = const StandardMethodCodec()])
// String name Channel的名字,要和Native端保持一致
// MethodCodec codec 消息的编解码器 默认是StandardMethodCodec,要和Native端保持一致;
Future<T> invokeMethod<T>(String method, [dynamic arguments])
// String method 要调用Native的方法名;
// [dynamic arguments] 调用Native方法传递的参数 可不传
// 使用
import 'package:flutter/services.dart';
...
static const MethodChannel _methodChannelPlugin = const MethodChannel('MethodChannelPlugin');
String response;
try{
// Dart调用 原生的send方法 传参是value Native返回值对象为response;
response = await _methodChannelPlugin.invokeMethod('send', value);
}on PlatformException catch(e) {
print(e);
}
...
Dart端:EventChannel
// 构造方法
const EventChannel(this.name, [this.codec = const StandardMethodCodec()])
// String name Channel的名字,要和Native端保持一致
// MethodCodec codec 消息的编解码器 默认是StandardMethodCodec,要和Native端保持一致;
Stream<dynamic> receiveBroadcastStream([ dynamic arguments])
// dynamic arguments 初始化广播流时 向Native传递的数据
// 返回的Stream对象 还需要调用listen方法来完成注册 监听回调用于接收Native端传过来的消息
// 页面销毁时还需要调用Stream的cancel方法来取消监听;
// 使用
import 'package:flutter/services.dart';
...
static const EventChannel _eventChannelPlugin = EventChannel('EventChannelPlugin');
StreamSubscription _streamSubscription;
@override
void initState(){
_streamSubscription = _eventChannelPlugin.receiveBroadcastStream().listen(_onToDart, onError: _onToDartError);
super.initState();
}
@override
void dispose() {
if (_streamSubscription != null){
_streamSubscription.cancel();
_streamSubscription = null;
}
super.dispose();
}
void _onToDart(message) {
setState((){
showMessage = message
})
}
void _onToDartError(error){
print(error);
}
...
与android通信开发指南:
- BasicMessageChannel
- MethodChannel
- EventChannel
Android:BasicMessageChannel
BasicMessageChannel(BinaryMessenger messenger, String name, MessageCodec<T> codec)
// BinaryMessenger messenger 消息信使 是消息的发送和接收的工具,传递flutterView实例
// name Channel名字 也是唯一标识符
// MessageCodec<T> codec 消息的编解码器 有四种类型的实现
// BinaryCodec:最简单的一种 返回值和入参均为二进制(Android中为ByteBuffer,iOS中为NSData);使用BinaryCodec可以使传递内存数据块时在编解码阶段免于内存拷贝;
// StringCodec:用于字符串与二进制之间的编解码,其编码格式为UTF-8;
// JSONMessageCodec:用于基础数据与二进制数据之间的编解码,支持基础数据类型如列表、字典等,在iOS端使用JSONSerialization作为序列化的工具,而在Android端使用了其自定义的JSONUtil与StringCodec作为序列化工具;
// StandardMessageCodec:默认解码器,支持基础数据类型、二进制数据、列表、字典;
void setMessageHandle(BasicMessageChannel.MessageHandler<T> handler)
// 在接收Dart端传递的消息时 需要为android端设置一个handler 消息处理器
// BasicMessageChannel.MessageHandler<T>的原型如下
public interface MessageHandler<T> {
void onMessage(T var1, BasicMessageChannel.Reply<T> var2);
// var1 指接收的消息内容
// var2 是回复此消息的回调函数
}
// Android向Dart端发送消息
void send(T message)
void send(T message, BasicMessageChannel.Reply<T> callback)
// message指传递给Dart的具体消息
// callback是受到Dart回复的回调函数;
Android:MethodChannel
MethodChannel(BinaryMessenger messenger, String name, MethodCodec codec)
setMethodCallHander(@Nullable MethodChannel.MethodCallHandler handler)
public interface MethodCallHandler {
void onMethodCall(MethodCall var1, MethodChannel.Result var2);
// var1指消息内容 MethodCall有两个属性 var1.method表示方法名,call.arguments表示方法入参;
// var2即result是回复此消息的回调函数对象,提供了 result.success、result.error、result.noImplemented方法调用;
}
Android:EventChannel
EventChannel(BinaryMessenger messenger, String name, MethodCodec codec)
void setStreamHandler(EventChannel.StreamHandler handler)
public interface StreamHandler {
void onListen(Object args, EventChannel.EventSink eventSink);
// args是传递的参数
// eventSink是回调Dart的 提供 success error endOfStream三个回调方法对应事件不同状态;
void onCancel(Object o);
// Flutter取消监听时调用
}
与ios通信开发指南:
- BasicMessageChannel
- MethodChannel
- EventChannel
iOS:BasicMessageChannel
+ (instancetype)messageChannelWithName:(NSSring*)name binaryMessenger:(NSObject<FlutterBinaryMessage>*)messenger codec:(NSObject<FlutterMessageCodec>*)codec;
// FlutterMessageCodec:
// BinaryCodec
// FlutterStringCodec
// FlutterJSONMessageCodec
// FlutterBinaryCodec 默认的
- (void)setMessageHandler:(FlutterMessageHandler _Nullable)handler;
// FlutterMessageHandler 消息处理器 处理Dart发送来的消息
// FlutterMessageHandler
typedef void (^FlutterMessageHandler)(id _Nullable message, FlutterReply callback);
// message 是接收到的消息内容
// reply是回复此消息的回调函数
// 向Dart端发送消息
- (void)sendMessage:(id _Nullable)message;
- (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback;
// 使用
// 接收Dart消息
- (void)initMessageChannel{
self.messageChannel = [FlutterBasicMessageChannel messageChannelWithName:@"BasicMessageChannelPlugin" binaryMessenger:self.flutterViewController codec:[FlutterStringCodec sharedInstance]];
MainViewController * __weak weakSelf = self;
[self.messageChannel setMessageHandler:^(NSString* message, FlutterReply reply){
// 向Dart回复
reply([NSString stringWithFormat:@"BasicMessageChannel收到:%@", message]);
// Native展示message
[weakSelf sendShow:message];
}];
}
// 向Dart发送消息
[self.messageChannel sendMessage:mesage reply:^(id _Nullable reply) {
if (reply != nil) {
[self sendShow:reply];
}
}]
iOS:MethodChannel
+ (instancetype)methodChannelWithName:(NSString *)name binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;
+ (instancetype)methodChannelWithName:(NSString *)name binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger codec:(NSObject<FlutterMethodCode>*)codec;
// Dart端调用iOS端的方法
- (void)setMethodCallHandler:(FlutterMethodCallHanlder _Nullable)handler;
// 使用
- (void)initMethodChannel {
self.methodChannel = [FlutterMethodChannel methodChannelWithName:@"MethodChannemPlugin" binaryMessenger:self.flutterViewController];
MainViewController * __weak weakself = self;
[self.methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result){
if([@"send" isEqualToString:call.method]) {
result([NSString stringWithFormat:@"MethodChannemPlugin收到:%@", call.arguments]);
[weakSelf sendShow: call.arguments];
}
}]
}
iOS:EventChannel
+ (instancetype)eventChannelWithName:(NSString*)name binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;
+ (instancetype)eventChannelWithName:(NSString*)name binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger codec:(NSObject<FlutterMethodCodec>*)codec;
- (void)setStreamHandler:(NSObject<FlutterStreamHandler>* _Nullable)handler;
@protocol FlutterStreamHandler
- (FlutterError * _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(FlutterEventSink *)eventSink;
- (FlutterError * _Nullable)onCancelWithArguments:(id _Nullable)arguments ;
// eventSink提供了 success error endOfStream回调方法对应事件的不同状态
// 使用
#import <Foundation/Foundation.h>
#import "MainViewController.h"
@interface MainViewController ()<FlutterStreamHandler>
@property (nonatomic) FlutterViewController * flutterViewController;
@property (nonatomic) FlutterEventChannel * eventChannel;
@property (nonatomic) FlutterEventSink eventSink;
@end
@implementation MainViewController
#pragma mark - sendMessage
- (void)sendMessage:(NSNotification*)notification {
NSString * message = [notification.object valueForKey:@"message"];
// 用EventChannel传递数据
if (self.eventSink != nil) {
self.eventSink(message);
}
}
#pragma mark -navigator
- (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender {
if ([segue.destinationViewController isKindOfClass:[FlutterViewController class]]){
self.flutterViewController = segue.destinationViewController;
[self.flutterViewController setInitialRoute:self.inputParams];
[self initChannel];
}
}
#pragma mark - init Channel
- (void)initChannel {
[self initEventChannel];
}
- (void)initEventChannel {
self.eventChannel = [FlutterEventChannel eventChannelWithName:@"EventChannelPlugin" binaryMessenger:self.flutterViewController];
// 设置消息处理器 处理来自Dart的消息
[self.eventChannel setStreamHandler:self];
}
#pragma mark - <FlutterStreamHandler>
// 这个onListen是Flutter端开始监听这个channel时的回调,第二个参数EventSink是用来传输局的载体
- (FlutterError * _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(FlutterEventSink *)eventSink{
self.eventSink = eventSink;
return nil;
}
// flutter不再接收
- (FlutterError * _Nullable)onCancelWithArguments:(id _Nullable)arguments {
self.eventSink = nil;
return nil;
}