Flutter开发(实践篇)
环境搭建
iOS
- 克隆Flutter项目
git clone -b beta https://github.com/flutter/flutter.git
将flutter加入path
使用zsh的同学要注意,修改.zshrc才有效,而不是.bash_profile。export PATH=[克隆项目的目录]/flutter/bin:$PATH
安装iOS依赖的工具
$ brew install --HEAD libimobiledevice $ brew install ideviceinstaller ios-deploy cocoapods
安装VS Code
下载地址在VS中安装Dart以及Flutter
若Android也使用VS Code的话,也是同样需要该步骤。运行Flutter的环境检查
第一次运行flutter指令时会下载额外的内容和编译出flutter工具,需要等一段时间。flutter doctor
Android
初始化项目
在AS的偏好设置的插件菜单中选择浏览仓库,再选择安装Flutter插件,同时也安装Dart插件,然后重启。参考安装adb及使用
brew cask install android-platform-tools adb devices adb install /Path/to/app-debug.apk
通过flutter doctor的指引来同意lisence
创建工程
App开发
创建纯Flutter的App工程
flutter create myApp
如果在App里导入了包(package)或者插件(plugin),执行flutter packages get
命令后,以iOS为例,插件的平台源码会在下图的新建路径上存放并引用,如下图
原身在下面的路径中
/Users/user/.pub-cache/hosted/pub.dartlang.org
在App编译时,包或者插件的lib中的dart以及plugin中的platform(iOS/Android)代码就被一并编译到App.framework中
包及插件开发
Flutter的包,是指通过利用pubspec.yaml文件发布版本的形式提供纯Dart的依赖Flutter框架实现的库给Flutter的App使用,或者提供给Native App中的Flutter Module使用。
flutter create --template=package hello
Flutter的插件,其实它也算是包的一种,是指提供可访问Native功能的Dart接口库,中间通过channel技术实现通信,插件同样可以通过发布版本提供。
flutter create --template=plugin -i objc -a java plugin_template
-i -a 参数是分别指定iOS和Android两平台代码使用的语言(预设到模板中)。
混合开发
混合开发是在Native App的基础上,嵌入Flutter的模块,共同开发。
flutter create -t module my_flutter
若想在混合开发中解除对Flutter的依赖,可以先来看看Flutter它所依赖的文件:
- Flutter库和引擎:Flutter.framework(flutter.jar);
- Flutter工程的产物:App.framework(vm/isolate_snapshot_data/instr),App和Module最后的产物都是这种形式;
- Flutter Plugin:编译出各种Plugin的framework,内含插件的Dart以及响应它的OC代码(iOS:GeneratedPluginRegistrant.m; Android:GeneratedPluginRegistrant.java)。
将这三部分的编译结果抽取出来,打包成一个SDK依赖的形式提供给Native工程,就可以解除Native工程对Flutter工程的直接依赖了。
运行调试
运行
flutter run
编译
flutter build ios --no-codesign [--simulator] #针对模拟器
flutter build apk
第一次打开Flutter的Android工程后,点击make project
按钮先构造项目,实质就是通过gradle拉取对应依赖的库,而iOS工程若无podfile则可以直接运行编译,否则需先执行pod install
命令。
热重载
ctrl+s/cmd+s,保存文件变更,即可触发热重载,或者点击运行时工具栏中的reload按钮。
但是在以下情况会导致重载失败:
- 编译错误;
- 控件类型从StatelessWidget到StatefulWidget的转换;
- 全局变量和静态成员变量的变更。
- 修改了main函数中创建的根控件节点;
- 某个类从普通类型转换成枚举类型,或者类型的泛型参数列表变化;
遇到的问题
iOS
在iOS端跑pod install时,提示
/Path/To/ExistingApp/Flutter/engine
路径无效,需要自行先在ExistingApp项目根目录下创建Flutter文件夹;iOS编译报错,FlutterPluginRegistrant Target中的GeneratedPluginRegistrant.h提示
#import <Flutter/Flutter.h>
找不到,可以手动为该Target引用上Flutter.framework,或将App Build Phases中的Run Script移到Check Pods Mnifest.lock之后;iOS编译报错,提示
/packages/flutter_tools/bin/xcode_backend.sh: No such file or directory
,此时要在BuildSettings中的User-Defined中设置环境变量FLUTTER_ROOT,值flutter sdk的路径(包含packages文件夹),其对应App Run Script脚本中的$FLUTTER_ROOT;如何切换设备,若Mac上有多个Xcode,且VScode使用最新版本的iPhone模拟器时,会有以下报错,此时,在VScode最底下的工具条中点击设备进行切换,如下图中的iPhoneX 12.0(ios)可切换为iPhoneX(ios Emulator)
Code Signing Warning: “Runner” isn’t code signed but requires entitlements. It is not possible to add entitlements to a binary without signing it.
Xcode10运行报错,在File->Workspace Settings->Build System 改成 Legacy Build System即可。参考
Multiple commands produce ‘/Users/Library/Developer/Xcode/DerivedData/Runner-dunimcekkdlkcxevvsirtjrlpkdg/Build/Products/Debug-iphonesimulator/Runner.app/Frameworks/Flutter.framework’:
1) Target ‘Runner’ has copy command from ‘/Users/Flutter/plugin_template/example/ios/Flutter/Flutter.framework’ to ‘/Users/Library/Developer/Xcode/DerivedData/Runner-dunimcekkdlkcxevvsirtjrlpkdg/Build/Products/Debug-iphonesimulator/Runner.app/Frameworks/Flutter.framework’
2) That command depends on command in Target ‘Runner’: script phase “[CP] Embed Pods Frameworks”App.framework中的dart代码没更新时,在debug模式下在vs上reload一下即可。
运行崩溃,提示 Image Not Found。Flutter工程生成的App.framework和Flutter.framework导入应用工程后运行崩溃。在Embedded Binaries中添加上App.framework和Flutter.framework即可解决。
Library not loaded: @rpath/App.framework. Reason: Image Not Found
展示FlutterViewController后没有内容显示,同时报错了。将Flutter(插件)工程中的flutter_assets复制到应用工程,且以绝对路径添加,flutter_assets在与二进制文件同级目录下。
[VERBOSE-2:engine.cc(112)]Engine run configuration was invalid.
[VERBOSE-2:FlutterViewController.mm(437)] Could not launch engine with configuration.无法在模拟器上跑release相关的代码
暂时还不支持在模拟器上启动release模式或跑引用了release模式下生成的Flutter App库的项目。而又为什么一定要跑release模式呢,因为debug模式生成的Flutter.framework含dart的虚拟机进行JIT,苹果暂时还不允许该动态编译(可以的话热更也就已经用上了)。debug模式是方便我们reload调试,不用重复停止和运行工程项目。参考无法在真机上安装
编译通过后在安装阶段报错,替换为debug模式下生成的App.framework和Flutter.framework则能正常安装(但运行时会崩溃,因为debug模式下导出的App.framework不含arm7s和arm64的真机框架)。而换回自行在Generated.xcconfig
修改FLUTTER_BUILD_MODE
和FLUTTER_FRAMEWORK_DIR
为release值(具体值为release和path/to/flutter/bin/cache/artifacts/engine/ios-release),通过运行Flutter项目后提出的App.framework和Flutter.framework则继续报错。后来通过以下方式编译Flutter项目再提取App.framework和Flutter.framework即解决。
执行下面这条指令会自动更新Generated.xcconfig
中上述提到的项为release值,这样编译得到的framework才有效。flutter build ios --no-codesign --release
app installation failed. Could not inspect the application package
Android
Android在gradle build时提示错误,将
.flutter/packages/flutter_tools/gradle/flutter.gradle
中的maven {...}
一段移到jcenter()
之上即可。参考flutter Could not find lint-gradle-api.jar com.android.tools.lint:lint-gradle-api:26.1.2`
Configuration(Target)的Android App中提示
Please select Android SDK
,此时运行选项 File -> Sync Project with Gradle Files(在主面板的右上角也有便捷按钮)即可解决。参考Android下,
No Target Device Found
,没有设备可用,在 Edit Configurations中,右下方的 Deployment Target 选项选择Open Select Deployment Target Dialog
即可。参考构建及定位Android的apk文件。菜单选项 Build->Build APK:是随机生成签名的构建,而 Build->Generate Signed APK:是指定签名的构建(签名为sha1文件)。构建完毕后,在弹出的Tips中点击Local链接可找到APK所在位置(flutterProject/build/host/outputs/apk)
功能实现问题
透明背景
官方探究中(跟进): https://github.com/flutter/flutter/issues/9321
Flutter上调用原生桥接口报错没找到方法
Dart Error Unhandled exception MissingPluginException No implementation found for method
由于添加channel的注册表对象没与作为Flutter视图显示的对象(实现FlutterPlugin协议的FlutterViewController)一致。
即是下面该方法中,生成register的flutterViewController(register=[flutterViewController registrarForPlugin:pluginName])在显示时,才能使用efun_flutter_plugin
该channel名调用EFFlutterModule中的代理方法
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"efun_flutter_plugin"
binaryMessenger:[registrar messenger]];
[registrar addMethodCallDelegate:[EFFlutterModule shareInstance] channel:channel];
}
拉伸listView子项
使用itemExtent属性,为listView内容指定高度。
限制listView滑动
将itemExtent设为指定高度后,再配合shrinkWrap设为true,但此时仍可能还是可以手动滑动到listView,这时将listView的padding设为EdgeInsets.zero即可解决,因为listView内容的底部默认多出一个padding的白色栏,若此高度超出屏幕的话listView还是可以被手动滑动,然后还有设置上controller属性(无需实现监听),这样就既能限制滑动也能有效使键盘弹出后界面自动上移。
原生跳转到Flutter白屏1秒
- 提早注册
- 采用release下的AOT模式
跳转Route无效
Navigator.pushNamed(context, arguments['route']);
以上的context必须为当前渲染View中的context,否则push无效。
从Native获取到的字典无法直接复制给Map
_InternalLinkedHashMap<dynamic, dynamic>’ is not a subtype of type ‘Map<String, dynamic>
var resultValues = Map<String, String>.from(result);
收起键盘且清除焦点
FocusScope.of(context).requestFocus(new FocusNode());
为ScrollView添加滑动事件监听
ScrollController scrollController = ScrollController();
scrollController.addListener(scrolledListView);
ListView(controller: scrollController,...);
Null scrolledListView() {
scrollController.jumpTo(scrollController.position.minScrollExtent);
}
隐世滑动ScrollView到指定位置
scrollController.jumpTo(scrollController.position.minScrollExtent);
获取TextField的text值
var textController = TextEditingController();
textfield = TextField(
controller: controller,
...
);
String wanted = textController.text
TextField的装饰
TextField(
decoration: InputDecoration(
hintText: placeholder,
fillColor: Colors.white,
filled: true,
contentPadding: EdgeInsets.fromLTRB(5.0, 5.0, 5.0, 5.0),
border: new OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
),
)
);
TextField输入密文
TextField(
obscureText: true,
...
);
为背景添加点击事件监听
GestureDetector(
onTap: () {},
child: backgroundWidget
)
显示Textfield
Flutter中的Textfield必须用MaterialApp包裹的Scaffold包裹的Widget来包裹,才能显示,否则渲染报错。
按比例分割的自动约束
在Column或者Row中,的Widget均使用Expanded包裹,且对每个Expanded设置其flex属性,其比例就等于它的flex在其所在的Column或Row中所有Expanded的flex总和的占比,同时在Expanded中的控件则会被自动伸缩。
均匀分布Colomn或Row的子项:
mainAxisAlignment: MainAxisAlignment.spaceEvenly
获取屏幕Size
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
...
return widget
}
定义可继承的成员变量
final platformMethodChannel = MethodChannel('com.fun.global/ui');
往Native传值
try {
Map<String, String> map = {
"email": email,
"password": password
};
final String result = await methodChannel.invokeMethod('#methodName',map);
//不一定返回String,现为示例
print(result);
} on PlatformException catch (e) {
String message = "exception: ${e.message}.";
print(message);
}
渐变动画
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (BuildContext context, _, __) {
return EmailLoginView();
},
transitionsBuilder: (___, Animation<double> animation, ____, Widget child) {
return FadeTransition(
opacity: Tween<double>(begin: 0.0, end: 1.0).animate(animation),
child: child,
);
},
transitionDuration: Duration(milliseconds: 500),),
);
倒计时
Future<Null> onTapSendVerificationCodeButton() async {
if (_seconds == 0) {
setState(() {
_startTimer();
});
else {
return;
}
}
_startTimer() {
_seconds = 60;
_timer = new Timer.periodic(new Duration(seconds: 1), (timer) {
if (_seconds == 0) {
_cancelTimer();
return;
}
_seconds--;
_sendButtonTitle = '$_seconds(s)';
setState(() {});
if (_seconds == 0) {
_sendButtonTitle = '发送验证码';
}
});
}
_cancelTimer() {
_timer?.cancel();
}
Indicator
先对State调用setState((){…})对界面刷新,在setState的回调块中将Indicator添加到Stack的顶层(children的最后一个元素,前面的元素为原来Widget),一般通过一个状态变量控制该Stack是否要添加Indicator。
https://codingwithjoe.com/flutter-how-to-build-a-modal-progress-indicator/
获取平台信息
import 'dart:io';
Platform.isIOS;
自定义路由跳转动画
class MyCustomRoute<T> extends MaterialPageRoute<T> {
MyCustomRoute({ WidgetBuilder builder, RouteSettings settings })
: super(builder: builder, settings: settings);
@override
Widget buildTransitions(BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
if (settings.isInitialRoute)
return child;
// Fades between routes. (If you don't want any animation,
// just return child.)
return new FadeTransition(opacity: animation, child: child);
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
onGenerateRoute:(RouteSettings settings) {
switch (settings.name) {
case '/': return new MyCustomRoute(
builder: (_) => new HomeView(),
settings: settings,
);
case '/emailLoginView': return new MyCustomRoute(
builder: (_) => EmailLoginView(),
settings: settings,
);
}
},
);
}
}
注意MaterialApp中的home和routes参数对onGenerateRoute的影响。
Android端的输入法不响应
在activity生命周期onResume之后创建显示的flutterView,输入法不会响应。这个是flutter的bug。解决方法是,改一下window的可见性(Visibility),例如隐藏再显示。
桥
Flutter 侧:
class GlobalBridge {
static const MethodChannel global_ui_channel = const MethodChannel('com.fun.global/ui');
//设置通道对Flutter的回调
static void registerChannel() {
global_ui_channel.setMethodCallHandler(handleMethodCall);
}
//原生调Flutter的处理
static Future<dynamic> handleMethodCall(MethodCall methodCall) async {
if (method == 'xxx') {
...
}
}
//Flutter调Native
static Future<dynamic> callMethod(String method,[dynamic arguments]) {
return global_ui_channel.invokeMethod(method,arguments);
}
Native 侧(iOS):
//设置Flutter的插件名
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
NSObject<FlutterPluginRegistrar>* registrar = [registry registrarForPlugin:@"global_plugin"];
[[self class] registerWithRegistrar:registrar];
}
//获取桥通道并设置通道对Native的回调
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel *globalUiChannel = [FlutterMethodChannel methodChannelWithName:@"com.fun.global/ui" binaryMessenger:[registrar messenger]];
[registrar addMethodCallDelegate:[EFNGlViewNavigator sharedNavigator] channel:globalUiChannel];
[EFNGlViewNavigator sharedNavigator].globalUiChannel = globalUiChannel;
}
//Flutter调原生的处理
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
NSString *method = call.method;
if ([method isEqualToString:@"#request_bindFacebook"]) {
[SomeClass requestWithCompletion:^(NSDictionary * _Nullable resultDict, NSError * _Nullable error) {
result(resultDict);
}];
}
}
//原生调Flutter
- (void)callFlutterMethod:(NSString *)method arguments:(id)arguments result:(nullable FlutterResult)callback {
if (method.length>0) {
[self.globalUiChannel invokeMethod:method arguments:arguments result:callback];
}
}
调试
在main函数中设置debugPaintSizeEnabled为true
import 'dart:developer';
void main() {
debugPaintSizeEnabled=true;
}
cmd+shift+p
,然后输入Flutter Toggle Debug Painting,开启Debug Painting模式
其它
国际化:参考
异步:参考
构造:参考
alertDialog:参考
正则表达式:参考
AppUiDemo:参考
效率:参考
隐藏状态栏:参考
总结
整体的环境搭建、开发流程并不复杂,Dart语言上手也很快。热重载对于调试App的UI真的是一大福音,不过在Module的混合开发模式下,要想与原生起到联调且能继续使用热重载功能的这部分还没搞清楚怎么弄。Flutter的UI搭建体验上也很好,经过封装,整体结构还是可以很清晰地被阅读,既有高的搭建效率也有高的易读性。这次实践主要是与原生交互、Flutter UI效果,后续还要了解网络、数据存储等一些关键部分。希望Flutter能继续保持现在高热的状态发展,造福跨平台的开发者,不要在某天和哪个平台的财主闹翻了而抛弃兼容影响大家。
Ref:
什么是 LLVM?Swift, Rust, Clang 等语言背后的支持
Dart-on-LLVM
揭秘Flutter Hot Reload
开发包与插件
Add-Flutter-to-existing-apps
[iOS原生项目集成](Flutter https://juejin.im/post/5b4615576fb9a04fb614d382)
Change iOS rootViewController with Flutter
Hummingbird: Building Flutter for the Web
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 mingfungliu@gmail.com
文章标题:Flutter开发(实践篇)
文章字数:3.6k
本文作者:Mingfung
发布时间:2019-01-09, 23:06:00
最后更新:2019-10-11, 12:36:22
原始链接:http://blog.ifungfay.com/前端/Flutter开发(实践篇)/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。