像Google那样编写Flutter代码——第二弹

本系列第一部分的反响令人难以置信。许多读者在尝试了一些方法后,分享了他们的想法、经验和问题。但还有更多值得探索的地方。

在过去的一段时间里,我继续分析了 Google 最复杂的 Flutter 实现------从 Material 3 动画到 Flutter Gallery 的性能优化。我发现的模式挑战了常见的 Flutter 实践。

最令人惊讶的发现是? Google 的高级技术往往比你在流行教程中找到的解决方案更简单

让我向你展示我学到的确切内容。

自定义绘制(Custom Painters):考虑Layers,而非Path

社区通常的做法:

dart 复制代码
class ComplexChartPainter extends CustomPainter {
  final List<DataPoint> data;
  final Animation<double> animation;

  ComplexChartPainter({
    required this.data,
    required this.animation,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final path = Path();
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 2.0
      ..style = PaintingStyle.stroke;

    for (int i = 0; i < data.length - 1; i++) {
      final current = data[i];
      final next = data[i + 1];

      final deltaX = next.x - current.x;

      final controlPoint1 = Offset(
        current.x + deltaX * 0.3,
        current.y,
      );
      final controlPoint2 = Offset(
        current.x + deltaX * 0.7,
        next.y,
      );

      path.cubicTo(
        controlPoint1.dx, controlPoint1.dy,
        controlPoint2.dx, controlPoint2.dy,
        next.x, next.y,
      );
    }
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

Google的方式:

dart 复制代码
class ChartPainter extends CustomPainter {
  const ChartPainter({
    required this.data,
    required this.animation,
  });

  final List<DataPoint> data;
  final Animation<double> animation;
  static const int gridLines = 10;
  static const double gridStrokeWidth = 0.5;
  static const double dataStrokeWidth = 2.0;
  static const Color backgroundColor = Color(0xFFF9F9F9);
  static const Color gridColor = Color(0xFFE0E0E0);
  static const Color dataColor = Colors.blue;

  @override
  void paint(Canvas canvas, Size size) {
    _drawBackground(canvas, size);
    _drawGrid(canvas, size);
    _drawData(canvas, size);
  }

  void _drawBackground(Canvas canvas, Size size) {
    final paint = Paint()..color = backgroundColor;
    canvas.drawRect(Offset.zero & size, paint);
  }

  void _drawGrid(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = gridColor
      ..strokeWidth = gridStrokeWidth;

    for (int i = 0; i <= gridLines; i++) {
      final y = size.height * i / gridLines;
      canvas.drawLine(
        Offset(0, y),
        Offset(size.width, y),
        paint,
      );
    }
  }

  void _drawData(Canvas canvas, Size size) {
    if (data.isEmpty) return;

    final paint = Paint()
      ..color = dataColor
      ..strokeWidth = dataStrokeWidth;

    for (int i = 0; i < data.length - 1; i++) {
      canvas.drawLine(
        _dataToOffset(data[i], size),
        _dataToOffset(data[i + 1], size),
        paint,
      );
    }
  }

  Offset _dataToOffset(DataPoint point, Size size) {
    return Offset(
      point.x * size.width,
      size.height - (point.y * size.height),
    );
  }

  @override
  bool shouldRepaint(covariant ChartPainter oldDelegate) {
    return data != oldDelegate.data ||
        animation.value != oldDelegate.animation.value;
  }
}

Google 的原则:

  • 将复杂的绘制器拆分为小巧、专注的方法。
  • 避免不必要的计算。使用常量来消除"魔法数字"。

动画:组合优于复杂

社区典型方法:

dart 复制代码
class ComplexAnimationWidget extends StatefulWidget {
  @override
  State<ComplexAnimationWidget> createState() => _ComplexAnimationWidgetState();
}

class _ComplexAnimationWidgetState extends State<ComplexAnimationWidget>
    with TickerProviderStateMixin {
  late final AnimationController _controller;
  late final AnimationController _secondaryController;
  late final Animation<double> _scaleAnimation;
  late final Animation<double> _rotationAnimation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(milliseconds: 800),
      vsync: this,
    );

    _secondaryController = AnimationController(
      duration: const Duration(milliseconds: 400),
      vsync: this,
    );
    _scaleAnimation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.elasticOut,
    ));
    _rotationAnimation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _secondaryController,
      curve: Curves.easeInOut,
    ));
  }
}

Google的方式:

dart 复制代码
class FadeScaleWidget extends StatefulWidget {
  const FadeScaleWidget({
    super.key,
    required this.child,
    this.duration = const Duration(milliseconds: 300),
  });

  final Widget child;
  final Duration duration;

  @override
  State<FadeScaleWidget> createState() => _FadeScaleWidgetState();
}

class _FadeScaleWidgetState extends State<FadeScaleWidget>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: widget.duration,
      vsync: this,
    )..forward();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.scale(
          scale: _controller.value,
          child: Opacity(
            opacity: _controller.value,
            child: widget.child,
          ),
        );
      },
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

Google 的方法:

  • 构建小巧、可重用的动画 Widget。
  • 通过组合它们来构建复杂的动画------除非必要,否则绝不协调多个控制器。

性能:衡量,不要做假设

社区经常过度使用 const,认为它是万灵药。 Google 团队关注的是实际的运行时性能------而不是代码的表面美观。

社区过度使用 const 的例子:

dart 复制代码
const Card(
  child: Padding(
    padding: EdgeInsets.all(16.0),
    child: Column(
      children: [
        Text('Product Name'), // 这不应该是 `const`,因为它的值是动态的。
      ],
    ),
  ),
);

Google 的实际做法:

dart 复制代码
class ProductCard extends StatelessWidget {
  const ProductCard({
    super.key,
    required this.product,
  });

  final Product product;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            _ProductImage(product: product),
            const SizedBox(height: 8.0),
            Text(product.name),
            Text('$${product.price.toStringAsFixed(2)}'),
          ],
        ),
      ),
    );
  }
}

class _ProductImage extends StatelessWidget {
  const _ProductImage({required this.product});

  final Product product;

  @override
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius: BorderRadius.circular(8.0),
      child: Image.network(
        product.imageUrl,
        loadingBuilder: _buildLoading,
        errorBuilder: _buildError,
      ),
    );
  }

  Widget _buildLoading(
      BuildContext context,
      Widget child,
      ImageChunkEvent? loadingProgress,
      ) {
    if (loadingProgress == null) return child;
    return const Center(child: CircularProgressIndicator());
  }

  Widget _buildError(
      BuildContext context,
      Object error,
      StackTrace? stackTrace,
      ) {
    return const Icon(Icons.error);
  }
}

状态管理:按需选择

Google 不强制使用某一种模式。他们务实地结合使用:

  • StatefulWidget 用于本地 UI 状态
  • Provider(或 InheritedWidget)用于共享应用状态
  • Services 用于业务逻辑

文件组织:按功能划分,而非按层划分

Google 的 Flutter 项目不严格遵循整洁架构(Clean Architecture)。他们按功能进行结构组织:

vbnet 复制代码
lib/
├── home/
│   ├── home_page.dart
│   └── home_service.dart
├── cart/
│   ├── cart_page.dart
│   └── cart_provider.dart
├── shared/
│   ├── widgets/
│   └── services/

"如果你在处理购物车功能,就去 cart/. 文件夹里找。而不是在 3 个不同的文件夹里找。"

为什么?

因为这使得功能更容易维护和上手。

测试:集成优于隔离

Google 更喜欢集成测试,而不是过度模拟的单元测试:

dart 复制代码
testWidgets('User updates profile', (tester) async {
await tester.pumpWidget(const TestApp());

await tester.tap(find.text('Profile'));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(const Key('name_field')), 'New Name');
await tester.tap(find.text('Save'));
await tester.pumpAndSettle();
expect(find.text('Profile updated successfully'), findsOneWidget);
});

性能监控:优先进行Instrument

Google 不会猜测瓶颈,而是进行衡量:

dart 复制代码
class PerformanceWidget extends StatelessWidget {
  const PerformanceWidget({
    super.key,
    required this.name,
    required this.child,
  });

  final String name;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    if (kDebugMode) {
      return _DebugPerformanceWrapper(
        name: name,
        child: child,
      );
    }
    return child;
  }
}

最终思考:简洁致胜

Google 的 Flutter 团队不会为了炫技而编写花哨的代码。 他们编写的是简单、易于维护、能解决实际问题的代码。

这对你意味着什么:

  • 从简单开始。
  • 衡量性能。
  • 测试实际用户流程。
  • 按功能组织代码。
  • 组合小型 Widget。

让我们一起编写更好的 Flutter 代码,也欢迎关注我的公众号:OpenFlutter。

相关推荐
fs哆哆6 分钟前
在VB.net中,函数:列数字转字母
java·服务器·前端·javascript·.net
Hilaku1 小时前
别再手写i18n了!深入浏览器原生Intl对象(数字、日期、复数处理)
前端·javascript·代码规范
每天吃饭的羊1 小时前
强制缓存与协商缓存
前端
缘来小哥1 小时前
Nodejs的多版本管理,不仅仅只是nvm的使用
前端·node.js
陈随易1 小时前
Vite和pnpm都在用的tinyglobby文件匹配库
前端·后端·程序员
LeeAt1 小时前
还在为移动端项目组件发愁?快来试试React Vant吧!
前端·web components
鹏程十八少1 小时前
4. Android 用户狂赞的UI特效!揭秘折叠卡片+流光动画的终极实现方案
前端
Cache技术分享2 小时前
141. Java 泛型 - Java 泛型方法的类型擦除
前端·后端
YGY_Webgis糕手之路2 小时前
OpenLayers 综合案例-基础图层控制
前端·gis
PineappleCoder2 小时前
玩转CSS3新特性:让你的网页会“呼吸”!
前端·css·设计