Flutter新手开发问题和记录
说明:
$ROOT_PATH: 代表flutter项目的根目录
一、访问加速
1、编译时加速
gradle会在安卓编译的时候下载一些依赖,默认的仓库是google的,在国内访问速度会很慢,所以替换阿里云的仓库,修改 $ROOT_PATH/android/build.gradle 文件,修改内容如下
buildscript {
ext.kotlin_version = '1.6.21'
repositories {
- google()
- mavenCentral()
+ maven { url 'https://maven.aliyun.com/repository/jcenter' }
+ maven { url 'https://maven.aliyun.com/repository/google' }
+ maven { url 'https://maven.aliyun.com/repository/central' }
+ maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.0-rc03'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
2、flutter的dart仓库访问加速
国内flutter镜像,执行下面的命令设置,或者写入到.zshrc文件中后,重启终端再运行 flutter pub get 就会快很多。
官方说明:Using Flutter in China | Flutter
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
二、常见实现
1、取消点击的水波效果
在iOS上一般不需要水波纹效果,修改主题就可以了
theme: ThemeData(
highlightColor: Colors.transparent,
splashColor: Colors.transparent
),
2、修改Tabbar指示器的宽度为固定宽度
组件默认仅支持(和tab文本一样长、和tab一样长,两种方式)
- 修改原生的UnderlineTabIndicator组件
- 修改渲染长度的函数
- 使用自定义的UnderlineTabIndicator来渲染指示器
目标效果,指示器是固定宽度的:
实现如下:
/// 可以固定宽度的tab指示器
/// 基于UnderlineTabIndicator修改
import 'package:flutter/material.dart';
class FixedUnderlineTabIndicator extends Decoration {
const FixedUnderlineTabIndicator({
// 定义宽度是可以在外部传入的
this.width = 20,
this.borderSide = const BorderSide(width: 2.0, color: Colors.white),
this.insets = EdgeInsets.zero,
});
final BorderSide borderSide;
final EdgeInsetsGeometry insets;
// 添加成员变量width
final double width;
@override
Decoration? lerpFrom(Decoration? a, double t) {
if (a is FixedUnderlineTabIndicator) {
return FixedUnderlineTabIndicator(
borderSide: BorderSide.lerp(a.borderSide, borderSide, t),
insets: EdgeInsetsGeometry.lerp(a.insets, insets, t)!,
);
}
return super.lerpFrom(a, t);
}
@override
Decoration? lerpTo(Decoration? b, double t) {
if (b is FixedUnderlineTabIndicator) {
return FixedUnderlineTabIndicator(
borderSide: BorderSide.lerp(borderSide, b.borderSide, t),
insets: EdgeInsetsGeometry.lerp(insets, b.insets, t)!,
);
}
return super.lerpTo(b, t);
}
@override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
return _FixedUnderlinePainter(this, onChanged);
}
Rect _indicatorRectFor(Rect rect, TextDirection textDirection) {
final Rect indicator = insets.resolve(textDirection).deflateRect(rect);
return Rect.fromLTWH(
// 这里计算指示器的左侧位置,和传入的指示器宽度进行计算
indicator.left + (indicator.width - width) / 2,
indicator.bottom - borderSide.width,
width,
borderSide.width,
);
}
@override
Path getClipPath(Rect rect, TextDirection textDirection) {
return Path()..addRect(_indicatorRectFor(rect, textDirection));
}
}
class _FixedUnderlinePainter extends BoxPainter {
_FixedUnderlinePainter(this.decoration, VoidCallback? onChanged)
: super(onChanged);
final FixedUnderlineTabIndicator decoration;
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
assert(configuration.size != null);
final Rect rect = offset & configuration.size!;
final TextDirection textDirection = configuration.textDirection!;
final Rect indicator = decoration
._indicatorRectFor(rect, textDirection)
.deflate(decoration.borderSide.width / 2.0);
final Paint paint = decoration.borderSide.toPaint()
..strokeCap = StrokeCap.square;
canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint);
}
}
3、修改状态栏为白色,需要在runApp后执行下面的代码
void main() {
runApp(const MyApp());
// 修改状态栏为白色
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(statusBarColor: Colors.white));
}
// 还有一种方式就是修改主题样式
// 设置appBar的样式, 添加 systemOverlayStyle
appBarTheme: const AppBarTheme(
elevation: 0,
color: Colors.white,
foregroundColor: Colors.black,
titleTextStyle: TextStyle(color: Colors.black, fontSize: 20),
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.white,
// 修改顶部状态栏的图标的颜色,深色主题就用亮色的图标
statusBarIconBrightness: Brightness.light
)
);
4、取消右上角的debug标识,需要配置MaterialApp
debugShowCheckedModeBanner: false
5、退出app
// iOS和安卓都支持
exit(0)
// 下面的这个只有安卓支持,iOS不支持
// SystemNavigator.pop();
6、配置appBar的高度
我们查看AppBar的定义并不能修改高度,需要使用PreferredSize包裹一下才可以设置高度,如下:
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size(double.infinity, 80),
child: AppBar(
title: Text(_tabs[_tabIndex]["name"]),
backgroundColor: Colors.red,
actions: [
IconButton(
onPressed: () {
context.push("/scan");
},
icon: const Icon(Icons.qr_code_scanner))
]),
)
)
}
展示效果:
7、隐藏和展示状态栏
例如跳转到拍照页面时候,需要隐藏,但是隐藏和展示的时候没有动画效果,比较卡顿,还是不建议使用的,目前还在寻找其他的解决方案。
// 隐藏
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
// 展示
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
8、Positioned组件宽度占满父容器的方案
只需要左右都设置为0即可,使用宽度设置为double.infinity无效;同理,要是想高度占满父容器,只需要上下都设置为0即可
Positioned(
left: 0,
right: 0,
child: Container(
color: Colors.red,
child: Text("测试定位"),
))
9、一段文本中某一部分文本可以点击
例如一段文本中有一个链接,可以跳转到其他页面,需要结合RichText和TextSpan一起实现
RichText(
text: TextSpan(
// 这段文本的默认样式
style: TextStyle(
color: const Color(0xff252525),
height: 20 / 14,
fontSize: px2dp(14)),
text: '上述是常见问题,如果您有新的疑问,请',
children: [
TextSpan(
text: "点此反馈",
// 可点击区域的样式
style: const TextStyle(color: Color(0xff2F92FF)),
// 添加点击事件
recognizer: TapGestureRecognizer()..onTap = () {
Get.toNamed('/feedback');
}
),
const TextSpan(text: ",我们会尽快联系您")
]
)
)
实现的效果如下,我们点击 “点此反馈” 的时候就会跳转到路由 /feedback。
10、Container设置圆角之后,子元素超出没有隐藏
给Container设置:clipBehavior: Clip.hardEdge 即可,左侧图设置了,右侧图没有设置。
11、自定的AppBar组件报错 can't be assigned to the parameter type 'PreferredSizeWidget'
需要将PreferredSizeWidget使用mixin的形式添加到自定义组件上(参考答案),并覆盖preferredSize get方法,如下所示:
import 'package:daily_neckcare/utils/layout.dart';
import 'package:flutter/material.dart';
// 左上角返回按钮样式
enum CustomAppBarBackIconType {
// 展示返回按钮
back,
// 展示关闭按钮
close
}
/// 自定义appBar,修改了返回按钮的样式
class CustomAppBar extends StatelessWidget with PreferredSizeWidget {
const CustomAppBar(
{super.key,
this.title,
this.actions,
this.centerTitle,
this.leading,
this.backIconType});
// 查看AppBar文档
final Widget? title;
final Widget? leading;
final List<Widget>? actions;
final bool? centerTitle;
// 左上角返回按钮样式,默认是返回箭头的样式
final CustomAppBarBackIconType? backIconType;
@override
Widget build(BuildContext context) {
Widget? currentLeading = leading;
final ModalRoute<dynamic>? parentRoute = ModalRoute.of(context);
final bool canPop = parentRoute?.canPop ?? false;
final bool useCloseButton =
parentRoute is PageRoute<dynamic> && parentRoute.fullscreenDialog;
// 默认返回按钮
String backIconAsset = "assets/images/icon_navigation_bar_back.png";
if (useCloseButton || backIconType == CustomAppBarBackIconType.close) {
backIconAsset = "assets/images/icon_navigation_bar_close.png";
}
if (canPop || (parentRoute?.impliesAppBarDismissal ?? false)) {
currentLeading = InkWell(
onTap: () {
Navigator.maybePop(context);
},
child: Container(
margin: EdgeInsets.only(left: px2dp(15)),
child: Image.asset(
backIconAsset,
width: px2dp(44),
height: px2dp(44),
fit: BoxFit.cover,
),
),
);
}
return AppBar(
title: title,
leading: currentLeading,
centerTitle: centerTitle,
actions: actions,
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
12、Flutter App清除缓存
就像浏览器一样,我们有时候需要清楚缓存来重新拉取一些数据。Flutter App的缓存数据存储在app的专属目录,我们获取到目录并清除里面的所有文件即可,首先我们要将 path_provider 加入到项目的依赖。
下面是安卓模拟器下面的缓存文件夹,可以看到HTTP Cache也在里面,我们删除后,webview的缓存也就没有了
注意:要是你在cache文件夹中存储了应用程序的数据,就不能直接调用deleteSync删除了,而是需要列出文件夹后,排除不能删除的文件夹。
_clearCache() async {
// 获取缓存文件夹,iOS和安卓在插件底层已经封装了
final dir = await getTemporaryDirectory();
// 递归删除文件和文件夹即可
dir.deleteSync(recursive: true);
}
13、在flutter页面中使用markdown展示内容
使用到的插件是: flutter_markdown
注意:markdown暂时不支持html标签,如果存在html标签会解析失败,使用方式如下:
MarkdownBody(
data: controller.list[index].answer,
selectable: true,
softLineBreak: true,
styleSheet: MarkdownStyleSheet(
p: TextStyle(
fontSize: px2dp(15),
color: const Color(0xff626262),
height: 21 / 15
)
),
onTapLink: (text, href, title) {
Get.toNamed('/webview?url=$href');
}
)
14、获取flutter颜色设置透明度的实际颜色
有时候我们需要使用一个颜色加上透明度,生成一个新的颜色。有下面两种实现方式:
- 直接使用 Colors.red.withOpacity(0.16) 实现,如果将其设置为背景颜色,会看到元素底部的内容
- 使用颜色合成,即 合成颜色 = 背景 + ( 原始颜色 + 透明度 ),优点就是生成的新颜色不会看到元素底部的内容,一般将白色设置为背景颜色。
Color.alphaBlend(Colors.red.withOpacity(0.16), Colors.white);
15、为操作添加震动反馈,例如点击按钮、滑动slide等
适当给用户反馈可以提升用户体验,例如点击重要按钮的时候震动一下等,可以使用flutter内置的API去调用震动;
// 震动的力度可以调用不同的方法,例如这是一个中等的震动
import 'package:flutter/services.dart';
HapticFeedback.mediumImpact();
三、常见问题
1、如果组件定义方法的时候有参数,在使用组件的时候传入的方法必须写上参数
例如下面的value是不能省略的,即使你不用,否则会报错
Switch(value: true, onChanged: (value) {}),
2、安卓模拟器一个有用的快捷键
摇一摇:CMD+M(windows应该是ctrl+M)
3、折叠控制台不需要的输出
因为控制台经常输出一些调试信息,会刷掉我们自己打印的信息,我们可以把这些信息折叠掉,仅限于基于JetBrains的IDE,例如Webstorm、Android Studio,具体操作如下,以 E/FrameEvents 开头的日志为例子。
折叠之前
选中要折叠的字符串 – 点击 “像这样折叠行” (没有设置中文的可以参考英文的菜单)
设置之后,相关的日志就回折叠起来,自己打印的日志就很清晰了,我折叠了相关的关键字之后的效果
4、iOS真机调试出现下面的错误
(lldb) warning: libobjc.A.dylib is being read from process memory. This indicates that LLDB could not find the on-disk shared cache for this device. This will likely reduce debugging performance.
解决方案,删除下面的文件之后重新使用xcode运行后,再使用Android Studio打开运行就可以了
rm -r ~/Library/Developer/Xcode/iOS\ DeviceSupport
解决方案参考原文。
**5、打的release包重新打开app有时候会白屏
解决方案以及原因请查看stackoverflow:https://stackoverflow.com/questions/72565943/pageview-homepage-sometimes-does-not-load-on-startup-in-release-mode
延迟200ms调用runApp方法,是因为 MediaQuery.of(context).size.height returns 0 on startup in release mode,查看Github issue。
6、页面适配问题,保证所有设备表现一致
目前主要开发手机端的app,设计稿一般是px为单位的,如果需要所有的设备都保持一致就需要修改为相对单位,根据设备的宽度得到实际的值,下面就以375的设计稿为例:
import 'package:get/get.dart';
// 屏幕宽度,这里的Get是使用了getx这个库
// Get.width = (ui.window.physicalSize / ui.window.devicePixelRatio).width
// physicalSize对应的Size对象可以直接除,是因为重写了operator,可看源码
double screenWidth = Get.width;
// 以375为基准的缩放比例
double scaleX = screenWidth / 375;
// 将像素值转换以375为基准的值(所有像素值都用该函数转换)
px2dp(double px) {
return px * scaleX;
}
// 通过px2dp处理后的单位就是相对单位了;
7、在项目中使用多个pub下载依赖
按照常规来讲,flutter只可以在一个pub仓库下载依赖,如果我们有私有的组件托管在公司内部搭建的pub仓库,我们就需要将全局的PUB_HOSTED_URL修改为私有的仓库的地址,然后在私有仓库的上游代理公开的pub库。
如果本地搭建的pub仓库服务器带宽不够的时候,我们想公有的包直接在公有的pub下载,私有的包在私有的pub下载来加速下载,可以借助flutter的包管理策略,将私有包手动指定包的路径。
dependencies:
cupertino_icons: ^1.0.2
base_net:
hosted: http://flutter-pub.yourdomain.com
version: ^0.0.2
这样包base_net包就会去配置的路径下载,这里的 flutter-pub.yourdomain.com
要走域名解析,如果是配置的本地hosts,依赖处理会有问题。
8、webview中加载了http的链接在安卓上报错
报错的信息如下:
WebView showing ERR_CLEARTEXT_NOT_PERMITTED although site is HTTPS
我们只需要修改下安卓的配置,让它支持http访问即可,修改 android/app/src/main/AndroidManifest.xml
文件,在application节点上添加下面的属性即可:
<application
....
android:usesCleartextTraffic="true"
....>
但是通过修改配置强行加载http的链接明显不安全的,最好就是全站使用https,更多的解决方案可以查看下方的参考文章。
官方说明:Android 开发者 | Android Developers
参考文章:Android 8: Cleartext HTTP traffic not permitted – Stack Overflow
四、常用插件
1、功能性组件
- 从服务器获取数据 dio
- 轻量强大的状态管理库 getx
- webview的flutter实现 webview_flutter
- 调用照相机 camera
- 二维码扫描识别 qr_code_scanner
- 下拉刷新上拉加载 pull_to_refresh
- 调用系统日历或展示日历组件 device_calendar
- 展示lottie动画 lottie
- 访问和缓存网络图片(支持加载过程占位) cached_network_image
- 获取app package的信息(加plus是因为原插件不维护了) package_info_plus
- 获取设备的信息(加plus是因为原插件不维护了) device_info_plus
- 获取网络的状态(加plus是因为原插件不维护了) connectivity_plus
- 保持屏幕唤醒 wakelock
- 神策数据埋点 sensors_analytics_flutter_plugin
- 获取app的一些文件路径(临时文件夹、数据文件夹等) path_provider
- 简单的kv持久化存储(可以存储本地设置等) shared_preferences
- 列表左滑右滑操作 flutter_slidable
- 打开软件自己的设置页面 app_settings
- 自动调整文本以完美拟合其边界 auto_size_text
- 一个弹奏插件,用于播放多个音频文件 audioplayers
2、UI组件
- svg工具和组件 flutter_svg
- 页面loading组件 flutter_easyloading
- 模糊进度条插件 flutter_spinkit
- 透明图片,一般作为占位符 transparent_image
- 小提示toast插件 fluttertoast
- 在flutter中使用markdown展示内容 flutter_markdown