Member-only story
Write Flutter Like Google
How I Discovered the Flutter Team’s Internal Coding Patterns After 5 Years of “Following Best Practices”
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:
- Keep widgets small and focused
- Use the simplest state management that works
- Name things clearly and concisely
- One widget per file
- Group constants by context
- Handle errors explicitly
- 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 story doesn’t end here
🌱 Enjoyed this? Even a small comment or clap helps grow the next big idea.
Written by Deepak Sharma
Software Engineer | Flutter, Mobile Apps, DSA, AI & Dev Productivity
Comments
Post a Comment