阅读视图

发现新文章,点击刷新页面。

Flutter ——flutter_screenutil 屏幕适配

flutter_screenutil 是 Flutter 生态中最常用的屏幕适配库,核心原理是基于设计稿尺寸做等比例缩放,支持宽度 / 高度适配、字体适配、边框适配等,能快速解决不同设备的 UI 适配问题。下面从安装配置、核心用法、高级技巧、避坑指南四个维度全面解析。

一、基础准备:安装与初始化

1. 安装依赖

pubspec.yaml 中添加最新版本依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_screenutil: ^5.9.0 # 推荐使用最新稳定版

执行安装命令:

flutter pub get

2. 全局初始化(关键步骤)

必须在 APP 入口处初始化,指定设计稿的基准尺寸(通常以 iPhone 14/15 的 390×844 或 iPhone 13 的 375×812 为例):

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    // 初始化ScreenUtil
    return ScreenUtilInit(
      // 设计稿尺寸(单位:px),根据你的UI设计稿修改
      designSize: const Size(390, 844), 
      // 适配规则:默认宽度适配,也可设为BoxConstraints.tightFor(height: ...)
      minTextAdapt: true, // 开启字体适配
      splitScreenMode: true, // 支持分屏适配
      builder: (context, child) {
        return MaterialApp(
          title: 'ScreenUtil示例',
          theme: ThemeData(primarySwatch: Colors.blue),
          home: child, // 传递子页面
          builder: (context, widget) {
            // 解决字体缩放导致的适配问题
            ScreenUtil.init(context);
            return MediaQuery(
              data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
              child: widget!,
            );
          },
        );
      },
      child: const HomePage(), // 你的首页
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('屏幕适配示例')),
      body: const Center(child: AdaptingWidget()),
    );
  }
}

二、核心用法:适配各种 UI 属性

flutter_screenutil 提供了两种常用适配方式:扩展方法(推荐)和静态方法,下面是最常用的适配场景:

1. 尺寸适配(宽 / 高 / 边距 / 圆角)

class AdaptingWidget extends StatelessWidget {
  const AdaptingWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Padding(
      // 边距适配:设计稿上的20px → 自动缩放
      padding: EdgeInsets.all(20.w), 
      child: Container(
        // 宽度适配:设计稿上的300px
        width: 300.w, 
        // 高度适配:设计稿上的150px
        height: 150.h, 
        // 圆角适配:设计稿上的16px
        decoration: BoxDecoration(
          color: Colors.blue,
          borderRadius: BorderRadius.circular(16.r), 
          // 边框宽度适配:设计稿上的2px
          border: Border.all(width: 2.w, color: Colors.white), 
        ),
        // 内边距适配
        child: Padding(
          padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.h),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // 字体适配:设计稿上的18px
              Text(
                '适配后的文字',
                style: TextStyle(fontSize: 18.sp), 
              ),
              SizedBox(height: 10.h), // 间距适配
              // 图标大小适配
              Icon(Icons.phone, size: 24.sp, color: Colors.white),
            ],
          ),
        ),
      ),
    );
  }
}

2. 核心扩展方法说明

扩展方法 作用 示例
num.w 宽度适配(等比例缩放) 300.w → 设计稿 300px 宽度适配到当前设备
num.h 高度适配(等比例缩放) 150.h → 设计稿 150px 高度适配到当前设备
num.r 圆角 / 半径适配(优先按宽度适配) 16.r → 设计稿 16px 圆角适配
num.sp 字体大小适配(支持最小字体限制) 18.sp → 设计稿 18px 字体适配
num.sw 屏幕宽度占比(0-1) 0.8.sw → 占屏幕宽度 80%
num.sh 屏幕高度占比(0-1) 0.5.sh → 占屏幕高度 50%

3. 静态方法(无上下文时使用)

如果在工具类、初始化代码等无context的场景下,可使用静态方法:

// 获取适配后的尺寸
double width = ScreenUtil().setWidth(300);
double height = ScreenUtil().setHeight(150);
double fontSize = ScreenUtil().setSp(18);

// 获取屏幕信息
double screenWidth = ScreenUtil().screenWidth; // 设备实际宽度
double screenHeight = ScreenUtil().screenHeight; // 设备实际高度
double pixelRatio = ScreenUtil().pixelRatio; // 设备像素比

三、高级技巧:适配优化与特殊场景

1. 字体适配优化

默认的sp适配可能在小屏设备上字体过小,可设置最小字体限制:

// 方式1:全局初始化时设置
ScreenUtilInit(
  designSize: const Size(390, 844),
  minTextAdapt: true,
  // 自定义字体适配规则
  builder: (context, child) {
    // 设置最小字体为12sp
    ScreenUtil.init(context, minTextSize: 12);
    return child!;
  },
  child: const HomePage(),
);

// 方式2:单独设置字体(强制最小字体)
Text(
  '最小12sp的文字',
  style: TextStyle(
    fontSize: 10.spMin, // 即使适配后小于12sp,也显示12sp
  ),
);

2. 横竖屏适配

处理横竖屏切换时的适配更新:

class OrientationPage extends StatefulWidget {
  const OrientationPage({super.key});

  @override
  State<OrientationPage> createState() => _OrientationPageState();
}

class _OrientationPageState extends State<OrientationPage> {
  @override
  Widget build(BuildContext context) {
    // 横竖屏切换时重新初始化
    ScreenUtil.init(context);
    return Scaffold(
      body: Center(
        child: Text(
          '屏幕宽度:${ScreenUtil().screenWidth.w}\n屏幕高度:${ScreenUtil().screenHeight.h}',
          style: TextStyle(fontSize: 16.sp),
        ),
      ),
    );
  }

  // 监听横竖屏切换
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final orientation = MediaQuery.of(context).orientation;
    print('当前方向:$orientation');
  }
}

3. ListView/GridView 适配

避免滚动组件因适配导致的布局异常:

// ListView 适配示例
ListView.builder(
  // 高度适配
  itemExtent: 80.h, 
  itemCount: 10,
  itemBuilder: (context, index) {
    return ListTile(
      leading: Icon(Icons.person, size: 32.sp),
      title: Text('列表项 $index', style: TextStyle(fontSize: 16.sp)),
      subtitle: Text('副标题', style: TextStyle(fontSize: 12.sp)),
      contentPadding: EdgeInsets.symmetric(horizontal: 16.w),
    );
  },
);

// GridView 适配示例
GridView.count(
  crossAxisCount: 2, // 列数
  crossAxisSpacing: 10.w, // 列间距
  mainAxisSpacing: 10.h, // 行间距
  childAspectRatio: 16/9, // 宽高比(无需适配)
  padding: EdgeInsets.all(10.w),
  children: List.generate(4, (index) {
    return Container(
      width: (ScreenUtil().screenWidth - 30.w) / 2, // 手动计算宽度
      height: 100.h,
      color: Colors.grey,
    );
  }),
);

4. 自定义适配规则

针对特殊 UI(如固定宽高比的图片),可混合使用sw/sh

// 保持16:9比例的图片
Container(
  width: 0.9.sw, // 占屏幕宽度90%
  height: (0.9.sw) * 9 / 16, // 按16:9计算高度
  child: Image.network(
    'https://example.com/image.jpg',
    fit: BoxFit.cover,
  ),
);

四、避坑指南:常见问题与解决方案

1. 适配后布局变形

问题 解决方案
部分 UI 元素过大 / 过小 检查设计稿尺寸是否设置正确(如把 dp 当成 px);优先用w/h而非sw/sh
字体缩放导致布局错乱 MaterialAppbuilder中设置textScaleFactor: 1.0,禁用系统字体缩放
圆角适配不一致 统一使用r而非w/hr会自动适配宽高比

2. 无上下文时初始化失败

  • 错误场景:在main函数中直接调用ScreenUtil().setWidth(300)

  • 解决方案:先初始化WidgetsFlutterBinding,再手动初始化ScreenUtil

    void main() async {
      WidgetsFlutterBinding.ensureInitialized();
      // 手动初始化(指定设计稿尺寸)
      await ScreenUtil.ensureScreenSize();
      ScreenUtil.init(
        BoxConstraints(
          maxWidth: WidgetsBinding.instance.window.physicalSize.width / WidgetsBinding.instance.window.devicePixelRatio,
          maxHeight: WidgetsBinding.instance.window.physicalSize.height / WidgetsBinding.instance.window.devicePixelRatio,
        ),
        designSize: const Size(390, 844),
      );
      runApp(const MyApp());
    }
    

3. 分屏 / 折叠屏适配

  • 开启splitScreenMode: true(初始化时);

  • 避免使用固定的sh(屏幕高度占比),优先用MediaQuery获取可用高度:

    double availableHeight = MediaQuery.of(context).size.height - MediaQuery.of(context).padding.top - kToolbarHeight;
    

总结

  1. 核心流程:先在ScreenUtilInit中指定设计稿尺寸,再通过w/h/r/sp扩展方法实现 UI 适配,覆盖 90% 的适配场景;
  2. 最佳实践:字体用sp(开启最小字体限制)、尺寸用w/h、圆角用r、占比用sw/sh
  3. 避坑要点:禁用系统字体缩放、横竖屏切换时重新初始化、无上下文时先初始化WidgetsFlutterBinding

如果需要针对某一特殊场景(如适配折叠屏、自定义设计稿尺寸、国际化多语言适配)的代码示例,我可以进一步补充。

Flutter ——device_info_plus详解

device_info_plus 是 Flutter 生态中最权威的设备信息获取库(官方维护),替代了已废弃的 device_info,支持 Android、iOS、Web、Windows、macOS、Linux、Android TV、watchOS 等几乎所有 Flutter 支持的平台。 库地址 pub.dev/packages/de…

安装

  1. 在项目的 pubspec.yaml 中添加依赖:
dependencies:
  flutter:
    sdk: flutter
    device_info_plus: ^12.3.0 # 建议使用最新版本
  1. 执行命令安装:
flutter pub get

一、核心基础:包架构与初始化

1. 核心类与初始化

device_info_plus 的核心是 DeviceInfoPlugin 单例,通过它可以获取不同平台的设备信息对象:

import 'package:device_info_plus/device_info_plus.dart';

// 1. 创建插件实例(全局唯一)
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();

// 2. 初始化(非必需,但异步操作建议在WidgetsFlutterBinding初始化后执行)
void initDeviceInfo() async {
  WidgetsFlutterBinding.ensureInitialized(); // 无上下文时必须调用
  // 后续获取设备信息的操作...
}

2. 平台与信息类对应表

平台 信息获取方法 核心信息类 适用场景
Android await deviceInfoPlugin.androidInfo AndroidDeviceInfo 获取安卓设备型号、系统版本、厂商等
iOS await deviceInfoPlugin.iosInfo IosDeviceInfo 获取 iOS 版本、设备型号、UUID 等
Web await deviceInfoPlugin.webBrowserInfo WebBrowserInfo 获取浏览器、操作系统、用户代理等
Windows await deviceInfoPlugin.windowsInfo WindowsDeviceInfo 获取 Windows 系统版本、处理器、设备名等
macOS await deviceInfoPlugin.macOsInfo MacOsDeviceInfo 获取 Mac 系统版本、机型、内存等
Linux await deviceInfoPlugin.linuxInfo LinuxDeviceInfo 获取 Linux 发行版、内核版本等
Android TV await deviceInfoPlugin.androidTvInfo AndroidTvDeviceInfo 区分安卓电视 / 普通安卓设备
watchOS await deviceInfoPlugin.watchOsInfo WatchOsDeviceInfo 获取苹果手表信息
Wear OS await deviceInfoPlugin.wearOsInfo WearOsDeviceInfo 获取安卓手表信息
Fuchsia await deviceInfoPlugin.fuchsiaInfo FuchsiaDeviceInfo 适配 Fuchsia 系统(暂未普及)

二、全平台核心 API 详解

1. Android 平台(AndroidDeviceInfo

最常用的属性(覆盖 90% 场景):

AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo;
print('=== Android 设备信息 ===');
print('设备型号:${androidInfo.model}'); // 例:Pixel 8 Pro、Redmi K70
print('系统版本:${androidInfo.version.release}'); // 例:14、13
print('API等级:${androidInfo.version.sdkInt}'); // 例:34(Android 14)、33(Android 13)
print('厂商:${androidInfo.manufacturer}'); // 例:Google、Xiaomi、Huawei
print('品牌:${androidInfo.brand}'); // 例:google、xiaomi、huawei
print('设备ID:${androidInfo.id}'); // 设备唯一标识(Android 10+ 无权限获取)
print('产品名:${androidInfo.product}'); // 例:cheetah(Pixel 8 Pro)
print('是否真机:${androidInfo.isPhysicalDevice}'); // true=真机,false=模拟器
print('指纹:${androidInfo.fingerprint}'); // 系统指纹(用于版本判断)
print('硬件名称:${androidInfo.hardware}'); // 例:qcom(高通)
print('支持的ABIs:${androidInfo.supportedAbis}'); // 支持的CPU架构(armeabi-v7a、arm64-v8a等)

2. iOS 平台(IosDeviceInfo

IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;
print('=== iOS 设备信息 ===');
print('设备名称:${iosInfo.name}'); // 例:iPhone 15 Pro
print('系统版本:${iosInfo.systemVersion}'); // 例:17.2
print('设备型号:${iosInfo.model}'); // 例:iPhone
print('具体机型:${iosInfo.utsname.machine}'); // 例:iPhone16,1(对应iPhone 15 Pro)
print('是否真机:${iosInfo.isPhysicalDevice}'); // true=真机,false=模拟器
print('设备UUID:${iosInfo.identifierForVendor}'); // 应用厂商唯一标识(卸载重装会变)
print('系统名称:${iosInfo.systemName}'); // 例:iOS、iPadOS
print('电池电量:${iosInfo.batteryLevel}'); // 0.0-1.0(需权限)
print('是否充电:${iosInfo.isCharging}');

3. Web 平台(WebBrowserInfo

WebBrowserInfo webInfo = await deviceInfoPlugin.webBrowserInfo;
print('=== Web 浏览器信息 ===');
print('浏览器名称:${webInfo.browserName}'); // BrowserName.chrome/edge/firefox/safari等
print('浏览器版本:${webInfo.browserVersion}'); // 例:120.0.0.0
print('操作系统:${webInfo.platform}'); // 例:Windows、macOS、Android
print('用户代理:${webInfo.userAgent}'); // 完整UA字符串
print('语言:${webInfo.language}'); // 例:zh-CN
print('是否移动设备:${webInfo.isMobile}'); // true/false

4. Windows 平台(WindowsDeviceInfo

WindowsDeviceInfo windowsInfo = await deviceInfoPlugin.windowsInfo;
print('=== Windows 设备信息 ===');
print('设备名称:${windowsInfo.computerName}'); // 电脑名
print('系统版本:${windowsInfo.systemVersion}'); // 例:10.0.22631
print('处理器名称:${windowsInfo.processorName}'); // 例:Intel(R) Core(TM) i7-12700H
print('内存大小(MB):${windowsInfo.memorySizeInMb}');
print('系统架构:${windowsInfo.systemArchitecture}'); // 例:X64
print('构建号:${windowsInfo.buildNumber}'); // 例:22631

5. macOS 平台(MacOsDeviceInfo

MacOsDeviceInfo macInfo = await deviceInfoPlugin.macOsInfo;
print('=== macOS 设备信息 ===');
print('系统版本:${macInfo.osVersion}'); // 例:14.1.1
print('机型:${macInfo.model}'); // 例:MacBook Pro
print('处理器:${macInfo.processorName}'); // 例:Apple M3 Pro
print('内存大小(GB):${macInfo.memorySizeInGb}');
print('UUID:${macInfo.systemGUID}'); // 系统唯一标识

三、高级用法:封装与实战场景

1. 封装通用工具类(推荐)

将设备信息获取逻辑封装为全局工具类,避免重复代码:

import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class DeviceInfoUtil {
  static final DeviceInfoPlugin _plugin = DeviceInfoPlugin();
  static bool _isInitialized = false;

  // 初始化(全局调用一次即可)
  static Future<void> init() async {
    if (!_isInitialized) {
      WidgetsFlutterBinding.ensureInitialized();
      _isInitialized = true;
    }
  }

  // 通用:获取设备类型(手机/平板/PC/浏览器)
  static Future<String> getDeviceType() async {
    if (kIsWeb) {
      return "Web浏览器";
    }
    // defaultTargetPlatform 获取 设置的类型 
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
        final info = await _plugin.androidInfo;
        return info.model.contains("Tablet") ? "安卓平板" : "安卓手机";
      case TargetPlatform.iOS:
        final info = await _plugin.iosInfo;
        return info.model.contains("iPad") ? "iPad" : "iPhone";
      case TargetPlatform.windows:
        return "Windows PC";
      case TargetPlatform.macOS:
        return "Mac电脑";
      case TargetPlatform.linux:
        return "Linux设备";
      default:
        return "未知设备";
    }
  }

  // 通用:获取系统版本(如 "Android 14"、"iOS 17.2")
  static Future<String> getSystemVersion() async {
    if (kIsWeb) {
      final info = await _plugin.webBrowserInfo;
      return info.platform;
    }
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
        final info = await _plugin.androidInfo;
        return "Android ${info.version.release}";
      case TargetPlatform.iOS:
        final info = await _plugin.iosInfo;
        return "iOS ${info.systemVersion}";
      case TargetPlatform.windows:
        final info = await _plugin.windowsInfo;
        return "Windows ${info.systemVersion}";
      case TargetPlatform.macOS:
        final info = await _plugin.macOsInfo;
        return "macOS ${info.osVersion}";
      default:
        return "未知系统";
    }
  }

  // Android专属:判断是否为鸿蒙系统(鸿蒙伪装成Android)
  static Future<bool> isHarmonyOS() async {
    if (defaultTargetPlatform != TargetPlatform.android) return false;
    final info = await _plugin.androidInfo;
    // 鸿蒙系统的fingerprint或brand会包含harmony相关字段
    return info.fingerprint.contains("harmony") || info.brand.contains("Harmony");
  }
}

// 使用示例
void main() async {
  await DeviceInfoUtil.init();
  String deviceType = await DeviceInfoUtil.getDeviceType();
  String sysVersion = await DeviceInfoUtil.getSystemVersion();
  bool isHarmony = await DeviceInfoUtil.isHarmonyOS();
  print("设备类型:$deviceType");
  print("系统版本:$sysVersion");
  print("是否鸿蒙:$isHarmony");
}

2. 实战场景 1:版本适配

根据系统版本执行不同逻辑(如 Android 权限适配):

// Android 13+ 通知权限适配
Future<void> requestNotificationPermission() async {
  final androidInfo = await deviceInfoPlugin.androidInfo;
  if (androidInfo.version.sdkInt >= 33) { // Android 13 (API 33)
    // 请求POST_NOTIFICATIONS权限
    final status = await Permission.notification.request();
    if (status.isGranted) {
      print("通知权限已授予");
    }
  }
}

3. 实战场景 2:埋点统计

收集设备信息用于用户行为分析(合规前提下):

// 生成埋点用的设备信息JSON
Future<Map<String, dynamic>> getTrackDeviceInfo() async {
  Map<String, dynamic> info = {
    "platform": kIsWeb ? "web" : defaultTargetPlatform.name,
    "device_type": await DeviceInfoUtil.getDeviceType(),
    "system_version": await DeviceInfoUtil.getSystemVersion(),
    "is_emulator": false,
  };

  if (defaultTargetPlatform == TargetPlatform.android) {
    final androidInfo = await deviceInfoPlugin.androidInfo;
    info["manufacturer"] = androidInfo.manufacturer;
    info["model"] = androidInfo.model;
    info["is_emulator"] = !androidInfo.isPhysicalDevice;
  } else if (defaultTargetPlatform == TargetPlatform.iOS) {
    final iosInfo = await deviceInfoPlugin.iosInfo;
    info["model"] = iosInfo.utsname.machine;
    info["is_emulator"] = !iosInfo.isPhysicalDevice;
  }

  return info;
}

四、避坑指南:常见问题与解决方案

1. 权限问题

问题 解决方案
Android 10+ 无法获取androidId 改用identifierForVendor(iOS)或自定义 UUID(如flutter_secure_storage存储)
iOS 获取电池信息失败 需要在Info.plist中添加NSBatteryUsageDescription说明用途
Web 无法获取精准设备型号 Web 受限于浏览器沙箱,只能通过 UA 解析(可结合第三方库ua_parser

2. 兼容性问题

问题 解决方案
模拟器与真机返回值差异 isPhysicalDevice判断,避免模拟器数据干扰
不同 Android 厂商定制系统字段差异 优先使用model/manufacturer,避免依赖fingerprint等非标字段
macOS 14+ 获取内存信息失败 升级device_info_plus到最新版本(>=10.0.0)

3. 性能与异步问题

  • 设备信息获取是异步操作,不要在build方法中直接调用,建议在initState或全局初始化时获取;
  • 避免频繁调用(如每次点击按钮都获取),建议缓存结果;
  • 无上下文时必须先调用WidgetsFlutterBinding.ensureInitialized(),否则会崩溃。

总结

  1. 核心定位device_info_plus 是 Flutter 多平台设备信息获取的官方首选,替代废弃的device_info,覆盖全平台;
  2. 使用原则:按平台调用对应info方法,优先封装为工具类,异步操作需处理异常和缓存;
  3. 避坑要点:注意权限合规、模拟器 / 真机差异、Android 10+ 设备标识限制,无上下文时先初始化WidgetsFlutterBinding

如果需要针对某一特殊场景(如鸿蒙系统识别、iOS 广告 ID 获取、Web 端设备指纹)的详细代码,我可以进一步补充。

Flutter ——流式布局(Wrap)

Flutter 中 Wrap组件是解决 Row/Column 溢出问题的另一种重要方案,下面从核心作用、基础用法、核心属性、实战场景和对比 Row 这几个方面,给你做全面且易懂的讲解。

一、Wrap 核心作用

Wrap 是流式布局组件,和 Row/Column 最大的区别是:

  • Row/Column 子组件总尺寸超过父容器时会溢出(出现警告);
  • Wrap 子组件总尺寸超过父容器时会自动换行 / 换列,不会溢出。

简单说:Wrap 就是 “可以自动换行的 Row” 或 “可以自动换列的 Column”。

二、基础用法

先看一个最基础的示例,直观感受 Wrap 的效果:

lass MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text("哈哈"),
        ),
        body: Padding(padding: EdgeInsetsGeometry.all(10),
          child: Wrap(
            children: [
              Container(width: 80,height: 80,
                margin: const EdgeInsets.all(5),
                color: Colors.red,
                child: Center(child: Text('1')),
              ),
              Container(width: 80,height: 80,
                margin: const EdgeInsets.all(5),
                color: Colors.red,
                child: Center(child: Text('2')),
              ),
              Container(width: 80,height: 80,
                margin: const EdgeInsets.all(5),
                color: Colors.red,
                child: Center(child: Text('3')),
              ),
              Container(width: 80,height: 80,
                margin: const EdgeInsets.all(5),
                color: Colors.red,
                child: Center(child: Text('4')),
              ),
              Container(width: 80,height: 80,
                margin: const EdgeInsets.all(5),
                color: Colors.red,
                child: Center(child: Text('5')),
              ),
              Container(width: 80,height: 80,
                margin: const EdgeInsets.all(5),
                color: Colors.red,
                child: Center(child: Text('6')),
              ),
              Container(width: 80,height: 80,
                margin: const EdgeInsets.all(5),
                color: Colors.red,
                child: Center(child: Text('7')),
              ),
              Container(width: 80,height: 80,
                margin: const EdgeInsets.all(5),
                color: Colors.red,
                child: Center(child: Text('8')),
              )
            ],
          ),
        ),
      ),
    );
  }
}
图片.png

效果:8 个 80x80 的容器会先在第一行排列,当剩余宽度不够放下下一个容器时,自动换行到第二行,完全适配父容器宽度,无溢出。

三、核心属性详解

Wrap 的属性和 Row/Column 高度相似,但新增了换行相关的属性:

属性 作用 常用值
direction 排列方向(主轴) Axis.horizontal(默认,水平)/ Axis.vertical(垂直)
alignment 主轴方向的对齐方式(单行 / 列的对齐) WrapAlignment.start(默认)/ center/ end/ spaceBetween/ spaceAround/ spaceEvenly
crossAxisAlignment 交叉轴方向的对齐方式(行 / 列之间的对齐) WrapCrossAlignment.start(默认)/ center/ end
runAlignment 多行 / 多列整体的对齐方式 WrapAlignment.start(默认)/ center/ end/ 等
spacing 主轴方向子组件之间的间距 数值(如 8.0)
runSpacing 交叉轴方向(行 / 列之间)的间距 数值(如 8.0)
children 子组件列表 Widget 数组

关键属性实战示例

Wrap(
  direction: Axis.horizontal, // 水平排列
  alignment: WrapAlignment.spaceBetween, // 单行内两端对齐
  runAlignment: WrapAlignment.center, // 多行整体居中
  crossAxisAlignment: WrapCrossAlignment.center, // 行内垂直居中
  spacing: 10, // 水平子组件间距 10
  runSpacing: 15, // 行与行之间的间距 15
  children: [
    Container(width: 70, height: 70, color: Colors.red),
    Container(width: 70, height: 80, color: Colors.green),
    Container(width: 70, height: 70, color: Colors.blue),
    Container(width: 70, height: 70, color: Colors.yellow),
    Container(width: 70, height: 70, color: Colors.purple),
    Container(width: 70, height: 70, color: Colors.yellow),
    Container(width: 70, height: 70, color: Colors.purple),
  ],
)
图片.png

四、常见使用场景

Wrap 是 Flutter 中实现 “标签流、按钮流、网格标签” 的首选组件,以下是两个高频实战场景:

场景 1:标签列表(最经典用法)

dart

// 模拟动态标签列表
Wrap(
  spacing: 8, // 标签之间的水平间距
  runSpacing: 8, // 行之间的垂直间距
  children: [
    // 标签组件封装
    _buildTag('Flutter'),
    _buildTag('Dart'),
    _buildTag('Android'),
    _buildTag('iOS'),
    _buildTag('前端'),
    _buildTag('移动端'),
    _buildTag('跨平台'),
    _buildTag('布局'),
    _buildTag('组件'),
  ],
)

// 封装标签 Widget
Widget _buildTag(String text) {
  return Container(
    padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
    decoration: BoxDecoration(
      color: Colors.grey[200],
      borderRadius: BorderRadius.circular(20),
    ),
    child: Text(text),
  );
}

五、Wrap vs Row/Column 核心对比

特性 Wrap Row/Column
溢出处理 自动换行 / 换列,无溢出 直接溢出,出现警告
空间占用 仅包裹子组件(mainAxisSize 固定为 min) 可设置 max/min,默认 max
适用场景 动态数量的子组件(标签、按钮) 固定数量的子组件(导航栏、表单行)
性能 略优(无需计算溢出) 需计算主轴空间,溢出时性能无影响

六、常见问题与注意事项

  1. Wrap 中使用 Expanded 无效:Expanded 是配合 Flex(Row/Column)的弹性布局组件,Wrap 不支持弹性分配空间,因此在 Wrap 的 children 中用 Expanded 不会有任何效果。

  2. 控制 Wrap 整体的宽度 / 高度:如果想让 Wrap 占满父容器宽度(而非仅包裹子组件),可以给 Wrap 包裹一个 Container 并设置宽度:

    dart

    Container(
      width: double.infinity, // 占满父容器宽度
      child: Wrap(/* ... */),
    )
    

总结

  1. 核心定位:Wrap 是流式布局,解决 Row/Column 溢出问题,子组件超出父容器时自动换行 / 换列。

  2. 核心属性spacing(子组件间距)、runSpacing(行 / 列间距)、direction(排列方向)是最常用的三个属性。

  3. 使用技巧

    • 动态数量的标签、按钮优先用 Wrap;
    • Wrap 不支持 Expanded,无需尝试弹性分配空间;
    • 控制间距优先用 spacing/runSpacing,而非子组件的 margin(更统一)。

掌握 Wrap 布局,就能轻松实现 Flutter 中绝大多数 “流式排列” 的 UI 场景,是替代 Row/Column 解决溢出问题的最佳选择。

Flutter——弹性布局(Flex、Expanded)

弹性布局(Flex)允许子组件按照一定比例来分配父容器空间。

一、核心关系与基础概念

首先要明确一个关键关系:Row 和 Column 都是 继承自Flex 组件的 ——

  • Row = Flex(direction: Axis.horizontal)(水平弹性布局)
  • Column = Flex(direction: Axis.vertical)(垂直弹性布局)

Expanded 是配合 Flex(包括 Row/Column)使用的 “空间分配器”,用于控制子组件在弹性布局中占据的空间比例。

1. Flex 组件的基础用法

Flex 是弹性布局的基类,直接使用的场景较少(优先用 Row/Column),但理解它能帮你掌握弹性布局的本质:

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Flex & Expanded 详解')),
        body: Padding(
          padding: const EdgeInsets.all(20),
          // 基础 Flex 布局(水平方向,等价于 Row)
          child: Flex(
            direction: Axis.horizontal, // 布局方向:horizontal(水平)/vertical(垂直)
            children: [
              Container(width: 80, height: 80, color: Colors.red),
              Container(width: 80, height: 80, color: Colors.green),
              Container(width: 80, height: 80, color: Colors.blue),
            ],
          ),
        ),
      ),
    );
  }
}

二、Flex 核心属性(Row/Column 通用)

Flex 的核心属性和 Row/Column 完全一致,这里重点强调弹性布局相关的关键属性:

属性 作用 常用值
direction 布局方向(Flex 独有,Row/Column 已固定) Axis.horizontal / Axis.vertical
mainAxisAlignment 主轴对齐方式 start/center/end/spaceBetween/spaceAround/spaceEvenly
crossAxisAlignment 交叉轴对齐方式 start/center/end/stretch
mainAxisSize 主轴占用空间 max(默认,占满父容器)/ min(仅包裹子组件)
children 子组件列表 Widget 数组

三、Expanded 组件详解

Expanded 是 Flex 布局的 “黄金搭档” ,用于解决布局溢出问题,并按比例分配剩余空间。

1. 核心作用

  • 强制子组件占据 Flex 布局中剩余的全部 / 部分空间
  • 自动适配父容器尺寸,避免子组件溢出(最核心的用途)
  • 支持通过 flex 属性设置空间分配比例

2. 基础属性

属性 作用 默认值
flex 空间分配权重(比例) 1
child 需要分配空间的子组件 必填

3. 实战示例

示例 1:基础比例分配(解决溢出)
// Flex(Row)+ Expanded 按比例分配空间
Row(
  children: [
    // 占1份空间
    Expanded(
      flex: 1,
      child: Container(color: Colors.red, height: 60),
    ),
    // 占2份空间
    Expanded(
      flex: 2,
      child: Container(color: Colors.green, height: 60),
    ),
    // 占1份空间
    Expanded(
      flex: 1,
      child: Container(color: Colors.blue, height: 60),
    ),
  ],
)

效果:父容器宽度被分成 1+2+1=4 份,红色占 1/4,绿色占 2/4,蓝色占 1/4,完全适配父容器,无溢出。

示例 2:混合固定尺寸 + 弹性尺寸
// 固定尺寸 + Expanded 弹性尺寸
Row(
  children: [
    // 固定宽度的按钮
    const SizedBox(width: 80, child: ElevatedButton(onPressed: () {}, child: Text('返回'))),
    // 占满剩余所有空间的文本(flex 默认1)
    Expanded(
      child: Container(
        color: Colors.grey[200],
        alignment: Alignment.center,
        child: const Text('这是标题,会占满剩余空间'),
      ),
    ),
    // 固定宽度的图标
    const SizedBox(width: 80, child: Icon(Icons.more_vert)),
  ],
)

效果:左右两个固定宽度的组件,中间文本区域自动占满剩余所有宽度,适配不同屏幕尺寸。

示例 3:垂直方向(Column + Expanded)
// Column + Expanded 垂直方向分配空间
Column(
  children: [
    // 顶部固定高度
    Container(height: 80, color: Colors.red, child: const Center(child: Text('顶部'))),
    // 中间占满剩余空间(核心内容区)
    Expanded(
      child: Container(color: Colors.green, child: const Center(child: Text('核心内容区'))),
    ),
    // 底部固定高度
    Container(height: 80, color: Colors.blue, child: const Center(child: Text('底部'))),
  ],
)

效果:顶部和底部固定高度,中间内容区自动占满屏幕剩余高度,是 App 页面的经典布局方式。

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text("哈哈"),
        ),
        body: Column(
          children: [Flex(direction: .horizontal,
            children: [
              Expanded(
                  flex: 1,
                  // 此处设置的宽度 100无效,flex 比 width 优先级高
                  child: Container(width: 100, height: 40,color: Colors.red,)),
              Expanded(
                  flex: 2,
                  child: Container(width: 100,height: 40,color: Colors.yellow,))
            ],
          )],
        ),
      ),
    );
  }
}

Expandedflex 优先级远高于子组件的 widthExpanded 的作用是强制子组件占据 Flex 布局中分配给它的空间,因此你给 Container 设置的 width: 100 会被完全忽略;

Flutter——线性布局(Row、Column)

Flutter 中最基础也最核心的 Row(行)和 Column(列)布局组件,我会从基础用法、核心属性、常见问题和实战示例几个方面,给你做一个通俗易懂的全面讲解。

一、核心概念与基础用法

Row 和 Column 是 Flutter 中用于线性布局的核心组件:

  • Row:在水平方向上排列子组件(从左到右,可通过属性调整)
  • Column:在垂直方向上排列子组件(从上到下,可通过属性调整)

它们都继承自 Flex 组件,核心逻辑一致,只是排列方向不同。

1. 最基础的使用示例

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Row & Column 详解')),
        body: Padding(
          padding: const EdgeInsets.all(20),
          // 外层 Column 垂直排列
          child: Column(
            // 子组件
            children: [
              // 第一个子组件:Row 水平排列
              Row(
                children: [
                  Container(width: 80, height: 80, color: Colors.red),
                  Container(width: 80, height: 80, color: Colors.green),
                  Container(width: 80, height: 80, color: Colors.blue),
                ],
              ),
              const SizedBox(height: 20), // 间距
              // 第二个子组件:Column 垂直排列
              Column(
                children: [
                  Container(width: 80, height: 80, color: Colors.yellow),
                  Container(width: 80, height: 80, color: Colors.purple),
                  Container(width: 80, height: 80, color: Colors.orange),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

二、核心属性详解(Row/Column 通用)

这是掌握布局的关键,重点属性如下:

属性 作用 常用值
mainAxisAlignment 主轴对齐方式(Row 主轴 = 水平,Column 主轴 = 垂直) start(默认)、centerendspaceBetweenspaceAroundspaceEvenly
crossAxisAlignment 交叉轴对齐方式(Row 交叉轴 = 垂直,Column 交叉轴 = 水平) startcenter(默认)、endstretch(拉伸填满)
mainAxisSize 主轴占用空间大小 max(默认,占满父组件)、min(仅包裹子组件)
children 子组件列表 任意 Widget 数组

关键属性实战示例

// 演示 mainAxisAlignment 和 crossAxisAlignment 的效果
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween, // 水平两端对齐,中间均匀间距
  crossAxisAlignment: CrossAxisAlignment.center,    // 垂直居中
  mainAxisSize: MainAxisSize.max,                   // 占满父组件宽度
  children: [
    Container(width: 50, height: 50, color: Colors.red),
    Container(width: 50, height: 80, color: Colors.green), // 高度不同,看交叉轴对齐
    Container(width: 50, height: 50, color: Colors.blue),
  ],
)

三、常见问题与解决方案

1. Row/Column 溢出(Overflow)

问题:子组件总宽度 / 高度超过父组件,出现黄色 / 黑色溢出警告。解决方案

  • 使用 Expanded:让子组件占满剩余空间(可设置 flex 比例)
  • 使用 Flexible:类似 Expanded,但不会强制填满(可设置 fit 属性)
  • 使用 SingleChildScrollView:可滚动的布局(横向 / 纵向)
// Expanded 解决溢出问题(按比例分配空间)
Row(
  children: [
    Expanded(flex: 1, child: Container(color: Colors.red, height: 50)), // 1份
    Expanded(flex: 2, child: Container(color: Colors.green, height: 50)), // 2份
    Expanded(flex: 1, child: Container(color: Colors.blue, height: 50)), // 1份
  ],
)

// SingleChildScrollView 解决溢出(可滚动)
SingleChildScrollView(
  scrollDirection: Axis.horizontal, // 横向滚动
  child: Row(
    children: [
      for (int i = 0; i < 10; i++) Container(width: 80, height: 80, color: Colors.primaries[i % 10]),
    ],
  ),
)

2. Column 嵌套 Column 高度异常

问题:内层 Column 占满外层 Column 高度,导致布局错乱。解决方案:给内层 Column 包裹 mainAxisSize: MainAxisSize.min

Column(
  children: [
    Text('标题'),
    // 内层 Column 仅包裹子组件,不占满高度
    Column(
      mainAxisSize: MainAxisSize.min,
      children: [Text('内容1'), Text('内容2'), Text('内容3')],
    ),
  ],
)

四、实战综合示例

// 模拟一个常见的页面布局
Scaffold(
  body: Column(
    children: [
      // 顶部导航栏区域(Row)
      Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          IconButton(icon: const Icon(Icons.arrow_back), onPressed: () {}),
          const Text('详情页', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
          IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}),
        ],
      ),
      // 分割线
      const Divider(height: 1),
      // 内容区域(占满剩余高度)
      Expanded(
        child: SingleChildScrollView(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              // 图片
              Image.network('https://picsum.photos/400/200'),
              // 文本区域
              Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start, // 文本左对齐
                  children: const [
                    Text('商品名称', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                    SizedBox(height: 8),
                    Text('价格:¥99.00', style: TextStyle(color: Colors.red, fontSize: 18)),
                    SizedBox(height: 8),
                    Text('商品描述:这是一个测试商品,用于演示 Row 和 Column 布局'),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
      // 底部按钮区域(Row)
      Row(
        children: [
          Expanded(child: ElevatedButton(onPressed: () {}, child: const Text('加入购物车'))),
          Expanded(child: ElevatedButton(onPressed: () {}, style: ElevatedButton.styleFrom(backgroundColor: Colors.red), child: const Text('立即购买'))),
        ],
      ),
    ],
  ),
)

总结

  1. 核心定位:Row 是水平线性布局,Column 是垂直线性布局,核心属性(mainAxisAlignment/crossAxisAlignment)控制对齐方式。
  2. 溢出解决:溢出是最常见问题,优先用 Expanded(按比例分配)或 SingleChildScrollView(可滚动)解决。
  3. 关键技巧:嵌套布局时,记得给内层 Column/Row 设置 mainAxisSize: MainAxisSize.min,避免不必要的空间占用。

iOS——IPATool工具的使用

IPATool 是一款命令行工具,可通过 Apple ID 从 App Store 下载加密 IPA 包,支持多平台(macOS/Windows/Linux),适用于开发者测试、版本归档等场景。

一、安装(分平台)

1. macOS(推荐 Homebrew)

# 安装 ipatool
brew install ipatool
# 验证
ipatool --version
// 结果 ipatool version 2.1.6
  1. 验证:终端输入 ipatool --version 显示版本号即可。

二、核心流程:认证 → 搜索 → 下载

1. 账号认证(必需)

bash

运行

# 登录 Apple ID(开启双重验证需输入验证码)
ipatool auth login -e 你的邮箱 -p 你的密码
# 查看登录信息
ipatool auth info
# 登出/撤销凭证
ipatool auth revoke

注意:双重验证环境下,密码需用「App 专用密码」(Apple ID 管理页生成),避免登录失败。

2. 搜索应用(获取 Bundle ID/App ID)

# 搜索关键词,限制返回 5 条结果
ipatool search "微信" --limit 5
# 输出示例(含 Bundle ID:com.tencent.xin)

3. IPA文件下载

找到目标应用后,使用应用ID进行下载:

ipatool download --app-id 应用ID --output 保存路径
//例 ipatool download --app-id 155342910943 --output 保存路径

备注: 下载提示「未购买」未加 --purchase 参数首次下载添加 --purchase 获取许可

❌