iOS分支技能 - Flutter入门技巧汇总(2025-03-21更新)

302 阅读7分钟
  • Expanded只能用在Flex组件中,比如Row、Column。在Row中,子组件会横向拓展;在Column中,子组件会纵向拓展。

  • 如果需要使Flex组件中的内容统一大小,可使用Intrinsic组件。在Row中使用IntrinsicHeight保持高度相同;在Column中使用IntrinsicWidth保持宽度相同。注意,使用这个组件会有额外的性能开销。

  • 需要缩放组件时可以使用Transform.scale,但并不会真的影响布局,实际尺寸没有变;如果想要改变组件尺寸并达到缩放效果,可以使用FittedBox并结合SizedBox

  • Text组件如果文本过长可以设置overFlow属性进行截断,但同时需要外层包裹Flexible组件;或者也可以使用Expanded组件,这也意味着Expanded外层需要是Flex才行。

  • 文本带背景色的情况下,同时还要支持长文本自动折行,如果使用Expended组件,背景色区域会无限拉伸,不能适应文本区域。可以使用Flexible伸缩组件来包装文本,放在Row组件中,同时设置其属性fit值为FlexFit.loose。这样不论文本是否折行,背景区域始终与文本区域保持一致。

  • 在Stack中使用Positioned组件可以指定位置。

  • 实现文本两端对齐:如果只包含半角字符,比如英文和数字,可以使用TextAlign.justify。如果包含全角字符,比如中文,则需要根据容器大小和字号,计算每个字符间距,设置Text组件的letterSpacing参数。

  • 使用permission_handler如果遇到获取的权限状态不对/没有显示授权弹框的问题,需要在podfile中配置相关的宏,pub.dev上也没明说,很坑。

    config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
      '$(inherited)',
      'PERMISSION_CAMERA=1',
      'PERMISSION_PHOTOS=1',
      'PERMISSION_LOCATION=1',
      'PERMISSION_NOTIFICATIONS=1',
      'PERMISSION_APP_TRACKING_TRANSPARENCY=1'
    ]
    
  • 使用Get.arguments获取参数时,建议放在onInit()onReady()中执行。如果过早执行(比如在logic的构造函数中)可能会获取不到。

  • 滑动组件不设置scrollDirection则无法滑动,且内容会被拉伸

  • 要模态显示一个页面,如果使用bottomSheet方式,页面顶部没有安全区,导航栏会和状态栏重叠;可以使用push / Get.to的方式,只是需要把过渡动画自定义成从下到上滑动就可以了

  • 页面导航的一些技巧:

    1. Get.to / Get.toNamed,推进一个页面,类似于push
    2. Get.back,返回到上一个页面,类似于pop
    3. Get.close,返回指定数量的层级
    4. Get.until,返回到指定页面,类似于popTo,方法传参判断是否是首页,可以实现返回到根视图的效果,类似于popToRoot
    5. Get.offAllNamed,设置新的根视图,并清空之前推入的页面
  • 在 iOS17 中输入框结尾字符会有选中效果,如果要去掉这个选中框,可以单独对TextField组件设置主题配色:

    Theme(
      data: ThemeData(
        textSelectionTheme: const TextSelectionThemeData(
          selectionColor: Colorful.clear,
        ),
      ),
      child: textField,
    )
    
  • 下载插件时,国内访问很慢,可以使用镜像https://pub.flutter-io.cn,需要配置~/.zshrc文件,添加:

    export PUB_HOSTED_URL=https://pub.flutter-io.cn
    export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
    

    另外,Cocoapods也可以替换Github源,在~/.gitconfig文件,添加:

    [url "国内的源"]
        insteadOf = https://github.com
    
  • 编译安卓项目时,如果没有翻墙,gradle执行会很慢/报错。可以设置国内镜像地址就会快很多:

    1. 修改文件你的flutter开发包/packages/flutter_tools/gradle/flutter.gradle(在3.16.0上是这个文件你的flutter开发包/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy
    buildscript {
        repositories {
            // google()
            // mavenCentral()
            // 添加这些
            maven { url 'https://maven.aliyun.com/repository/public' }
            maven { url 'https://maven.aliyun.com/repository/google' }
            maven { url 'https://maven.aliyun.com/repository/jcenter' }
            maven { url 'https://maven.aliyun.com/repository/central' }
            maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
        }
        dependencies {
            ...
        }
    }
    // ...
    // 修改默认的主机地址为
    private static final String DEFAULT_MAVEN_HOST = "https://storage.flutter-io.cn";
    
    1. 修改文件你的flutter开发包/packages/flutter_tools/gradle/resolve_dependencies.gradle
    // 修改默认地址为
    String storageUrl = System.getenv('FLUTTER_STORAGE_BASE_URL') ?: "https://storage.flutter-io.cn"
    
    repositories {
        // google()
        // mavenCentral()
        maven {
            url "$storageUrl/download.flutter.io"
        }
    }
    
    1. 修改你项目中的build.gradle文件
    buildscript {
        ...
        repositories {
            // google()
            // mavenCentral()
            maven { url 'https://maven.aliyun.com/repository/public' }
            maven { url 'https://maven.aliyun.com/repository/google' }
            maven { url 'https://maven.aliyun.com/repository/jcenter' }
            maven { url 'https://maven.aliyun.com/repository/central' }
            maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
        }
        ...
    }
    
    allprojects {
        repositories {
            // google()
            // mavenCentral()
            maven { url 'https://maven.aliyun.com/repository/public' }
            maven { url 'https://maven.aliyun.com/repository/google' }
            maven { url 'https://maven.aliyun.com/repository/jcenter' }
            maven { url 'https://maven.aliyun.com/repository/central' }
            maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
        }
    }
    
  • 使用GridView组件需要指定大小,如果不指定,需要设置shrinkWrap参数为true。注意,这个参数会有比较大的性能开销。

  • 目前flutter中显示emoji表情符号会导致整个页面的文字被加粗,暂时没有解决办法。(在最新版3.16.0上这个问题已经消失)

  • 目前flutter中显示超长图片时会被压扁,图片失真。你可以先获取图片的尺寸,再强制固定图片组件的大小,同时将图片的fit属性设置为BoxFit.fill

  • 升级3.16.0后:

    1. 设置状态栏颜色失败:如果你的iOS工程配置了UIViewControllerBasedStatusBarAppearance,那么需要去掉。
    2. iOS12设备无法真机调试(3.16.5已修复这个问题)
    3. 在playcover上可能会无法运行
    4. 个别插件可能存在不兼容的情况,编译会报错
    5. PopScope在iOS上无法拦截用户返回手势
  • 用flutter开发iOS扩展(3.16.0+支持)可以参考官方的文档:链接。但需要注意的是,不同种类的扩展,有不同的内存限制要求,超出限制会导致扩展程序崩溃,如果你需要在扩展中嵌入flutter页面,那么引擎将会产生很大的开销。建议页面部分由原生实现,flutter只提供基础逻辑等服务。

    如果要上架应用商店,可能会在上传时遇到错误,主要是因为苹果不允许在扩展中使用动态库,需要把扩展所用的Flutter.frameworkApp.framework放在主应用包的Frameworks下。同时需在工程中对应的Target设置EMBEDDED_CONTENT_CONTAINS_SWIFTALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES为NO,LD_RUNPATH_SEARCH_PATHS动态库搜索路径指向主应用包中@executable_path/../../Frameworks,确保.appex的编译产物中没有Frameworks文件夹。

    另外,如果你的主应用也使用了flutter,那么就会和扩展中使用的flutter动态库产生命名冲突。可以通过修改扩展使用的framework名称的方式来解决问题。具体做法是将framework中Info.plist文件中的BundleId进行修改,使用codesign --remove-signature命令移除原有的签名,再重新签名。这样处理后上传就没问题了。

  • 使用runtimeType()方法时要小心,它可能不会返回预期的结果。特别是打包时开启了混淆参数后,命名会被混淆,如果你通过运行时获取类型名称,会和你代码中的类型命名不同。

  • 有时候需要获取组件的实际尺寸,在渲染到屏幕上后更新。由于渲染2次,样式不同会出现闪动的问题。 小技巧是,可以在第一次渲染时使用Opacity组件将其设置为透明,此时依然能够获取到尺寸,第二次更新时再完全显示,这样就不会有闪动问题了。 更好的解决方式是继承SingleChildLayoutDelegate类自己处理布局。

  • 使用parse方法需要注意,如果不是预期的格式将会直接导致崩溃,当然你也可以使用try-catch捕获异常,但这样始终不太优雅。这个方法应该废弃,建议改用更安全的tryParse方法。

  • 有时候需要根据条件在界面上排列一些组件,可以直接在数组声明中使用if-else语句,比如:

Column(
  children: [
    if (show) ...{
      Text("根据条件显示"),
    } else ...{
      Text("其他"),
    },
    // 也可以将list中的元素展开并添加到children中
    ...list,
    // 或者遍历数组进行添加
    for (final e in list) ...{
      Text(e),
    },
  ],
);

当然,这个语法糖也同样适用于一般的数组和字典:

final list = [
  123,
  if (add) ...{
    456,
  },
];

final map = {
  "aaa": 123,
  if (add) ...{
    "bbb": 456,
  },
};
  • 同一字号显示中英文时,大小不一致,这是由于中英文字体的上下两端留白不同(leading)。可以设置StructStyleforceStrutHeight属性为true就可以了。

  • 苹果系统显示的中文默认是用苹方字体,但是设置字重只有w400和w600有效。这个是flutter的问题,解决方案是设置TextStylefontFamilyFallbackfontFamily属性设置["PingFang SC"]

  • 使用StatelessWidget和GetBuilder实现一个能够多实例并存、可主动刷新的组件:使用GetBuilder管理刷新,通过外部传入GetxController控制逻辑,生成随机数作为tag。如果要使用Get生命周期,还需要通过Get.put(tag:)方法注入。一个在页面中嵌入组件的例子:

class FatherLogic extends GetxController {
  final son = SonLogic();
  
  @override
  void onClose() {
    // 不需要显式调用son的onClose方法,Get生命周期会自动管理,在页面退出时会自行调用
    super.onClose();
  }
}

class FatherWidget extends StatelessWidget {
  final logic = FatherLogic();

  FatherWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return GetBuilder<FatherLogic>(builder: (_) => SonWidget(controller: logic.son));
  }
}

class SonLogic extends GetxController {
  final tag = UniqueKey().toString();
  
  @override
  void onClose() {
    // 在这里释放你的资源
    super.onClose();
  }
}

class SonWidget extends StatelessWidget {
  final SonLogic logic;

  SonWidget({
    super.key,
    required SonLogic controller, // 通过外部持有,否则会有生命周期问题导致无法刷新
  }) : logic = controller {
    if (!Get.isRegistered<SonLogic>(tag: logic.tag)) {
      Get.put(logic, tag: logic.tag);
    }
  }

  @override
  Widget build(BuildContext context) {
    return GetBuilder<SonLogic>(tag: logic.tag, builder: (_) => Container());
  }
}

如果是通过导航器navigator推入的页面,要实现多实例共存、处理被动刷新的情况,在GetBuilder的didUpdateWidget回调方法中释放资源,一个例子:

class ExampleLogic extends GetxController {
  final tag = UniqueKey().toString();
  
  @override
  void onClose() {
    // 在这里释放你的资源
    super.onClose();
  }
}

class ExamplePage extends StatelessWidget {
  final logic = ExampleLogic();

  ExampleWidget({super.key}) {
    if (!Get.isRegistered<ExampleLogic>(tag: logic.tag)) {
      Get.put(logic, tag: logic.tag);
    }
  }

  @override
  Widget build(BuildContext context) {
    return GetBuilder<ExampleLogic>(
      tag: logic.tag,
      builder: (_) => Container(),
      didUpdateWidget: (old, _) {
        // 处理被强制刷新的情况。这里判断如果新旧controller的tag是一样的,说明con-
        // troller是指定的同一个实例,不需要手动释放,页面退出时会自动释放;如果不一
        // 样,说名是新的实例,需要手动释放旧的。
        if (Get.isRegistered<ExampleLogic>(tag: old.tag) && old.tag != logic.tag) {
          Get.delete(tag: old.tag);
        }
      },
    );
  }
}
  • 如果将Widget以参数形式传递给某个方法进行展示,页面退出时Widget相关资源可能不会释放,正确的做法是改为函数形式传参:
showSomeWidget(() => widgetBuilder(someParam));

千万不能这样写:

final widget = widgetBuilder(someParam);// 错误,组件提前创建了实例,仍然会导致不释放问题
showSomeWidget(() => widget);
  • math库建议使用别名。如果你在遍历数组时习惯使用变量名"e",那么某些情况下可能会意外的引用math中的e,而且能编译通过,很容易踩坑。
import 'dart:math' as math;