Flutter新手开发问题和记录

872 阅读2分钟

说明: $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来渲染指示器

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

image.png|400

实现如下:

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

exit(0)

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

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

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

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

image.png|200image.png|200

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的缓存也就没有了

20230310180125_6463e2.png

注意:要是你在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);

三、常见问题

1、如果组件定义方法的时候有参数,在使用组件的时候传入的方法必须写上参数,例如下面的value是不能省略的,即使你不用

Switch(value: true, onChanged: (value) {}),

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

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

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

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

折叠之前

image.png|500

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

image.png|500

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

image.png|700

四、常用插件