🎬 第一章:隐式动画
隐式动画是 Flutter 中最简单的动画方式。你只需要声明目标值,Flutter 会自动在旧值和新值之间进行平滑过渡。所有隐式动画 Widget 都以 Animated 开头。
1.1 AnimatedContainer
AnimatedContainer 可以自动对容器的大小、颜色、边框、圆角等属性变化进行动画。它是最常用的隐式动画组件。
Dart class AnimatedBoxDemo extends StatefulWidget { @override State<AnimatedBoxDemo> createState() => _AnimatedBoxDemoState(); } class _AnimatedBoxDemoState extends State<AnimatedBoxDemo> { bool _expanded = false; @override Widget build(BuildContext context) { return GestureDetector( onTap: () => setState(() => _expanded = !_expanded), child: AnimatedContainer( duration: const Duration(milliseconds: 400), curve: Curves.easeInOut, width: _expanded ? 250 : 100, height: _expanded ? 250 : 100, decoration: BoxDecoration( color: _expanded ? Colors.blue : Colors.red, borderRadius: BorderRadius.circular( _expanded ? 32 : 8, ), boxShadow: [ BoxShadow( color: Colors.black26, blurRadius: _expanded ? 20 : 5, ), ], ), child: const Center( child: Text('点击切换', style: TextStyle(color: Colors.white)), ), ), ); } }
1.2 AnimatedOpacity
AnimatedOpacity 用于实现淡入淡出效果,常用于显示/隐藏元素的场景。
Dart AnimatedOpacity( opacity: _visible ? 1.0 : 0.0, duration: const Duration(milliseconds: 500), curve: Curves.easeIn, child: Container( width: 200, height: 200, color: Colors.green, child: const Center(child: Text('渐隐渐现')), ), )
1.3 AnimatedPositioned
AnimatedPositioned 必须在 Stack 中使用,可以对子组件的位置进行平滑动画。
Dart Stack( children: [ AnimatedPositioned( duration: const Duration(milliseconds: 600), curve: Curves.elasticOut, left: _moved ? 200 : 20, top: _moved ? 150 : 20, child: Container( width: 80, height: 80, decoration: BoxDecoration( color: Colors.orange, borderRadius: BorderRadius.circular(12), ), ), ), ], )
1.4 AnimatedSwitcher
AnimatedSwitcher 在子组件切换时自动播放过渡动画。需要给子组件不同的 key 才能触发动画。
Dart AnimatedSwitcher( duration: const Duration(milliseconds: 300), transitionBuilder: (Widget child, Animation<double> animation) { return ScaleTransition(scale: animation, child: child); }, child: Text( '$_count', key: ValueKey<int>(_count), style: const TextStyle(fontSize: 48), ), )
1.5 TweenAnimationBuilder
TweenAnimationBuilder 允许你使用任意 Tween 创建隐式动画,比内置的隐式动画组件更灵活。
Dart TweenAnimationBuilder<double>( tween: Tween<double>(begin: 0, end: _angle), duration: const Duration(milliseconds: 800), curve: Curves.easeOutBack, builder: (context, value, child) { return Transform.rotate( angle: value, child: child, ); }, child: const Icon(Icons.refresh, size: 60), )
duration 和 curve 参数。curve 控制动画的缓动效果,常用的有 Curves.easeInOut、Curves.bounceOut、Curves.elasticOut 等。
✨ 常用隐式动画组件
- AnimatedContainer - 容器属性动画
- AnimatedOpacity - 透明度动画
- AnimatedPadding - 内边距动画
- AnimatedAlign - 对齐动画
- AnimatedDefaultTextStyle - 文本样式动画
- AnimatedCrossFade - 交叉淡入淡出
🎯 使用场景
- 按钮点击后的视觉反馈
- 列表项展开/收起
- 页面元素的渐入效果
- 主题切换过渡
- 状态指示器变化
🎭 第二章:显式动画
显式动画提供了对动画过程的完全控制。你需要手动管理 AnimationController,但可以实现更复杂的动画效果,如循环、反向、组合动画等。
2.1 AnimationController
AnimationController 是显式动画的核心,它控制动画的播放、暂停、反向等。需要配合 SingleTickerProviderStateMixin 使用。
Dart class PulseAnimation extends StatefulWidget { @override State<PulseAnimation> createState() => _PulseAnimationState(); } class _PulseAnimationState extends State<PulseAnimation> with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(seconds: 2), )..repeat(reverse: true); // 循环播放并反向 } @override void dispose() { _controller.dispose(); // 必须释放资源 super.dispose(); } @override Widget build(BuildContext context) { return ScaleTransition( scale: _controller, child: const FlutterLogo(size: 100), ); } }
2.2 Tween 与 CurvedAnimation
Tween 定义动画的值范围,CurvedAnimation 为动画添加缓动曲线。两者组合可以精确控制动画的表现。
Dart late AnimationController _controller; late Animation<double> _sizeAnimation; late Animation<Color?> _colorAnimation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), ); // 带缓动曲线的动画 final curvedAnimation = CurvedAnimation( parent: _controller, curve: Curves.easeOutBack, ); // 大小动画:50 -> 200 _sizeAnimation = Tween<double>( begin: 50, end: 200, ).animate(curvedAnimation); // 颜色动画:红 -> 蓝 _colorAnimation = ColorTween( begin: Colors.red, end: Colors.blue, ).animate(curvedAnimation); }
2.3 AnimatedBuilder
AnimatedBuilder 用于将动画与 Widget 解耦,减少不必要的重建,是性能最佳的显式动画方式。
Dart @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return Container( width: _sizeAnimation.value, height: _sizeAnimation.value, decoration: BoxDecoration( color: _colorAnimation.value, borderRadius: BorderRadius.circular(16), ), child: child, // child 不会重建 ); }, child: const Center( child: Icon(Icons.star, color: Colors.white, size: 30), ), ); }
2.4 交错动画 (Staggered Animations)
交错动画通过 Interval 让多个动画按顺序执行,创造丰富的视觉效果。
Dart class StaggeredDemo extends StatefulWidget { @override State<StaggeredDemo> createState() => _StaggeredDemoState(); } class _StaggeredDemoState extends State<StaggeredDemo> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _opacity; late Animation<double> _width; late Animation<double> _height; late Animation<EdgeInsets> _padding; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 2000), ); // 0% - 30%:淡入 _opacity = Tween<double>(begin: 0, end: 1).animate( CurvedAnimation( parent: _controller, curve: const Interval(0.0, 0.3, curve: Curves.easeIn), ), ); // 20% - 60%:宽度展开 _width = Tween<double>(begin: 50, end: 250).animate( CurvedAnimation( parent: _controller, curve: const Interval(0.2, 0.6, curve: Curves.easeOut), ), ); // 40% - 80%:高度展开 _height = Tween<double>(begin: 50, end: 250).animate( CurvedAnimation( parent: _controller, curve: const Interval(0.4, 0.8, curve: Curves.easeOut), ), ); // 60% - 100%:内边距变化 _padding = EdgeInsetsTween( begin: const EdgeInsets.all(0), end: const EdgeInsets.all(20), ).animate( CurvedAnimation( parent: _controller, curve: const Interval(0.6, 1.0, curve: Curves.easeInOut), ), ); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return Opacity( opacity: _opacity.value, child: Container( width: _width.value, height: _height.value, padding: _padding.value, decoration: BoxDecoration( color: Colors.deepPurple, borderRadius: BorderRadius.circular(16), ), child: const FlutterLogo(), ), ); }, ); } }
2.5 完整示例:旋转缩放动画
Dart class RotateScaleDemo extends StatefulWidget { @override State<RotateScaleDemo> createState() => _RotateScaleDemoState(); } class _RotateScaleDemoState extends State<RotateScaleDemo> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _rotation; late Animation<double> _scale; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(seconds: 3), )..repeat(); _rotation = Tween<double>(begin: 0, end: 2 * 3.14159) .animate(_controller); _scale = TweenSequence<double>([ TweenSequenceItem( tween: Tween(begin: 1.0, end: 1.5), weight: 50, ), TweenSequenceItem( tween: Tween(begin: 1.5, end: 1.0), weight: 50, ), ]).animate(_controller); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return Transform.rotate( angle: _rotation.value, child: Transform.scale( scale: _scale.value, child: child, ), ); }, child: Container( width: 80, height: 80, decoration: BoxDecoration( gradient: const LinearGradient( colors: [Colors.purple, Colors.blue], ), borderRadius: BorderRadius.circular(16), ), child: const Icon(Icons.star, color: Colors.white, size: 40), ), ); } }
dispose() 中调用 _controller.dispose() 释放资源,否则会导致内存泄漏。如果有多个 AnimationController,使用 TickerProviderStateMixin 代替 SingleTickerProviderStateMixin。
| 对比项 | 隐式动画 | 显式动画 |
|---|---|---|
| 复杂度 | 简单,声明式 | 较复杂,需要手动管理 |
| 控制力 | 仅目标值变化 | 完全控制(播放/暂停/反向/循环) |
| 性能 | 一般 | 可通过 AnimatedBuilder 优化 |
| 适用场景 | 简单状态切换 | 复杂、连续、组合动画 |
| 代码量 | 少 | 多 |
🦸 第三章:Hero 动画
Hero 动画用于在页面导航时创建共享元素过渡效果。典型场景是列表页的图片点击后「飞入」详情页。两个页面中的 Hero Widget 需要使用相同的 tag。
3.1 基本用法
Dart // ===== 列表页 ===== class ProductListPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('商品列表')), body: ListView.builder( itemCount: products.length, itemBuilder: (context, index) { final product = products[index]; return GestureDetector( onTap: () => Navigator.push( context, MaterialPageRoute( builder: (_) => ProductDetailPage(product: product), ), ), child: Hero( tag: 'product-${product.id}', child: Image.network(product.imageUrl, width: 80, height: 80, fit: BoxFit.cover), ), ); }, ), ); } } // ===== 详情页 ===== class ProductDetailPage extends StatelessWidget { final Product product; const ProductDetailPage({required this.product}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(product.name)), body: Column( children: [ Hero( tag: 'product-${product.id}', child: Image.network(product.imageUrl, width: double.infinity, height: 300, fit: BoxFit.cover), ), Padding( padding: const EdgeInsets.all(16), child: Text(product.description), ), ], ), ); } }
3.2 自定义飞行动画 (flightShuttleBuilder)
使用 flightShuttleBuilder 可以自定义动画过渡时元素的外观,比如给飞行中的元素添加阴影、变形等效果。
Dart Hero( tag: 'avatar-hero', flightShuttleBuilder: ( BuildContext flightContext, Animation<double> animation, HeroFlightDirection flightDirection, BuildContext fromHeroContext, BuildContext toHeroContext, ) { return AnimatedBuilder( animation: animation, builder: (context, child) { return Material( color: Colors.transparent, child: Container( decoration: BoxDecoration( shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black26, blurRadius: 20 * animation.value, spreadRadius: 4 * animation.value, ), ], ), child: ClipOval( child: Image.network('avatar_url', fit: BoxFit.cover), ), ), ); }, ); }, child: ClipOval( child: Image.network('avatar_url', width: 50, height: 50, fit: BoxFit.cover), ), )
3.3 Hero 动画与自定义路由过渡
Dart // 自定义页面路由,让 Hero 动画更丝滑 Navigator.push( context, PageRouteBuilder( transitionDuration: const Duration(milliseconds: 600), reverseTransitionDuration: const Duration(milliseconds: 400), pageBuilder: (context, animation, secondaryAnimation) { return FadeTransition( opacity: animation, child: ProductDetailPage(product: product), ); }, ), );
tag 必须在同一页面内唯一。如果列表中有多个 Hero,每个都应该有不同的 tag(如使用 id 区分)。Hero 动画在 Navigator.push 和 Navigator.pop 时都会触发。
📱 第四章:Platform Channel
Platform Channel 是 Flutter 与原生平台(Android/iOS)之间的通信桥梁。通过它可以调用原生 API、获取设备信息、使用原生 SDK 等。Flutter 提供了三种 Channel 类型。
4.1 MethodChannel
MethodChannel 用于一次性的方法调用,支持请求-响应模式。是最常用的通信方式。
Flutter 端
Dart import 'package:flutter/services.dart'; class BatteryService { static const _channel = MethodChannel('com.example.app/battery'); // 获取电池电量 static Future<int> getBatteryLevel() async { try { final int level = await _channel.invokeMethod('getBatteryLevel'); return level; } on PlatformException catch (e) { throw Exception('获取电池电量失败: ${e.message}'); } } // 带参数的调用 static Future<String> getDeviceInfo(String key) async { final result = await _channel.invokeMethod('getDeviceInfo', { 'key': key, }); return result as String; } } // 在 Widget 中使用 ElevatedButton( onPressed: () async { final level = await BatteryService.getBatteryLevel(); setState(() => _batteryLevel = level); }, child: const Text('获取电池电量'), )
Android 端 (Kotlin)
Kotlin class MainActivity: FlutterActivity() { private val CHANNEL = "com.example.app/battery" override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) .setMethodCallHandler { call, result -> when (call.method) { "getBatteryLevel" -> { val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager val level = batteryManager.getIntProperty( BatteryManager.BATTERY_PROPERTY_CAPACITY ) if (level != -1) { result.success(level) } else { result.error("UNAVAILABLE", "无法获取电池电量", null) } } else -> result.notImplemented() } } } }
iOS 端 (Swift)
Swift @UIApplicationMain class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { let controller = window?.rootViewController as! FlutterViewController let channel = FlutterMethodChannel( name: "com.example.app/battery", binaryMessenger: controller.binaryMessenger ) channel.setMethodCallHandler { (call, result) in if call.method == "getBatteryLevel" { let device = UIDevice.current device.isBatteryMonitoringEnabled = true let level = Int(device.batteryLevel * 100) result(level) } else { result(FlutterMethodNotImplemented) } } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } }
4.2 EventChannel
EventChannel 用于从原生平台向 Flutter 推送持续的事件流,适用于传感器数据、电池状态变化等场景。
Dart class BatteryEventService { static const _eventChannel = EventChannel('com.example.app/battery_events'); // 监听电池状态变化 static Stream<int> batteryLevelStream() { return _eventChannel .receiveBroadcastStream() .map((event) => event as int); } } // 在 Widget 中使用 StreamBuilder<int>( stream: BatteryEventService.batteryLevelStream(), builder: (context, snapshot) { if (snapshot.hasData) { return Text('电池电量: ${snapshot.data}%'); } return const CircularProgressIndicator(); }, )
4.3 Pigeon(类型安全通信)
Pigeon 是 Flutter 团队推荐的类型安全 Platform Channel 代码生成工具,避免手动编写字符串匹配。
Dart import 'package:pigeon/pigeon.dart'; // 定义接口(Pigeon 会自动生成各平台代码) @HostApi() abstract class BatteryApi { int getBatteryLevel(); String getBatteryState(); } @FlutterApi() abstract class BatteryEventApi { void onBatteryLevelChanged(int level); }
com.example.app/feature)来避免命名冲突。对于简单的平台调用,也可以考虑使用社区提供的插件(如 battery_plus),避免手动编写原生代码。
| Channel 类型 | 方向 | 模式 | 适用场景 |
|---|---|---|---|
| MethodChannel | 双向 | 请求-响应 | 方法调用、获取数据 |
| EventChannel | 原生 → Flutter | 事件流 | 传感器、状态变化 |
| BasicMessageChannel | 双向 | 消息传递 | 自定义编解码 |
| Pigeon | 双向 | 类型安全代码生成 | 大型项目推荐 |
🎨 第五章:自定义绘制
Flutter 提供了 CustomPaint 和 Canvas API,让你可以直接在画布上绘制任意图形。这是实现自定义图表、特殊形状、手写板等功能的基础。
5.1 CustomPaint 基础
CustomPaint 接收一个 CustomPainter 来执行绘制逻辑。
Dart class CirclePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.blue ..style = PaintingStyle.fill; // 绘制填充圆 canvas.drawCircle( Offset(size.width / 2, size.height / 2), 50, paint, ); // 绘制边框圆 paint ..color = Colors.red ..style = PaintingStyle.stroke ..strokeWidth = 3; canvas.drawCircle( Offset(size.width / 2, size.height / 2), 70, paint, ); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } // 使用 CustomPaint( size: const Size(200, 200), painter: CirclePainter(), )
5.2 Canvas API 常用方法
Dart void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.black87 ..strokeWidth = 2; // 绘制线段 canvas.drawLine( const Offset(0, 0), Offset(size.width, size.height), paint, ); // 绘制矩形 canvas.drawRect( Rect.fromLTWH(20, 20, 100, 60), paint..style = PaintingStyle.stroke, ); // 绘制圆角矩形 canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromLTWH(20, 100, 100, 60), const Radius.circular(12), ), paint..color = Colors.green, ); // 绘制弧形 canvas.drawArc( Rect.fromCenter( center: Offset(size.width / 2, 200), width: 120, height: 120, ), 0, // 起始角度 3.14, // 扫描角度(弧度) false, // 是否连接中心点 paint..color = Colors.purple, ); // 绘制文字 final textPainter = TextPainter( text: const TextSpan( text: 'Canvas 文字', style: TextStyle(color: Colors.black, fontSize: 16), ), textDirection: TextDirection.ltr, ); textPainter.layout(); textPainter.paint(canvas, const Offset(20, 280)); }
5.3 Path 绘制
Path 可以绘制复杂的形状,如波浪线、多边形、贝塞尔曲线等。
Dart class WavePainter extends CustomPainter { final double animationValue; WavePainter(this.animationValue); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.blue.withOpacity(0.6) ..style = PaintingStyle.fill; final path = Path(); path.moveTo(0, size.height * 0.5); // 绘制波浪 for (double i = 0; i < size.width; i++) { path.lineTo( i, size.height * 0.5 + sin((i / size.width * 2 * pi) + (animationValue * 2 * pi)) * 30, ); } path.lineTo(size.width, size.height); path.lineTo(0, size.height); path.close(); canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant WavePainter oldDelegate) => oldDelegate.animationValue != animationValue; }
5.4 Paint 属性详解
Dart final paint = Paint() ..color = Colors.blue // 颜色 ..style = PaintingStyle.stroke // fill 或 stroke ..strokeWidth = 3.0 // 线宽 ..strokeCap = StrokeCap.round // 线段端点样式 ..strokeJoin = StrokeJoin.round // 线段连接样式 ..isAntiAlias = true // 抗锯齿 ..shader = LinearGradient( // 渐变着色器 colors: [Colors.blue, Colors.purple], ).createShader(Rect.fromLTWH(0, 0, 200, 200)) ..maskFilter = const MaskFilter.blur( // 模糊效果 BlurStyle.normal, 5.0, );
5.5 CustomClipper
CustomClipper 用于裁剪 Widget 的形状,常用于创建波浪形、弧形的头部区域。
Dart class WaveClipper extends CustomClipper<Path> { @override Path getClip(Size size) { final path = Path(); path.lineTo(0, size.height - 40); // 贝塞尔曲线形成波浪 path.quadraticBezierTo( size.width / 4, size.height, size.width / 2, size.height - 40, ); path.quadraticBezierTo( size.width * 3 / 4, size.height - 80, size.width, size.height - 40, ); path.lineTo(size.width, 0); path.close(); return path; } @override bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false; } // 使用 ClipPath( clipper: WaveClipper(), child: Container( height: 200, color: Colors.blue, child: const Center( child: Text('波浪裁剪', style: TextStyle(color: Colors.white, fontSize: 24)), ), ), )
5.6 完整折线图示例
Dart class LineChartPainter extends CustomPainter { final List<double> data; final Color lineColor; final Color fillColor; LineChartPainter({ required this.data, this.lineColor = Colors.blue, this.fillColor = Colors.blue, }); @override void paint(Canvas canvas, Size size) { if (data.isEmpty) return; final maxVal = data.reduce((a, b) => a > b ? a : b); final minVal = data.reduce((a, b) => a < b ? a : b); final range = maxVal - minVal; final stepX = size.width / (data.length - 1); const padding = 20.0; // 绘制网格线 final gridPaint = Paint() ..color = Colors.grey.withOpacity(0.2) ..strokeWidth = 0.5; for (int i = 0; i < 5; i++) { final y = padding + (size.height - padding * 2) / 4 * i; canvas.drawLine( Offset(0, y), Offset(size.width, y), gridPaint, ); } // 计算数据点 final points = <Offset>[]; for (int i = 0; i < data.length; i++) { final x = i * stepX; final y = size.height - padding - ((data[i] - minVal) / range) * (size.height - padding * 2); points.add(Offset(x, y)); } // 绘制填充区域 final fillPath = Path() ..moveTo(0, size.height) ..lineTo(points.first.dx, points.first.dy); for (final p in points) { fillPath.lineTo(p.dx, p.dy); } fillPath ..lineTo(size.width, size.height) ..close(); canvas.drawPath( fillPath, Paint() ..shader = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [fillColor.withOpacity(0.3), fillColor.withOpacity(0.0)], ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)), ); // 绘制折线 final linePaint = Paint() ..color = lineColor ..strokeWidth = 2.5 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; final linePath = Path()..moveTo(points.first.dx, points.first.dy); for (final p in points.skip(1)) { linePath.lineTo(p.dx, p.dy); } canvas.drawPath(linePath, linePaint); // 绘制数据点 final dotPaint = Paint()..color = lineColor; for (final p in points) { canvas.drawCircle(p, 4, dotPaint); canvas.drawCircle(p, 2, Paint()..color = Colors.white); } } @override bool shouldRepaint(covariant LineChartPainter oldDelegate) => oldDelegate.data != data; } // 使用 CustomPaint( size: const Size(double.infinity, 200), painter: LineChartPainter( data: [30, 45, 28, 62, 55, 78, 40, 90, 65], lineColor: Colors.blue, fillColor: Colors.blue, ), )
shouldRepaint 返回 true 会导致每帧都重绘。请务必根据实际数据变化来判断是否需要重绘,避免性能浪费。对于复杂绘制,考虑使用 RepaintBoundary 隔离重绘区域。
⚡ 第六章:性能优化
性能优化是进阶开发者的核心技能。Flutter 应用需要保持 60fps(或 120fps)的流畅渲染。以下是关键的优化策略。
6.1 Flutter DevTools
DevTools 是 Flutter 官方提供的性能分析工具,可以分析 Widget 重建、布局耗时、帧率等信息。
Shell # 在调试模式下启动应用后,打开 DevTools flutter run # 控制台会输出 DevTools 链接 # 或者直接通过命令打开 dart devtools # 使用 Profile 模式运行(更接近真实性能) flutter run --profile
📊 DevTools 核心功能
- Widget Inspector - 查看 Widget 树结构
- Timeline - 分析帧渲染耗时
- Memory - 内存使用分析
- Performance Overlay - 实时帧率监控
- CPU Profiler - 函数调用耗时
- Network - 网络请求监控
🚨 性能警告信号
- 帧渲染时间超过 16ms
- 频繁不必要的 build 调用
- 内存使用持续增长
- Shader 编译导致的 Jank
- 列表滚动卡顿
- 图片加载导致的掉帧
6.2 使用 const 构造函数
const 修饰的 Widget 在编译期创建,不会在每次 build 时重新实例化,可以大幅减少不必要的重建。
Dart // 不好 - 每次 build 都会创建新实例 Widget build(BuildContext context) { return Padding( padding: EdgeInsets.all(8), // 每次创建新对象 child: Text( '静态文本', style: TextStyle(fontSize: 16), // 每次创建新对象 ), ); } // 好 - 使用 const,编译期常量 Widget build(BuildContext context) { return const Padding( padding: EdgeInsets.all(8), child: Text( '静态文本', style: TextStyle(fontSize: 16), ), ); }
6.3 RepaintBoundary
RepaintBoundary 将子树隔离到独立的图层,避免父组件变化时重绘子组件。适用于动画和静态区域混合的场景。
Dart Stack( children: [ // 背景动画不会影响前景列表的重绘 RepaintBoundary( child: AnimatedBackground(), // 频繁重绘的动画 ), RepaintBoundary( child: ProductList(), // 不需要频繁重绘 ), ], ) // 检查重绘情况:开启调试重绘彩虹 import 'package:flutter/rendering.dart'; debugRepaintRainbowEnabled = true;
6.4 ListView.builder 懒加载
对于长列表,始终使用 ListView.builder 而不是直接传入 children 列表。它只构建可见区域的 Widget。
Dart // 不好 - 一次性构建所有项 ListView( children: items.map((item) => ItemWidget(item: item)).toList(), ) // 好 - 按需构建可见项 ListView.builder( itemCount: items.length, itemBuilder: (context, index) => ItemWidget(item: items[index]), ) // 更好 - 已知固定高度时指定 itemExtent ListView.builder( itemCount: items.length, itemExtent: 72, // 跳过布局计算,性能更优 itemBuilder: (context, index) => ItemWidget(item: items[index]), ) // 高性能 - 使用 ListView.separated 带分割线 ListView.separated( itemCount: items.length, itemBuilder: (context, index) => ItemWidget(item: items[index]), separatorBuilder: (_, __) => const Divider(), )
6.5 图片优化
Dart // 1. 指定缓存尺寸,避免加载原始大图到内存 Image.network( 'https://example.com/large_image.jpg', cacheWidth: 300, // 缓存缩放后的图片 cacheHeight: 300, ) // 2. 使用 cached_network_image 进行磁盘缓存 CachedNetworkImage( imageUrl: 'https://example.com/image.jpg', placeholder: (context, url) => const CircularProgressIndicator(), errorWidget: (context, url, error) => const Icon(Icons.error), memCacheWidth: 300, ) // 3. 预缓存图片(在 didChangeDependencies 中调用) @override void didChangeDependencies() { super.didChangeDependencies(); precacheImage( const AssetImage('assets/hero_bg.jpg'), context, ); }
6.6 Isolate 计算隔离
耗时的计算任务(如 JSON 解析、图片处理、数据排序)应该放在 Isolate 中执行,避免阻塞 UI 线程导致掉帧。
Dart import 'dart:isolate'; import 'package:flutter/foundation.dart'; // 方式一:使用 compute(简单任务) Future<List<Product>> parseProducts(String jsonStr) async { return await compute(_parseJson, jsonStr); } // 顶层函数(Isolate 要求) List<Product> _parseJson(String jsonStr) { final List data = jsonDecode(jsonStr); return data.map((e) => Product.fromJson(e)).toList(); } // 方式二:使用 Isolate.run(Dart 2.19+,更简洁) Future<List<Product>> parseProductsV2(String jsonStr) async { return await Isolate.run(() { final List data = jsonDecode(jsonStr); return data.map((e) => Product.fromJson(e)).toList(); }); } // 方式三:长期运行的 Isolate(多次通信) Future<void> longRunningTask() async { final receivePort = ReceivePort(); await Isolate.spawn(_heavyWork, receivePort.sendPort); receivePort.listen((message) { print('收到结果: $message'); }); } void _heavyWork(SendPort sendPort) { // 执行耗时计算... sendPort.send('计算完成'); }
- 尽量使用
const构造函数 - 避免在
build()方法中执行耗时操作 - 使用
ListView.builder替代ListView(children: [...]) - 对频繁变化的区域使用
RepaintBoundary - 图片指定
cacheWidth/cacheHeight - 耗时计算使用
compute()或Isolate.run() - 使用
Profile模式测试性能(而非 Debug 模式) - 用
const修饰不变的 Widget 子树
flutter build 时使用 --bundle-sksl-path 参数将 SkSL 着色器预编译到应用中:flutter build apk --bundle-sksl-path flutter_01.sksl.json。
🌍 第七章:国际化
国际化(i18n)让你的应用支持多种语言和地区。Flutter 官方提供了 flutter_localizations 和 intl 包来实现国际化。
7.1 添加依赖
pubspec.yaml dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter intl: ^0.19.0 flutter: generate: true
7.2 配置 l10n.yaml
l10n.yaml arb-dir: lib/l10n template-arb-file: app_zh.arb output-localization-file: app_localizations.dart
7.3 ARB 文件
ARB(Application Resource Bundle)文件是 Flutter 官方推荐的翻译文件格式。每种语言一个文件。
lib/l10n/app_zh.arb { "@@locale": "zh", "appTitle": "我的应用", "@appTitle": { "description": "应用标题" }, "welcome": "你好,{name}!", "@welcome": { "placeholders": { "name": { "type": "String" } } }, "itemCount": "{count, plural, =0{没有项目} =1{1 个项目} other{{count} 个项目}}", "@itemCount": { "placeholders": { "count": { "type": "int" } } } }
lib/l10n/app_en.arb { "@@locale": "en", "appTitle": "My App", "welcome": "Hello, {name}!", "itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}" }
7.4 配置 MaterialApp
Dart import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: '国际化示例', localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [ Locale('zh'), // 中文 Locale('en'), // 英文 Locale('ja'), // 日文 ], locale: const Locale('zh'), // 默认语言 home: HomePage(), ); } }
7.5 使用翻译
Dart class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return Scaffold( appBar: AppBar(title: Text(l10n.appTitle)), body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(l10n.welcome('Flutter')), // 你好,Flutter! Text(l10n.itemCount(0)), // 没有项目 Text(l10n.itemCount(1)), // 1 个项目 Text(l10n.itemCount(5)), // 5 个项目 ], ), ); } }
7.6 easy_localization(第三方方案)
easy_localization 提供了更简洁的 API 和运行时语言切换功能,适合快速开发。
Dart import 'package:easy_localization/easy_localization.dart'; // 初始化 void main() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); runApp( EasyLocalization( supportedLocales: const [ Locale('en'), Locale('zh'), Locale('ja'), ], path: 'assets/translations', fallbackLocale: const Locale('en'), child: MyApp(), ), ); } // 配置 MaterialApp MaterialApp( localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, locale: context.locale, home: HomePage(), ) // 在 Widget 中使用 Text('app_title'.tr()) // 简洁的翻译调用 Text('welcome'.tr(args: ['张三'])) // 带参数 Text('items'.plural(5)) // 复数形式 // 运行时切换语言 context.setLocale(const Locale('en')); // 获取当前语言 print(context.locale); // zh
7.7 JSON 翻译文件示例
assets/translations/zh.json { "app_title": "我的应用", "welcome": "你好,{}!", "items": { "zero": "没有项目", "one": "1 个项目", "other": "{} 个项目" }, "settings": { "title": "设置", "language": "语言", "theme": "主题", "dark_mode": "深色模式" } }
🏆 第八章:实践练习
通过以下练习巩固本章所学的进阶技能。建议依次完成,每个练习都会综合运用多个知识点。
📝 练习一:动画登录页
创建一个带有丰富动画效果的登录页面:
- Logo 图标使用 Hero 动画从启动页飞入
- 表单字段使用交错动画依次滑入(从左到右)
- 登录按钮使用 AnimatedContainer 实现点击后缩小为圆形加载状态
- 登录成功后使用 AnimatedSwitcher 切换为成功图标
- 错误提示使用 AnimatedOpacity 渐入渐出
📝 练习二:自定义仪表盘
使用 CustomPaint 绘制一个数据仪表盘:
- 绘制一个半圆形仪表盘(带刻度线和数值标签)
- 使用 Path 绘制指针,支持动画旋转
- 底部绘制一个折线图展示历史数据趋势
- 使用 AnimationController 驱动指针和数据更新动画
- 添加 RepaintBoundary 优化性能,确保帧率稳定在 60fps
📝 练习三:多语言天气应用
构建一个支持中英文切换的天气应用:
- 使用 flutter_localizations 或 easy_localization 实现中英文支持
- 通过 Platform Channel 获取设备当前位置(或模拟数据)
- 天气图标使用 CustomPaint 绘制(太阳、云朵、雨滴)
- 页面切换使用 Hero 动画实现城市卡片到详情的过渡
- 使用 Isolate 在后台解析天气 JSON 数据
- 确保列表使用 ListView.builder 并做好性能优化