Introduction
Getting Started
- QuickStart
Patterns
- Languages
- Supported Languages
- Python
- Java
- JavaScript
- TypeScript
- Node.js
- React
- Fastify
- Next.js
- Terraform
- C#
- C++
- C
- Go
- Rust
- Swift
- React Native
- Spring Boot
- Kotlin
- Flutter
- Ruby
- PHP
- Scala
- Perl
- R
- Dart
- Elixir
- Erlang
- Haskell
- Lua
- Julia
- Clojure
- Groovy
- Fortran
- COBOL
- Pascal
- Assembly
- Bash
- PowerShell
- SQL
- PL/SQL
- T-SQL
- MATLAB
- Objective-C
- VBA
- ABAP
- Apex
- Apache Camel
- Crystal
- D
- Delphi
- Elm
- F#
- Hack
- Lisp
- OCaml
- Prolog
- Racket
- Scheme
- Solidity
- Verilog
- VHDL
- Zig
- MongoDB
- ClickHouse
- MySQL
- GraphQL
- Redis
- Cassandra
- Elasticsearch
- Security
- Performance
Integrations
- Code Repositories
- Team Messengers
- Ticketing
Enterprise
Flutter is Googles UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase using the Dart programming language.
Flutter, despite its powerful cross-platform capabilities, has several common anti-patterns that can lead to performance issues, maintenance problems, and poor user experience. Here are the most important anti-patterns to avoid when writing Flutter code.
// Anti-pattern: Rebuilding entire widget tree on state change
class CounterPage extends StatefulWidget {
@override
_CounterPageState createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('You have pushed the button this many times:'),
Text('$_counter', style: TextStyle(fontSize: 24)),
ExpensiveWidget(), // Rebuilds unnecessarily
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: Icon(Icons.add),
),
);
}
}
// Better approach: Extract widgets and use const constructors
class CounterPage extends StatefulWidget {
@override
_CounterPageState createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const AppBar(title: Text('Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('You have pushed the button this many times:'),
CounterDisplay(count: _counter),
const ExpensiveWidget(), // Won't rebuild
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: const Icon(Icons.add),
),
);
}
}
class CounterDisplay extends StatelessWidget {
final int count;
const CounterDisplay({Key? key, required this.count}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text('$count', style: const TextStyle(fontSize: 24));
}
}
Rebuilding entire widget trees on state changes causes unnecessary work. Extract widgets, use const
constructors for static widgets, and consider using packages like provider
or flutter_bloc
for more granular rebuilds.
// Anti-pattern: Not using keys for dynamic lists
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index].title),
);
},
)
// Better approach: Use keys for dynamic lists
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
key: ValueKey(items[index].id), // Use unique identifier as key
title: Text(items[index].title),
);
},
)
Not using keys for dynamic lists can lead to unexpected behavior when items are added, removed, or reordered. Always use keys with unique identifiers for list items.
// Anti-pattern: Business logic in widget
class UserProfilePage extends StatefulWidget {
@override
_UserProfilePageState createState() => _UserProfilePageState();
}
class _UserProfilePageState extends State<UserProfilePage> {
User? _user;
bool _isLoading = true;
@override
void initState() {
super.initState();
_fetchUserData();
}
Future<void> _fetchUserData() async {
setState(() => _isLoading = true);
try {
// API call directly in widget
final response = await http.get(Uri.parse('https://api.example.com/user/123'));
if (response.statusCode == 200) {
setState(() {
_user = User.fromJson(jsonDecode(response.body));
_isLoading = false;
});
} else {
throw Exception('Failed to load user');
}
} catch (e) {
setState(() => _isLoading = false);
// Error handling
}
}
@override
Widget build(BuildContext context) {
// UI implementation
}
}
// Better approach: Separate business logic
// UserRepository class
class UserRepository {
Future<User> fetchUser(String id) async {
final response = await http.get(Uri.parse('https://api.example.com/user/$id'));
if (response.statusCode == 200) {
return User.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load user');
}
}
}
// UserViewModel or BLoC
class UserViewModel extends ChangeNotifier {
final UserRepository _repository;
User? user;
bool isLoading = true;
String? error;
UserViewModel(this._repository);
Future<void> fetchUser(String id) async {
isLoading = true;
error = null;
notifyListeners();
try {
user = await _repository.fetchUser(id);
} catch (e) {
error = e.toString();
} finally {
isLoading = false;
notifyListeners();
}
}
}
// Widget using ViewModel
class UserProfilePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => UserViewModel(UserRepository())..fetchUser('123'),
child: Consumer<UserViewModel>(
builder: (context, viewModel, _) {
if (viewModel.isLoading) {
return Center(child: CircularProgressIndicator());
}
if (viewModel.error != null) {
return Center(child: Text('Error: ${viewModel.error}'));
}
return UserProfileView(user: viewModel.user!);
},
),
);
}
}
Mixing business logic with UI makes code hard to test and maintain. Separate concerns using patterns like BLoC, Provider, or Riverpod.
// Anti-pattern: Building lists inefficiently
List<Widget> buildItems() {
List<Widget> widgets = [];
for (var item in items) {
widgets.add(ListTile(title: Text(item.title)));
}
return widgets;
}
@override
Widget build(BuildContext context) {
return ListView(
children: buildItems(),
);
}
// Better approach: Use ListView.builder
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: Text(items[index].title));
},
);
}
Building lists by adding widgets to a list is inefficient for large lists. Use ListView.builder
, GridView.builder
, or other builder constructors for efficient list rendering.
// Anti-pattern: Excessive widget nesting
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: Offset(0, 5),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.all(8),
child: Text(
'Welcome',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(height: 16),
Container(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'This is a sample app',
style: TextStyle(fontSize: 16),
),
),
],
),
),
),
);
}
// Better approach: Extract widgets and reduce nesting
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: WelcomeCard(),
),
);
}
class WelcomeCard extends StatelessWidget {
const WelcomeCard({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
elevation: 5,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: const [
WelcomeHeader(),
SizedBox(height: 16),
WelcomeMessage(),
],
),
),
);
}
}
class WelcomeHeader extends StatelessWidget {
const WelcomeHeader({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
'Welcome',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
);
}
}
class WelcomeMessage extends StatelessWidget {
const WelcomeMessage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
'This is a sample app',
style: TextStyle(fontSize: 16),
);
}
}
Excessive nesting makes code hard to read and maintain. Extract widgets into smaller components and use existing Flutter widgets like Card
instead of custom containers.
// Anti-pattern: Not using const constructors
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
child: Column(
children: [
Icon(Icons.star, size: 50, color: Colors.yellow),
Text('Rating', style: TextStyle(fontSize: 20)),
],
),
);
}
// Better approach: Use const constructors
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
children: const [
Icon(Icons.star, size: 50, color: Colors.yellow),
Text('Rating', style: TextStyle(fontSize: 20)),
],
),
);
}
Not using const
constructors for widgets with immutable properties causes unnecessary rebuilds. Use const
for widgets with fixed properties to improve performance.
// Anti-pattern: Global variables for state
User currentUser;
List<Message> messages = [];
class ChatPage extends StatefulWidget {
@override
_ChatPageState createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
void sendMessage(String text) {
final message = Message(text: text, sender: currentUser);
setState(() {
messages.add(message);
});
}
@override
Widget build(BuildContext context) {
// UI implementation
}
}
// Better approach: Proper state management
class ChatPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => ChatViewModel(),
child: ChatView(),
);
}
}
class ChatViewModel extends ChangeNotifier {
final List<Message> _messages = [];
User? _currentUser;
List<Message> get messages => List.unmodifiable(_messages);
User? get currentUser => _currentUser;
void setUser(User user) {
_currentUser = user;
notifyListeners();
}
void sendMessage(String text) {
if (_currentUser == null) return;
final message = Message(text: text, sender: _currentUser!);
_messages.add(message);
notifyListeners();
}
}
class ChatView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final viewModel = Provider.of<ChatViewModel>(context);
return Scaffold(
appBar: AppBar(title: Text('Chat')),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: viewModel.messages.length,
itemBuilder: (context, index) {
return MessageBubble(message: viewModel.messages[index]);
},
),
),
MessageInput(
onSend: viewModel.sendMessage,
),
],
),
);
}
}
Using global variables or improper state management makes apps hard to maintain and test. Use proper state management solutions like Provider, Riverpod, or BLoC.
// Anti-pattern: Loading all data at once
class ProductsPage extends StatefulWidget {
@override
_ProductsPageState createState() => _ProductsPageState();
}
class _ProductsPageState extends State<ProductsPage> {
List<Product> products = [];
bool isLoading = true;
@override
void initState() {
super.initState();
_loadAllProducts();
}
Future<void> _loadAllProducts() async {
setState(() => isLoading = true);
try {
// Loading all products at once
final response = await http.get(Uri.parse('https://api.example.com/products'));
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
setState(() {
products = data.map((json) => Product.fromJson(json)).toList();
isLoading = false;
});
}
} catch (e) {
setState(() => isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Products')),
body: isLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
return ProductListItem(product: products[index]);
},
),
);
}
}
// Better approach: Use pagination/lazy loading
class ProductsPage extends StatefulWidget {
@override
_ProductsPageState createState() => _ProductsPageState();
}
class _ProductsPageState extends State<ProductsPage> {
final List<Product> products = [];
bool isLoading = false;
bool hasMore = true;
int page = 1;
final int pageSize = 20;
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_loadMoreProducts();
_scrollController.addListener(_scrollListener);
}
@override
void dispose() {
_scrollController.removeListener(_scrollListener);
_scrollController.dispose();
super.dispose();
}
void _scrollListener() {
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent * 0.8 &&
!isLoading && hasMore) {
_loadMoreProducts();
}
}
Future<void> _loadMoreProducts() async {
if (isLoading || !hasMore) return;
setState(() => isLoading = true);
try {
final response = await http.get(
Uri.parse('https://api.example.com/products?page=$page&limit=$pageSize'),
);
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
final newProducts = data.map((json) => Product.fromJson(json)).toList();
setState(() {
products.addAll(newProducts);
isLoading = false;
page++;
hasMore = newProducts.length == pageSize;
});
}
} catch (e) {
setState(() => isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Products')),
body: ListView.builder(
controller: _scrollController,
itemCount: products.length + (hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == products.length) {
return Center(child: CircularProgressIndicator());
}
return ProductListItem(product: products[index]);
},
),
);
}
}
Loading all data at once can cause performance issues and poor user experience. Implement pagination or infinite scrolling for large data sets.
// Anti-pattern: Not optimizing image loading
Image.network('https://example.com/large_image.jpg')
// Better approach: Use cached_network_image
CachedNetworkImage(
imageUrl: 'https://example.com/large_image.jpg',
placeholder: (context, url) => Center(child: CircularProgressIndicator()),
errorWidget: (context, url, error) => Icon(Icons.error),
fit: BoxFit.cover,
)
Not optimizing image loading can lead to poor performance and user experience. Use packages like cached_network_image
for efficient image loading, caching, and error handling.
// Anti-pattern: Hardcoded styles
Text(
'Hello World',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
)
// Better approach: Use theme
Text(
'Hello World',
style: Theme.of(context).textTheme.headline5?.copyWith(
color: Theme.of(context).primaryColor,
),
)
// Even better: Define a consistent theme
MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
textTheme: TextTheme(
headline5: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
// Other text styles
),
// Other theme properties
),
home: MyHomePage(),
)
Hardcoding styles makes it difficult to maintain a consistent look and feel. Use Flutter’s theming system to define and apply consistent styles throughout your app.
// Anti-pattern: Poor error handling
Future<void> fetchData() async {
final response = await http.get(Uri.parse('https://api.example.com/data'));
final data = jsonDecode(response.body);
// No error handling
}
// Better approach: Proper error handling
Future<Result<Data>> fetchData() async {
try {
final response = await http.get(Uri.parse('https://api.example.com/data'));
if (response.statusCode == 200) {
final data = Data.fromJson(jsonDecode(response.body));
return Result.success(data);
} else {
return Result.failure(
HttpError(
statusCode: response.statusCode,
message: 'Failed to fetch data',
),
);
}
} on SocketException {
return Result.failure(ConnectionError('No internet connection'));
} on FormatException {
return Result.failure(ParseError('Invalid response format'));
} catch (e) {
return Result.failure(UnknownError(e.toString()));
}
}
// Result class
class Result<T> {
final T? data;
final AppError? error;
Result.success(this.data) : error = null;
Result.failure(this.error) : data = null;
bool get isSuccess => error == null;
bool get isFailure => error != null;
}
// Usage
FutureBuilder<Result<Data>>(
future: fetchData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return ErrorView(message: 'Unexpected error: ${snapshot.error}');
}
final result = snapshot.data;
if (result == null || result.isFailure) {
return ErrorView(
error: result?.error ?? UnknownError('No data received'),
onRetry: () => setState(() {}),
);
}
return DataView(data: result.data!);
},
)
Poor error handling leads to crashes and poor user experience. Implement proper error handling with specific error types and user-friendly error messages.
// Anti-pattern: Everything in one file
// main.dart with models, services, widgets, etc.
// Better approach: Proper code organization
// Project structure
// lib/
// ├── main.dart
// ├── app.dart
// ├── models/
// │ ├── user.dart
// │ └── product.dart
// ├── services/
// │ ├── api_service.dart
// │ └── auth_service.dart
// ├── screens/
// │ ├── home_screen.dart
// │ └── profile_screen.dart
// ├── widgets/
// │ ├── product_card.dart
// │ └── custom_button.dart
// └── utils/
// ├── constants.dart
// └── helpers.dart
Poor code organization makes it difficult to maintain and scale your app. Organize your code into a clear directory structure with separate files for models, services, screens, and widgets.
// Anti-pattern: Manual navigation management
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ProductDetailPage(product: product)),
);
}
// Better approach: Named routes
// In MaterialApp
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/products': (context) => ProductsPage(),
'/product_detail': (context) => ProductDetailPage(),
'/cart': (context) => CartPage(),
},
)
// Navigation
onPressed: () {
Navigator.pushNamed(
context,
'/product_detail',
arguments: product,
);
}
// In ProductDetailPage
@override
Widget build(BuildContext context) {
final product = ModalRoute.of(context)!.settings.arguments as Product;
// Use product
}
// Even better: Use a navigation package like go_router or auto_route
Manual navigation management can lead to inconsistent navigation behavior. Use named routes or a navigation package for more structured navigation.