第四章Flutter实战
4.1 Fluter APP 代码结构
-
''lib” Dart代码目录
-
“ios”、“android”是两个平台相关代码、配置目录
-
pubspec.yaml是依赖的组件库配置如:
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
http: 0.13.3
dio: ^4.0.0 #大于等于4.0.0小于5.0.0
image_picker: ^0.8.3+2
也可以配置本地图片,在工程目录下创建个images文件,将所需图片导入到该目录,并配置如下:
# To add assets to your application, add an assets section, like this:
assets:
- images/
注意: 由于 yaml 文件对缩进严格,所以必须严格按照每一层两个空格的方式进行缩进,此处 assets 前面应有两个空格
4.2 代码实战
模拟微信APP,项目名称WeChatDemo。
- 项目入口:main
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
highlightColor: Color.fromRGBO(1, 0, 0, 0.0),
splashColor: Color.fromRGBO(1, 0, 0, 0.0),
cardColor: Color.fromRGBO(1, 1, 1, 0.65),//有透明层叠视图的设置
primarySwatch: Colors.blue,
appBarTheme: AppBarTheme(iconTheme: IconThemeData(color: Colors.black))
),
home: RootPage(),
);
}
}
- 主要模块:微信、通讯录、发现、我
class RootPage extends StatefulWidget {
const RootPage({Key? key}) : super(key: key);
@override
_RootPageState createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
int _currentIndex = 0;
List <Widget> _pages = [ChatPage(),FriendsPage(),DiscoverPage(),MinePage()];
final PageController _controller = PageController(initialPage: 0);
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageView( //通过pageView 来保持状态
physics: NeverScrollableScrollPhysics(), //不允许滚动
children: _pages,
controller: _controller,
),
bottomNavigationBar: BottomNavigationBar(
onTap: (index){
setState(() {
_currentIndex = index;
});
_controller.jumpToPage(index);
},
selectedFontSize: 12,
unselectedFontSize: 12,
currentIndex: _currentIndex,
fixedColor: Colors.green,
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: Image.asset('images/tabbar_chat.png',height: 25,width: 25),
activeIcon: Image.asset('images/tabbar_chat_hl.png',height: 25,width: 25,),
label: '微信'
),
BottomNavigationBarItem(
icon: Image.asset('images/tabbar_friends.png',height: 25,width: 25,),
activeIcon: Image.asset('images/tabbar_friends_hl.png',height: 25,width: 25,),
label: '通讯录'
),
BottomNavigationBarItem(
icon: Image.asset('images/tabbar_discover.png',height: 25,width: 25,),
activeIcon: Image.asset('images/tabbar_discover_hl.png',height: 25,width: 25,),
label: '发现'
),
BottomNavigationBarItem(
icon: Image.asset('images/tabbar_mine.png',height: 25,width: 25,),
activeIcon: Image.asset('images/tabbar_mine_hl.png',height: 25,width: 25,),
label: '我'
),
],
),
);
}
}
详细代码请参考项目WechatDemo
4.2.1 AutomaticKeepAliveClientMixin 混入
state 混入 AutomaticKeepAliveClientMixin,保持Tabbar 切换时state不被重新初始化
- state 混入 AutomaticKeepAliveClientMixin
class _ChatPageState extends State<ChatPage>
with AutomaticKeepAliveClientMixin
- state 添加成员属性
@override
bool get wantKeepAlive => true;
- 在 build 方法中添加
super.build(context);
4.2.2 混合开发
- Flutter调原生功能,如我的页面,更换头像需要调用系统相册,有两种实现方式
1、MethodChannel 通道
类似于通知,通过发送消息,监听消息回调实现与原生通讯
//第一步:Flutter端创建通道,传入唯一标识
MethodChannel _methodChannel = MethodChannel('mine_page');
//第二步:Flutter端发送消息
_methodChannel.invokeListMethod('picture'); //发送了一条要更换图片的消息
//第三步:原生注册‘mine_page’通道,并监听消息发送
FlutterViewController *vc = (FlutterViewController *)self.window.rootViewController;
FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:@"mine_page" binaryMessenger:vc];
self.methodChannel = methodChannel;
//监听flutter消息
[methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
if ([call.method isEqualToString:@"picture"]) {
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
imagePicker.delegate = self;
imagePicker.modalPresentationStyle = UIModalPresentationFullScreen;
[vc presentViewController:imagePicker animated:YES completion:nil];
}
}];
//第四步:原生发送拿到相册消息
[self.methodChannel invokeMethod:@"imagePath" arguments:imagePath];
//第五步:dart监听回调,并更新状态
_methodChannel.setMethodCallHandler((call) {
if(call.method == 'imagePath') {
String imagePath = call.arguments.toString().substring(7); //截取前面7个字符,拿到的参数带有file://
setState(() {
_avatarFile = File(imagePath);
});
}
return Future(() => null);
});
注:原生与Flutter交互的过程中必须确保是同一个通道,唯一性
2、ImagePicker,三方插件无需原生写多余代码
void _imagePick() async {
XFile? file = await ImagePicker().pickImage(source: ImageSource.gallery); //相册
if(file != null) {
setState(() {
_avatarFile = File(file.path);
});
}
}
-
原生项目中使用Flutter页面
1、创建Flutter 组件,在组件项目中写dart代码
- Flutter App: 创建一个完整的flutter项目,里面包含安卓和iOS项目
- Flutter Module: 创建Flutter组件时使用,混编到已有的安卓/iOS工程
- Flutter Plugin:就是Plugin的方式进行开发,比如一些百度地图,flutter不提供,通过自建Plugin,通过Plugin中的android和ios原生项目,集成原生百度功能,通过plugin中的lib中的方法来进行flutter调用百度的相关方法
- Flutter Package:纯Dart插件工程,仅包含Dart层的实现,往往定义一些公共Widget
2、通过cocoapods 导入到原生项目中,如:本地有个flutter_moudle组件,原生项目中需要植入Flutter页面,podfile文件配置如下:
3、导入Flutter库,并使用
注意:flutter页面和原生页面频繁切换会导致内存泄漏
4.2.3 安装包结构
用Xcode打开项目,选择真机编译,选择Products ->Runner.app -> 右键Show in Finder右键显示包内容,可以看到编译后结构:可执行文件、资源文件、签名、Frameworks。重点关注下Frameworks
- App.framewok: dart代码编译后的产物,App可执行文件(二进制机器代码)
- Flutter.framewok: 就是Flutter SDK源码文件,我们看到可执行文件大小是65.7M,而Flutter SDK目录下的同名framework也是65.7M,很好的证明了APP在打包过程中会将Flutter SDK 引擎导入到安装包中,这也是Flutter App安装包比原生项目体积大的原因
路径:/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-armv7_arm64/Flutter.framework
4.3 Flutter 插件开发(plugin )
-
1、Flutter插件介绍
一种专用的Dart包,其中包含用Dart代码编写的API,以及针对Android(使用Java或Kotlin)和针对iOS(使用OC或Swift)平台的特定实现(另外也可以包含Native的组件代码),也就是说插件包括原生代码与Dart代码。插件开发完成后,将上传到dart插件管理服务仓库,类似于maven、pod库,然后在flutter开发过程中可以通过pubspec.yaml(dart包管理配置文件)来获取插件服务。 -
2、为什么要开发Flutter插件
首先,虽然Flutter的生态现在已经越来越完善了,但是相比于Android跟iOS原生的生态体系,还是远远不够。很多在Android跟iOS原生上有的很酷炫的库,在Flutter中还没有或者是并没有那么的完善。其次,想必大家在原生工程里都有一套用了多年的稳定基础组件,包括网络组件、数据组件等,要重新在Flutter中用dart来搭建一套,时间成本、风险成本、组件兼容性等都是不可控的。所以,最理想的方式就是Flutter的基础组件可以对我们现有原生的组件做一层包装,然后提供接口给Flutter模块进行调用,这样一来什么时间、风险、兼容性都不是问题。我们只要维护一套原生组件就好,Flutter组件只是一层包装,并不在意内部如何去实现。那么Flutter跟原生怎么进行交互呢? -
3、Flutter如何与原生交互
1)MethodChannel 通道, 具体使用4.2.2混合开发已介绍。Flutter与原生的交互模型,类似于一种C-S模型。其中Flutter为Client层,原生为Server层,两者通过MethodChannel进行消息通信,原生端向Flutter提供已有的Native组件功能。2)究竟什么是MethodChannel
Flutter定义了3种Channel模型,分别是: BasicMessageChannel:用于传递字符串和半结构化的信息;MethodChannel:用于传递方法调用(methodinvocation)EventChannel: 用于数据流(event streams)的通信。3种channel基本类似,这里以MethodChannel展开介绍
3) MethodChannel有3个重要的成员变量:
- String name
一个Channel对象通过name来进行唯一的标识,所以在Channel的命名上一定要独一无二,推荐采用组件名_Channel名 组合来进行命名
- BinaryMessenger messenger
BinaryMessenger是Platform端与Flutter端通信的工具,其实是个中间信使,当我们初始化一个Channel,并向该Channel注册处理消息的Handler时,实际上会生成一个与之对应的BinaryMessageHandler,并以channel name为key,注册到BinaryMessenger中。当Flutter端发送消息到BinaryMessenger时,BinaryMessenger会根据其入参channel找到对应的BinaryMessageHandler,并交由其处理。BinaryMessenger维护了一个map
Binarymessenger在Android端是一个接口,其具体实现为FlutterNativeView。而其在iOS端是一个协议,名称为FlutterBinaryMessenger,FlutterViewController遵循了它。Binarymessenger只和BinaryMessageHandler打交道,而Channel和BinaryMessageHandler则是一一对应的。由于Channel从BinaryMessageHandler接收到的消息是二进制格式数据,无法直接使用,故Channel会将该二进制消息通过Codec(消息编解码器)解码为能识别的消息并传递给Handler进行处理。当Handler处理完消息之后,会通过回调函数返回result,并将result通过编解码器编码为二进制格式数据,通过BinaryMessenger返回。
- MethodCodec codec
消息编解码器Codec主要用于将二进制格式的数据转化为Handler能够识别的数据,MethodCodec主要是对MethodCall中这个对象进行序列化与反序列化,MethodCall是Flutter向Native发起调用产生的对象,其中包含了方法名以及一个参数集合
- 4、插件工程创建
推荐通过命令行来创建,因为通过IDE来创建有时候会卡住,而且会比较慢
flutter create --org com.plug.jcfc.cn --template=plugin --platforms=android,ios -i objc -a java flutter_plug
-
5、目录结构
[图片上传失败…(image-236f71-1635245763669)] -
ib dart模块
-
android android模块
-
ios iOS模块
-
example 示例测试工程可用于插件的调试
-
pubspec.yaml flutter项目的配置文件
….pubspec.yaml: dart生态下的包管理配置文件类似 Android中的gradle、iOS中的Podfile,在这里可以统一管理整个flutter工程的dart依赖包,以及管理整个插件的发布属性。
-
6、原生端开发
实现MethodCallHandler接口,注册MethodChannel对象,MethodChannel在创建时一定要保证name唯一
将MethodHandler接口注册到MethodChannel中
包装原生端组件,包括一些二方库、三方库,将包好的方法通过MethodCallHandler暴露给Flutter端 -
7、Flutter端开发
找到MethodChannel对象,通过唯一标识name,注意(name一定要与原生端注册的一致)
定义dart方法,因为要保证方法的执行不产生阻塞,所以推荐用Future async await .相关的语法见dart语法
调用methodChannel.invokeMothed()与原生进行通信 -
8、插件测试
在example/lib/main.dart下调用插件中的方法,然后直接通过命令将工程跑起来查看输出
fultter run
插件都还没有发布,为什么example工程可以直接引用?看一下example目录下的pubspec.yaml文件,里面有
dependencies:
flutter:
sdk: flutter
flutter_plug:
# When depending on this package from a real application you should use:
# flutter_plug: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../ 相对路径
pubspec.yaml 不但可以引用服务器上的插件,也可以引用本地路径下的插件。如此我们可以在插件未发布的情况下,直接在本地的测试工程里对插件进行测试
- 9、插件发布
- 完善pubspec.yaml文件
name: 插件名称
description: 插件描述
version: 0.0.1 版本号
homepage: 项目主页地址
publish_to: 填写私有服务器的地址(如果是发布到flutter pub则不用填写,插件默认是上传到flutter pub)
- 检查发布条件
flutter packages pub publish --dry-run
--dry-run 参数表示本次执行会检查插件的配置信息是否有效,插件是否满足上传条件。如果成功的话并不会真正的将插件上传,而是会显示本次要发布插件的信息,并提示成功。一般在插件的正式发布前,建议先执行该命令,避免在上传过程中出现错误
- 正式发布
发布至pub平台
flutter packages pub publish
4.3 Flutter 插件开发(Package)
纯Dart 语言,封装的有特定功能的Widget组件,创建和发布流程和plugin插件类似
参考资料
Flutter中文网
Dart和Flutter应用程序的官方软件包库
Dart语言中文网