Skip to content

Instantly share code, notes, and snippets.

@evaisse
Last active September 30, 2025 11:42
Show Gist options
  • Select an option

  • Save evaisse/bde608cfb0bb463515bf7674d3fbc8d6 to your computer and use it in GitHub Desktop.

Select an option

Save evaisse/bde608cfb0bb463515bf7674d3fbc8d6 to your computer and use it in GitHub Desktop.

Google Way of Dart

Copyright :

There are countless Flutter coding conventions floating around. From the popular “Flutter Clean Architecture” to community-driven style guides, developers have been following external conventions for years. I was one of them.

For the past 5 years, I followed community best practices, enforced strict linting rules, and structured my Flutter apps according to popular Medium articles and YouTube tutorials. My code was “clean,” and my architecture was “proper.”

But everything changed when I started studying how Google’s Flutter team actually writes Flutter code.

The revelation was shocking: Google’s Flutter team follows patterns that completely contradict popular community conventions.

Not a Medium member? You can read this article for free right here 👇!🌱Your clap or comment might just spark the next idea.

In this article, I’ll share the internal coding patterns I discovered by analyzing 50+ Flutter framework files, Google’s own Flutter apps, and contributions from core Flutter team members.

Widget Composition Over Complex Inheritance

The Community Way:

abstract class BaseWidget extends StatelessWidget {  
  final String title;  
  final EdgeInsets padding;  
    
  const BaseWidget({  
    required this.title,  
    this.padding = EdgeInsets.zero,  
  });  
    
  @override  
  Widget build(BuildContext context) {  
    return buildContent(context);  
  }  
    
  Widget buildContent(BuildContext context);  
}  
  
class MyWidget extends BaseWidget {  
  const MyWidget({required super.title});  
    
  @override  
  Widget buildContent(BuildContext context) {  
    return Container(  
      padding: padding,  
      child: Text(title),  
    );  
  }  
}

The Google Way:

class MyWidget extends StatelessWidget {  
  const MyWidget({  
    super.key,  
    required this.title,  
  });  
    
  final String title;  
    
  @override  
  Widget build(BuildContext context) {  
    return _buildContent(context);  
  }  
    
  Widget _buildContent(BuildContext context) {  
    return Container(  
      padding: const EdgeInsets.all(16.0),  
      child: Text(title),  
    );  
  }  
}

Google’s Flutter team avoids complex inheritance hierarchies in favor of composition. They prefer small, focused widgets that do one thing well, rather than creating elaborate base classes that try to solve multiple problems.

Real-world example:

// Instead of creating a BaseCard widget  
class ProductCard extends StatelessWidget {  
  const ProductCard({  
    super.key,  
    required this.product,  
  });  
    
  final Product product;  
    
  @override  
  Widget build(BuildContext context) {  
    return Card(  
      child: Column(  
        children: [  
          _buildImage(),  
          _buildTitle(),  
          _buildPrice(),  
        ],  
      ),  
    );  
  }  
  
  Widget _buildImage() => Image.network(product.imageUrl);  
  Widget _buildTitle() => Text(product.name);  
  Widget _buildPrice() => Text('\$${product.price}');  
}

State Management: Keep It Simple, Stupid

The Community Obsession:

// Everyone's doing this  
class CounterBloc extends Bloc<CounterEvent, CounterState> {  
  CounterBloc() : super(CounterInitial()) {  
    on<CounterIncrement>(_onIncrement);  
    on<CounterDecrement>(_onDecrement);  
  }  
    
  void _onIncrement(CounterIncrement event, Emitter<CounterState> emit) {  
    emit(CounterValue(state.value + 1));  
  }  
    
  void _onDecrement(CounterDecrement event, Emitter<CounterState> emit) {  
    emit(CounterValue(state.value - 1));  
  }  
}

The Google Reality:

// Google's Flutter team does this  
class CounterWidget extends StatefulWidget {  
  const CounterWidget({super.key});  
    
  @override  
  State<CounterWidget> createState() => _CounterWidgetState();  
}  
  
class _CounterWidgetState extends State<CounterWidget> {  
  int _counter = 0;  
    
  void _increment() {  
    setState(() {  
      _counter++;  
    });  
  }  
    
  @override  
  Widget build(BuildContext context) {  
    return Column(  
      children: [  
        Text('$_counter'),  
        ElevatedButton(  
          onPressed: _increment,  
          child: const Text('Increment'),  
        ),  
      ],  
    );  
  }  
}

After analyzing Google’s Flutter codebase, I discovered that the Flutter team uses StatefulWidget and setState far more than any complex state management solution. They reserve advanced state management for genuinely complex scenarios, not simple UI state.

The principle: Use the simplest solution that works. Don’t over-engineer.

Method Naming: Action-Oriented, Not Description-Oriented

The Community Pattern:

class UserService {  
  Future<User> getUserById(String id) async {}  
  Future<void> updateUserProfile(User user) async {}  
  Future<void> deleteUserAccount(String id) async {}  
}

The Google Pattern:

class UserService {  
  Future<User> fetch(String id) async {}  
  Future<void> update(User user) async {}  
  Future<void> delete(String id) async {}  
}

Google’s Flutter team prefers concise, action-oriented method names where context is clear. The context (UserService) already tells you it’s about users, so methods focus on the action: fetch, update, delete. However, they don't sacrifice clarity for brevity - when ambiguity could arise, they still use descriptive names like fetchUserById() or getAuthToken().

Real-world example:

class CartService {  
  Future<void> add(Product product) async {}  
  Future<void> remove(String productId) async {}  
  Future<void> clear() async {}  
  Future<double> total() async {}  
}

Widget File Structure: One Widget, One Purpose

The Community Structure:

// user_widgets.dart  
class UserCard extends StatelessWidget {}  
class UserList extends StatelessWidget {}  
class UserProfile extends StatelessWidget {}  
class UserSettings extends StatelessWidget {}

The Google Structure:

// user_card.dart  
class UserCard extends StatelessWidget {}  
  
// user_list.dart    
class UserList extends StatelessWidget {}  
  
// user_profile.dart  
class UserProfile extends StatelessWidget {}  
  
// user_settings.dart  
class UserSettings extends StatelessWidget {}

Google’s Flutter team follows a strict one-widget-per-file rule. Each widget lives in its own file.

  • This follows the Single Responsibility Principle: one file, one widget, one purpose.
  • its own file, making the codebase more maintainable and easier to navigate.

Constants: Context-Driven Grouping

The Community Way:

// constants.dart  
class AppConstants {  
  static const double defaultPadding = 16.0;  
  static const double defaultRadius = 8.0;  
  static const Color primaryColor = Colors.blue;  
  static const String appName = 'MyApp';  
  static const int maxRetries = 3;  
}

The Google Way:

// ui_constants.dart  
class UiConstants {  
  static const double defaultPadding = 16.0;  
  static const double defaultRadius = 8.0;  
}  
  
// theme_constants.dart  
class ThemeConstants {  
  static const Color primaryColor = Colors.blue;  
  static const Color secondaryColor = Colors.grey;  
}  
  
// network_constants.dart  
class NetworkConstants {  
  static const int maxRetries = 3;  
  static const Duration timeout = Duration(seconds: 30);  
}

Google groups constants by context, not by putting everything in one massive file. This makes it easier to find and maintain related constants.

Error Handling: Explicit and Immediate

The Community Pattern:

class ApiService {  
  Future<Result<User>> getUser(String id) async {  
    try {  
      final user = await _fetchUser(id);  
      return Success(user);  
    } catch (e) {  
      return Failure(e.toString());  
    }  
  }  
}

The Google Pattern:

class ApiService {  
  Future<User> getUser(String id) async {  
    final response = await _httpClient.get('/users/$id');  
      
    if (response.statusCode != 200) {  
      throw ApiException('Failed to fetch user: ${response.statusCode}');  
    }  
      
    return User.fromJson(response.data);  
  }  
}

Google’s Flutter team prefers explicit error handling with exceptions rather than wrapping everything in Result types. They let errors bubble up naturally and handle them at the appropriate level.

Widget Testing: Behavior Over Implementation

The Community Focus:

testWidgets('Counter increments smoke test', (tester) async {  
  await tester.pumpWidget(const MyApp());  
    
  // Verify initial state  
  expect(find.text('0'), findsOneWidget);  
  expect(find.text('1'), findsNothing);  
    
  // Tap increment button  
  await tester.tap(find.byIcon(Icons.add));  
  await tester.pump();  
    
  // Verify state changed  
  expect(find.text('0'), findsNothing);  
  expect(find.text('1'), findsOneWidget);  
});

The Google Focus:

testWidgets('User can increment counter', (tester) async {  
  await tester.pumpWidget(const MyApp());  
    
  // User sees initial counter  
  expect(find.text('0'), findsOneWidget);  
    
  // User taps increment  
  await tester.tap(find.byIcon(Icons.add));  
  await tester.pump();  
    
  // User sees updated counter  
  expect(find.text('1'), findsOneWidget);  
});

Google’s app-level widget tests focus on user behavior rather than implementation details. While framework internals still require implementation-level testing (like render objects or layout behaviors), their app tests from projects like Flutter Gallery emphasize user experience over code structure.

The Bottom Line

After studying Google’s Flutter patterns, I realized that many developers (myself included) have been overcomplicating Flutter development. That’s not to say community patterns are wrong — they’re valuable for large teams, regulated industries, or enterprise apps. But Google’s approach is refreshingly simple because their teams prioritize simplicity and know when to optimize.

The key patterns:

  1. Keep widgets small and focused
  2. Use the simplest state management that works
  3. Name things clearly and concisely
  4. One widget per file
  5. Group constants by context
  6. Handle errors explicitly
  7. Test user behavior in app-level tests

These patterns have transformed how I write Flutter code. My apps are more maintainable, my team is more productive, and debugging is significantly easier.

The insight? Community conventions aren’t wrong they’re just heavier than needed for many apps. Google’s internal teams can afford to keep it simple because they have deep framework knowledge and know when complexity is truly justified.

The response to Part 1 of this series has been incredible. Many readers shared their thoughts, experiences, and questions after trying out some of the approaches. But there’s still more to explore.

Over the past month, I’ve continued analyzing Google’s most complex Flutter implementations — from Material 3 animations to Flutter Gallery’s performance optimizations. The patterns I found challenge common Flutter practices.

The most surprising discovery?
Google’s advanced techniques are often simpler than the solutions you’ll find in popular tutorials.

Let me show you exactly what I learned.

Not a Medium member? You can read this article for free right here 👇!👋 If this helped, clap or comment — it might spark the next idea.

Custom Painters: Think in Layers, Not Paths

What the Community Usually Does:

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;  
}

The Google Approach:

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’s principle:

Break complex painters into small, focused methods.
Avoid unnecessary calculations. Use constants to eliminate “magic numbers.”

Animations: Composition Over Complexity

Typical Community Approach:

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,  
    ));  
  }  
}

The Google Way:

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’s approach:
Build small, reusable animation widgets.
Compose complex animations by combining them — never orchestrate multiple controllers unless necessary.

Performance: Measure, Don’t Assume

The community often overuses const, thinking it’s the silver bullet.
Google’s team focuses on actual runtime performance—not code cosmetics.

Community Over-Const Example:

const Card(  
  child: Padding(  
    padding: EdgeInsets.all(16.0),  
    child: Column(  
      children: [  
        Text('Product Name'), // This shouldn't be const because it's dynamic  
      ],  
    ),  
  ),  
);

Google’s Real Practice:

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);  
  }  
}

State Management: Use What Fits

Google doesn’t force one pattern. They pragmatically combine:

  • StatefulWidget for local UI state
  • Provider (or InheritedWidget) for shared app state
  • Services for business logic

File Organization: By Feature, Not Layer

Google’s Flutter projects don’t follow clean architecture strictly. They structure by feature:

lib/
├── home/
│ ├── home_page.dart
│ └── home_service.dart
├── cart/
│ ├── cart_page.dart
│ └── cart_provider.dart
├── shared/
│ ├── widgets/
│ └── services/

“If you’re working on cart, look in cart/. Not in 3 different folders.”

Why?
Because this makes features easier to maintain and onboard.

Testing: Integration Over Isolation

Google prefers integration tests over over-mocked unit tests:

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);  
});

Performance Monitoring: Instrument First

Instead of guessing bottlenecks, Google measures:

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;  
  }  
}

Final Thoughts: Simplicity Wins

Google’s Flutter team doesn’t write fancy code to impress.
They write simple, maintainable code that solves real problems.

What This Means for You:

  • Start simple.
  • Measure performance.
  • Test actual user flows.
  • Organize by feature.
  • Compose small widgets.

Let’s write better Flutter together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment