Flutter|Flutter 开发小结 | Tips
code小生 一个专注大前端领域的技术平台公众号回复Android
加入安卓技术群
作者:水月沐風接触 Flutter 已经有一阵子了,期间记录了很多开发小问题,苦于忙碌没时间整理,最近项目进度步上正轨,借此机会抽出点时间来统一记录这些问题,并分享项目开发中的一点心得以及多平台打包的一些注意事项,希望能对大家有所帮助????。
链接:https://www.jianshu.com/p/d763d2d23d8d
声明:本文已获水月沐風
授权发表,转发等请联系原作者授权
UI 组件使用
官方为我们提供了大量原生效果的组件,如以 Android 中常见的 Material Design 系列组件和 iOS 系统中让设计师们“欲罢不能”的 Cupertino 系列组件。从我这一个月左右对于 Flutter UI 组件的使用情况来看,不得不感慨一句:“真香”。由于本人之前是做 Android 开发的,所以对于 Android 方面的一些“诟病”深有体会。例如,设计师经常让我们还原设计稿中的阴影效果,一般需要设置阴影颜色、x/y偏移量和模糊度等,然而 Android 原生并没有提供支持所有这些属性的一款组件,所以只能我们自己通过自定义控件去实现,现在还有多少人依然通过 CardView 来“鱼目混珠”呢?然而,在 Flutter 中就无需担心这种问题,通过类似前端中常用的盒子组件—— Container 就可以轻松实现。
当然,Flutter 虽然很强大,但 UI 组件也不是万能的,跨平台之路注定漫长而布满荆棘,偶尔也会伴随着一些小问题。
TextField
- 软键盘弹起后组件溢出的问题
A RenderFlex overflowed by xx pixels on the bottom.
常用的解决方案就是通过嵌套一层 SingleChildScrollView 来规避,当软键盘弹起时,下方的组件会被软键盘自动顶上去。
- HintText 不居中问题
TextFormField(
decoration: InputDecoration(
prefixIcon: Icon(
Icons.lock_outline
),
hintText: S.of(context).loginPasswordHint,
),
style: TextStyle(
/// handle hint text offset problem.
textBaseline: TextBaseline.alphabetic
),
keyboardType: TextInputType.number,
onSaved: (password) {},
)
具体可参考:https://github.com/flutter/flutter/issues/40118
- 焦点问题
- 前往另一个页面返回后自动弹出了软键盘(即自动获取了焦点)
- iOS手机上切换至数字键盘后无法关闭软键盘
FocusNode _writingFocusNode = FocusNode();
...void _clearTextFieldFocus() {
if (_writingFocusNode.hasFocus) {
_writingFocusNode.unfocus();
}
}
上述代码创建了一个 FocusNode 对象,并声明了移除焦点的方法,相信大家不难判断出。此外,我们需要给 TextField 的 focusNode 属性传入我们创建的 _writingFocusNode。问题一中,我们可以在页面跳转前先移除焦点,这样,从二级页面返回后输入框就不会自动弹出软键盘。问题二中,我们可以在用户点击空白区域后自动移除焦点(关闭软键盘),以下代码供参考:
Widget _buildInputArea() =>
Stack(
children: [
// 通过空白区域的点击事件来关闭软键盘
GestureDetector(
onTap: () {
_clearTextFieldFocus();
},
child: Container(
/// 此处注意设置背景颜色,否则默认透明色可能会穿透,无法响应点击事件
color: AppTheme.surfaceColor,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
),
),
Column(
children: [
ScreenUtils.verticalSpace(32),
// account input edit text
Padding(
padding: EdgeInsets.only(bottom: AutoSize.covert.dpToDp(12)),
child: TextField(
controller: _accountTextController,
decoration: InputDecoration(
prefixIcon: Padding(
padding: EdgeInsets.all(AutoSize.covert.dpToDp(12)),
child: ImageIcon(AssetImage(ImageAssets.ic_login_user_input)),
),
hintText: S.of(context).loginAccountHint,),
keyboardType: TextInputType.number,
),
),// password input edit text
Padding(
padding: EdgeInsets.only(bottom: AutoSize.covert.dpToDp(12)),
child: ValueListenableBuilder(
valueListenable: obscureTextModel,
builder: (context, value, child) => TextField(
controller: _passwordTextController,
obscureText: value,
decoration: InputDecoration(
prefixIcon: Padding(
padding: EdgeInsets.all(AutoSize.covert.dpToDp(12)),
child: ImageIcon(AssetImage(ImageAssets.ic_login_pwd_input)),
),
suffixIcon: IconButton(
icon: Icon(value ? Icons.visibility_off : Icons.visibility, size: AutoSize.covert.dpToDp(20)),
onPressed: () {
obscureTextModel.value = https://www.it610.com/article/!value;
}
),
hintText: S.of(context).loginPasswordHint,
),
keyboardType: TextInputType.text,
),
),
),
],
),
],
);
Container
- 盒子模型特点
- 设置背景色问题
SafeArea Android中存在状态栏、底部导航栏,而 iOS 中也存在状态栏和"底部导航条",所以如果我们页面中的边界部分需要固定显示一些小组件,那么我们最好能够在最外层嵌套一层 SafeArea 组件,即让UI组件处于“安全区域”,不至于引起适配问题。
Material(
color: AppTheme.surfaceColor,
child: SafeArea(
child: Container(),
),
)
列表组件
Flutter中常见的列表组件有 ListView、GridView、PageView 等,一个完整的应用肯定也离不开这些组件。我们在使用时,需要留意以下几点:
- Vertical viewport was given unbounded height 问题
Column(
children:[
...,
Expanded(
child: GridView.builder(
....
)
)
)
]
)
- physics 属性
自定义弹窗
Flutter 为我们提供了一些内置的定制弹窗,这里不再一一说明了。如何自定义弹窗?其实很简单,只需要明白:弹窗即页面。以下面的效果为例:
文章图片
自定义弹窗效果图 相信对于大家来说,上面的UI页面实现起来并不困难,那我们离 Dialog 效果仅剩一步之遥了:点击空白区域关闭。其实,在上面的某段代码中我已经贴了关键代码,细心的小伙伴应该也察觉到了,没错,我们可以通过 Stack 组件包裹半透明蒙层(如Container)和分享功能组件,我们只需为半透明蒙层增加点击事件即可:
Stack(
children: [
// 通过空白区域的点击事件来关闭弹窗
GestureDetector(
onTap: () {
//关闭弹窗
Navigator.maybePop(context);
},
child: Container(
color: AppTheme.dialogBackgroundColor,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
),
),
Container(
child: ...
)
)
哈哈,是不是有种恍然大悟的感觉,如此一来,弹窗对于我们来说不就是写一个页面那么简单了吗????。
InkWell InkWell 在 Android 中比较常见,俗称“水波纹”效果,属于按钮的一种,它支持设置波纹颜色、圆角等属性。我们偶尔可能会遇到水波纹失效的问题,这一般是因为我们在 InkWell 内部的 child 中设置了背景,从而导致水波纹效果被遮盖。如何解决这个问题?其实很简单,只需要在 InkWell 外层套上 Material 并设置 color 即可:
Material(
color: Colors.white,
child: InkWell(
borderRadius: AppTheme.buttonRadius, // 圆角
splashColor: AppTheme.splashColor,// 波纹颜色
highlightColor: Colors.transparent,// 点击状态
onTap: () {},// 点击事件
child: Container(
...
),
),
)
或者,我们也可以借助于之前实现自定义 Dialog 的思路,使用 Stack 包裹需要点击的区域,并将 InkWell 放在上层:
Stack(
children: [
Image(),
Material(
color: Colors.transparent,
child: InkWell(
splashColor: AppTheme.splashColor,
onTap: () {},
),
)
)
],
)
以上仅列举了部分常见UI组件的使用技巧和问题,如有其他问题欢迎留言探讨。
功能需求实现
除了 Flutter 中的一些 UI 组件的的使用以外,应用自然还需要涉及到很多具体的业务功能需求,常见的有第三方登录、分享、地图、Lottie 动画接入、第三方字体下载和加载等等。这个时候就需要我们灵活变通了,在保证项目进度顺利进行的前提下有选择性地去借助一些插件和工具,或者前往 Flutter 的 Github Issue 社区去寻找答案了,这里也选择几个常用需求简单说一下。
当前设备的系统语言 很多时候我们需要根据当前系统使用的语言去动态选择加载的内容,举个例子,我们经常需要根据当前语言去加载中文或者英文版的用户隐私条款,我们可以借助 Localizations 去获取当前使用语言的 languageCode,进而比对和处理:
/// 判断当前语言类型
_navigateToUrl(Localizations.localeOf(context).languageCode == 'zh'
? Api.PRIVACY_POLICY_ZH_CN
: Api.PRIVACY_POLICY_EN);
第三方登录/分享 这部分当初考虑自己写插件来对接原生的分享sdk,但考虑到时间成本就暂时搁置了,找到几个不错的插件来实现了该部分功能:
- fluwx
另外,他们组织中还有其他很多优秀的 Flutter 相关项目,大家也可以去学习一下。
- flutter fake toolkit
QQ插件:https://github.com/v7lin/fake_tencentLottie动画
微博插件:https://github.com/v7lin/fake_weibo
支付宝:https://github.com/v7lin/fake_alipay
相信大家对 Airbnb 公司推出的这个动画工具已经有所耳闻了,Lottie 支持多平台,使用同一个JSON 动画文件,可在不同平台实现相同的动画效果。现在复杂动画很多时候都借助于它,能够有效减少开发成本和保持动画的高还原度。同样,Flutter 中也有一些封装了 Lottie 动画的插件,让我们可以在 Flutter 上也可以感受到它的魅力。
这里,我个人使用的插件是 flutter_lottie 插件,还算稳定,支持动画属性和进度操作,唯一遗憾就是有段时间没更新了????,后续考虑到 iOS 方面的兼容性可能会自己写一个插件。在 pubspec.yaml 中依赖操作如下:
# Use Lottie animation in Flutter.
# @link: https://pub.dev/packages/flutter_lottie
flutter_lottie: 0.2.0
具体使用技巧可参考它的example:https://github.com/CameronStuartSmith/flutter_lottie
这里附上控制动画进度的部分代码:
int _currentIndex = 0;
LottieController _lottieController;
PageController _pageController = PageController();
// the key frames of animation
final ANIMATION_PROGRESS = [
0.0,
0.2083,
0.594,
0.8333,
1
];
// the duration of each animation ps
final ANIMATION_TIMES = [
2300,
4500,
3500
];
// animation progress controller
Animation animation;
AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = new AnimationController(
duration: Duration(milliseconds: ANIMATION_TIMES[_currentIndex]), vsync: this);
final Animation curve =
new CurvedAnimation(parent: _animationController, curve: Curves.linear);
animation = new Tween(begin: 0.0, end: 1.0).animate(curve);
animation.addListener(() {
_applyAnimation(animation.value);
});
}// 布局代码
.......Positioned(
bottom: 0,
child: Container(
width: MediaQuery.of(context).size.width,
// 此处为了将动画组件居下放置
height: AutoSize.covert.dpToDp(667),
child: LottieView.fromFile(
filePath: 'assets/anims/user_guide_anim.json',
autoPlay: false,
loop: true,
reverse: true,
onViewCreated: (controller) {
_lottieController = controller;
Future.delayed(Duration(milliseconds: 1), () {
_animationController.forward();
});
},
),
),
),// description page view
Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
margin: EdgeInsets.only(bottom: 60),
child: PageView(
physics: BouncingScrollPhysics(),
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentIndex = index;
_animationController.duration = Duration(milliseconds: ANIMATION_TIMES[index]);
});
Future.delayed(Duration(microseconds: 600), () {
_animationController.forward(from: 0);
});
},
children: _buildPageGroup(),
),
),......void _applyAnimation(double value) {
var startProgress = ANIMATION_PROGRESS[_currentIndex];
var endProgress = ANIMATION_PROGRESS[_currentIndex + 1];
var progress = startProgress + (endProgress - startProgress) * value;
_lottieController.setAnimationProgress(progress);
}
简单解释一下上述代码逻辑,我们这里主要借助于 Lottie 来实现用户引导页的切换动画,引导页分为三个画面,所以需要我们记录和保存动画的关键帧和每段画面的执行时间。至于动画的控制执行权交由上层的 PageView 来滑动实现,每次滑动通过 AnimationController 和 setState((){}) 来控制和刷新每段动画的执行时间和执行刻度。具体demo效果如下所示:
Flutter中的lottie动画效果
外部字体下载和加载
如果接触过文字编辑功能开发的小伙伴应该都知道,我们一般会提供几十种字体供用户使用,当然,我们不可能在项目打包时就放入这么多字体包,这样显而会严重增加安装包大小。我们一般的做法是:当用户第一次点击想使用某个字体时,我们会先将其下载到手机本地存储,然后加载字体,后续当用户再次选择该字体,那么直接从本地加载即可。那么问题来了,Flutter 目前的示例中仅为我们提供了从本地 Asset 目录下加载字体的方式,显然想要实现上述需求,需要我们自己寻求出路。
幸运的是,上帝为我们关上了一扇门,也为我们打开了一扇窗,Flutter 中为我们提供了一个 FontLoader 工具,它有一个 addFont 方法,支持将 ByteData 格式数据转化为字体包并加载到应用字体资源库:
/// Registers a font asset to be loaded by this font loader.
///
/// The [bytes] argument specifies the actual font asset bytes. Currently,
/// only TrueType (TTF) fonts are supported.
void addFont(Future bytes) {
if (_loaded)
throw StateError('FontLoader is already loaded');
_fontFutures.add(bytes.then(
(ByteData data) => Uint8List.view(data.buffer, data.offsetInBytes, data.lengthInBytes)
));
}
... /// Loads this font loader's font [family] and all of its associated assets
/// into the Flutter engine, making the font available to the current
/// application.
///
/// This method should only be called once per font loader. Attempts to
/// load fonts from the same loader more than once will cause a [StateError]
/// to be thrown.
///
/// The returned future will complete with an error if any of the font asset
/// futures yield an error.
Future load() async {
if (_loaded)
throw StateError('FontLoader is already loaded');
_loaded = true;
final Iterable> loadFutures = _fontFutures.map(
(Future f) => f.then(
(Uint8List list) => loadFont(list, family)
)
);
return Future.wait(loadFutures.toList());
}
如此一来,那我们解决思路也就“手到擒来”了:只需要将字体下载到本地并以文件形式存储,在使用时将字体文件再转为 ByteData 数据格式供 FontLoader 加载即可。这里附上简化后的部分关键代码:
/// 加载外部的字体
Future loadFontFile(LetterFont font) async {
// load font file
var fontLoader = FontLoader(font.fontName);
fontLoader.addFont(await fetchFont(font));
await fontLoader.load();
}
/// 从网络下载字体资源
Future fetchFont(LetterFont font) async {
final response = await https.get(
font.fontUrl);
if (response.statusCode == 200) {
// 这里也可以做保存到本地的逻辑处理
return ByteData.view(response.bodyBytes.buffer);
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load font');
}
}
打包上架相关
打包方面也有一部分细节需要注意一下,这里谈一下 Android 和 iOS 开发环境配置和打包差异以及列举部分常见问题,其他问题因人而异,也因版本而异,就不单独拿出来讲了。
Android方面
- 开发工具
- 代码编译环境
目前Flutter创建项目默认勾选两个选项
- 版本号配置
注意:如果在 pubspec.yaml 中配置了version,那么 Flutter 具体打包的版本会实际根据 pubspec.yaml 的 version 来构建。
- 网络配置
然后在 AndroidManifest.xml 文件中设置 networkSecurityConfig 属性即可:
......
- 权限配置
当然这些还是不够的,Android6.0及以上,我们还需要在代码中动态申请权限,Flutter中有很多优秀的权限申请插件,iOS 上面一般没问题,Android由于碎片化比较严重,可能会在不同机型上出现各种奇怪问题,比如,红米部分机型借助于 permission_hanlder 插件申请定位权限可能会失败的问题,这里需要注意一下,留个心眼。
- Logo 配置
具体可以参考该文章:https://blog.csdn.net/guolin_blog/article/details/79417483
- 打包
flutter build apk --target-platform android-arm,android-arm64,android-x64 --split-per-abi
执行完毕后就会在 release 目录下生成三种格式的apk包。
另外,大家可以选择一些apk体积优化的方案,具体可参考:
https://my.oschina.net/u/1464083/blog/2395914
https://www.jianshu.com/p/555c948e5195
iOS 方面 由于本人之前做 Android 开发,没有接触过 iOS,所以打包到 iOS 平台还是遇到不少问题。
- 开发工具:
- 代码编译环境:Swift + Objective-C (目前创建Flutter项目默认勾选为swift,由于项目启动时Flutter尚未更新该配置,所以项目中部分插件采用的是oc),希望后面逐步替换为主流的swift。
- 版本号配置:
- 网络配置
在 Runner -> targets -> General -> Info 中添加 App Transport Security Settings 属性,并在此属性标签内添加 Allow Arbitrary Loads 子属性,并将值设置为 YES 即可。
- Logo配置
文章图片
xcode的logo配置 点击 ?? 即可进入 logo 资源目录,默认的为 Flutter 的官方 logo,我们只需要根据具体 logo 尺寸去替换资源即可。
- 国际化语言配置
- 打包相关
文章图片
beta版打包问题 以上就是本人对近期 Flutter 开发过程的一点简单总结,如果能够帮助到您那将再好不过????。刚接触 Flutter 不久,相关阐述可能不够严谨或存在理解错误,如果您发现了,还请指出,感谢您的阅读。
相关阅读
1 Flutter 添加到现有项目
2 Flutter Platform View:在 Flutter 中使用Android、iOS的原生 View
3 Flutter1.12 升级后的问题
4 Flutter 实现 App 内更新安装包
5 Flutter 与原生交互总结
文章图片
如果你有写博客的好习惯
欢迎投稿
【Flutter|Flutter 开发小结 | Tips】点个在看,小生感恩??
推荐阅读
- flutter使用记录|flutter 问题记录
- Flutter|Flutter开发中的一些小技巧整理
- 程序员|Flutter 10天高仿大厂App及小技巧积累总结,被阿里面试官征服了
- Flutter|【Flutter】Android原生WebView(非Flutter WebView)与FlutterWeb交互
- Xmas!送你Flutter|Xmas!送你Flutter Animation小星星!
- 程序员|Flutter(实战技巧,最新阿里Android高级面试题及答案)
- Android开发|Flutter 小技巧之 MediaQuery 和 build 优化你不知道的秘密
- Android开发的日常记录|【Android P】OTA升级包定制,移除不需要更新的分区,重新打包签名
- 二、Spring程序开发
- A8(K8)模式,哈希竞猜游戏开发案例分析和应用