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、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

3、解决Gradle包下载慢

如果Flutter中包含了安卓应用程序,需要使用gradle打包,执行打包的时候会很慢,可以先把对应的gradle版本(对应的版本以及链接可以看项目中的配置)下载到本地或者上传到公司比较快的服务器上,修改代码中的分发url即可:

腾讯的镜像地址: https://mirrors.cloud.tencent.com/gradle/

2024-03-22-20240323000956540

二、常见实现

1、取消点击的水波效果

在iOS上一般不需要水波纹效果,修改主题就可以了

theme: ThemeData(
    highlightColor: Colors.transparent, 
    splashColor: Colors.transparent
),

2、修改Tabbar指示器的宽度为固定宽度

组件默认仅支持(和tab文本一样长、和tab一样长,两种方式)

  • 修改原生的UnderlineTabIndicator组件
  • 修改渲染长度的函数
  • 使用自定义的UnderlineTabIndicator来渲染指示器

目标效果,指示器是固定宽度的:

image.png

实现如下:

/// 可以固定宽度的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))  
              ]),  
        )
    )
}

展示效果:

image.png

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。

image.png

10、Container设置圆角之后,子元素超出没有隐藏

给Container设置:clipBehavior: Clip.hardEdge 即可,左侧图设置了,右侧图没有设置。

image.pngimage.png

11、通过ColorFiltered给图片替换颜色

效果 颜色 模式
原始图片 2024-03-11-20240311172630048 Colors.white BlendMode.srcIn
添加滤镜 2024-03-11-20240311172716174 const Color(0xff23135E).withOpacity(0.2) BlendMode.srcIn

代码如下

ColorFiltered(
    colorFilter: ColorFilter.mode(
        const Color(0xff23135E).withOpacity(0.2),
        BlendMode.srcIn,
    ),
    child: Image.asset(
        "${assetsImgNew}home_icon_expand@2x.png",
        fit: BoxFit.contain,
    ),
)

12、Flutter App清除缓存

就像浏览器一样,我们有时候需要清楚缓存来重新拉取一些数据。Flutter App的缓存数据存储在app的专属目录,我们获取到目录并清除里面的所有文件即可,首先我们要将 path_provider 加入到项目的依赖。

下面是安卓模拟器下面的缓存文件夹,可以看到HTTP Cache也在里面,我们删除后,webview的缓存也就没有了

20230310180125_6463e2.png

注意:要是你在cache文件夹中存储了应用程序的数据,就不能直接调用deleteSync删除了,而是需要列出文件夹后,排除不能删除的文件夹。

_clearCache() async {  
  // 获取缓存文件夹,iOS和安卓在插件底层已经封装了 
  final dir = await getTemporaryDirectory();  
  // 递归删除文件和文件夹即可  
  dir.deleteSync(recursive: true);  
}

13、在页面中使用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、获取颜色设置透明度的实际颜色

有时候我们需要使用一个颜色加上透明度,生成一个新的颜色。有下面两种实现方式:

  • 直接使用 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();

16、实现毛玻璃效果

在页面滚动的时候,可以对导航栏做毛玻璃而实现比较好的用户反馈,如下图所示:

20240310212515486

实现代码如下,Container中的内容就有一个模糊的效果:

ClipRect(
        child: BackdropFilter(
            filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
            child: Container(
                decoration: BoxDecoration(
                    color: Colors.white.withOpacity(0.4),
                ),
                child: Text(""),
            ),
        ),
    )

BackdropFilter用来设置模糊效果,BoxDecoration用来设置模糊的透明度;

17、打开苹果健康APP

需要下载一个包:url_launcher

import 'package:url_launcher/url_launcher_string.dart';

if (await canLaunchUrlString("x-apple-health://")) {
    launchUrlString("x-apple-health://");
    // 下面这个是不可以的
    // launchUrl(Uri.parse("x-apple-health://"));
}

18、dio作为请求库时使用Charles抓包

可以通过在代码中设置代理服务器来实现抓包,Charles里面展示的内容要比flutter devtools展示的网络请求详细一些,也可以使用 Charles 的高级特性(例如网络断点、本地Map等)。

// https://pub.dev/documentation/dio/latest/dio/Dio/httpClientAdapter.html
_dio.httpClientAdapter = IOHttpClientAdapter()  
    ..onHttpClientCreate = (client) {  
        client.findProxy = (uri) {  
            // 这里一定要使用IP地址,不可使用localhost或者127.0.0.1
            return 'PROXY 192.168.60.202:8888';  
        };  
    };

注意:

A) 如果需要使用https代理,需要在手机上安装Charles证书,可以查询相关的文档。

B) 如果你想同时使用VPN和Charles,因为VPN和Charles都会修改系统代理,所以需要修改手机代理到Charles代理,再到Charles中设置上游的代理(即VPN)。具体设置如下:

打开Charles,菜单:Proxy -> External Proxy Settings…

20240401145236185

9999是我的VPN的端口,整体的运行就成就会变成:
Mobile Phone(192.168.60.202:8888) -> Charles(127.0.0.1:9999) -> VPN

19、调用Apple授权时候获取用户的邮箱

20240422155253307

可以在AppleAuthProvider创建的时候设置授权的范围,代码如下所示

final appleProvider = AppleAuthProvider();
appleProvider.addScope("email");
appleProvider.addScope("name");
return await FirebaseAuth.instance.signInWithProvider(appleProvider);

提示获取邮箱只有首次登录的时候有效,如果要重新提示,需要将苹果的授权先删除后,重新授权可得,iOS设置如下:

Settings > Apple ID > Sign-In & Security > Sign-In with Apple

删除已经授权的App,在打开App重新使用Apple授权时就会弹出获取邮箱的选项;

三、常见问题

1、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

2、安卓模拟器一个有用的快捷键

摇一摇:CMD+M(windows应该是ctrl+M)

3、折叠控制台不需要的输出

因为控制台经常输出一些调试信息,会刷掉我们自己打印的信息,我们可以把这些信息折叠掉,仅限于基于JetBrains的IDE,例如Webstorm、Android Studio,具体操作如下,以 E/FrameEvents 开头的日志为例子。

折叠之前

image.png

选中要折叠的字符串 – 点击 “像这样折叠行” (没有设置中文的可以参考英文的菜单)

image.png

设置之后,相关的日志就回折叠起来,自己打印的日志就很清晰了,我折叠了相关的关键字之后的效果

image.png

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处理后的单位就是相对单位了;


//  如果在Get.context没有的时候上述的代码会报错
/// Note: Usually no need to pass the context param manually  
/// Only when Get.context has not been generated, it needs to be passed manually  
double px2dp(double px, [BuildContext? context]) {  
  if (context == null && Get.context == null) {  
    throw AssertionError("Unable to find context to calculate device width");  
  }  
  
  double screenWidth = 0;  
  if (Get.context != null) {  
    screenWidth = Get.width;  
  } else {  
    screenWidth = MediaQuery.of(context!).size.width;  
  }  
  double scaleX = screenWidth / basicWidth;  
  
  return px * scaleX;  
}

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,依赖处理会有问题。

四、常用插件

1、功能性组件

2、UI组件

留下回复