Common Mistakes in Flutter and How to Fix Them

Muhammad Younas
5 min readDec 29, 2023

--

Photo by olia danilevich: https://www.pexels.com/photo/man-sitting-in-front-of-three-computers-4974915/

Hey there, Flutter family! Today, let’s talk about some tricky Flutter blunders that can trip you up. We’ll dive into these pitfalls and give you some tips to breeze through Flutter development like a boss.

1. Misusing setState() - A Beginner's Blunder

Mistake:
Beginner Error: Using setState() excessively for even minor state changes in a Flutter app can lead to unnecessary widget tree rebuilds, impacting app performance and code readability.

Solution:
Technical Fix: Adopt more efficient state management approaches. Utilize state management packages like Provider or Riverpod to segregate business logic from UI, minimizing unnecessary widget updates.

Bad Approach:

class CounterApp extends StatefulWidget {
@override
_CounterAppState createState() => _CounterAppState();
}

class _CounterAppState extends State<CounterApp> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++; // Incrementing counter using setState
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Counter App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$_counter'),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment'),
),
],
),
),
);
}
}

Improved Approach (Using Provider):

// Define a ChangeNotifier class
class CounterModel extends ChangeNotifier {
int _counter = 0;

int get counter => _counter;

void increment() {
_counter++;
notifyListeners(); // Notify listeners on change
}
}

// Use Provider for state management
class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => CounterModel(),
child: CounterScreen(),
);
}
}

class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
var counter = Provider.of<CounterModel>(context);

return Scaffold(
appBar: AppBar(
title: Text('Counter App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('${counter.counter}'),
ElevatedButton(
onPressed: counter.increment,
child: Text('Increment'),
),
],
),
),
);
}
}

2. Using global variables and singletons

Mistake: Using global variables or singletons (objects accessible from anywhere in your code) might initially seem convenient, but they can lead to several problems:

  1. Memory Leaks: Global variables and singletons can occupy memory permanently, even when they’re not actively used, potentially causing memory leaks.
  2. Coupling: They create strong dependencies between different parts of your code, making it harder to test, reuse, or modify, resulting in tightly coupled code.
  3. Conflicts: Simultaneous access or modification of global variables by multiple code sections can cause conflicts and bugs, leading to unexpected behavior in your app.

Solution:
Technical Fix: Implement dependency injection (DI) methodologies using tools like GetIt or injectable. These facilitate passing dependencies to widgets or classes directly, enhancing code modularity and testability while avoiding tight coupling.

Bad Approach:

// Using a global variable
int globalCounter = 0;

class GlobalCounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Global Counter App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$globalCounter'),
ElevatedButton(
onPressed: () {
globalCounter++; // Modifying the global counter
},
child: Text('Increment'),
),
],
),
),
);
}
}

Improved Approach (Using GetIt for Dependency Injection):

// Using GetIt for dependency injection
GetIt getIt = GetIt.instance;

class CounterService {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
}
}
void setup() {
getIt.registerSingleton(CounterService());
}

class GetItCounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
setup(); // Setup GetIt
var counterService = getIt<CounterService>();
return Scaffold(
appBar: AppBar(
title: Text('GetIt Counter App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('${counterService.counter}'),
ElevatedButton(
onPressed: () {
counterService.increment(); // Increment using service
},
child: Text('Increment'),
),
],
),
),
);
}
}

In this improved approach, the CounterService handles the counter logic, and GetIt is used for dependency injection. It allows accessing the CounterService instance wherever needed without creating global variables or singletons directly. This practice makes the code more organized, easier to test, and reduces the risks associated with global state management.

3. Ignoring the context

Understanding and using BuildContext correctly is crucial. Incorrect usage might lead to unexpected errors. Always ensure to use it properly within the allowed scope.

Incorrect usage of context:

@override 
void initState() {
super.initState(); // Incorrect usage of context here
MediaQuery.of(context).size.width;
} //

Correct usage:

@override
Widget build(BuildContext context) {
// Correct usage of context within build method
MediaQuery.of(context).size.width;
}

4. Using too many nested widgets

In Flutter, think of widgets as LEGO blocks for making your app. You can put them together to create cool stuff. But sometimes, using too many widgets inside each other can make your app slow, your code messy, and fixing things a headache.

What’s the problem?

  1. Performance: Loads of nested widgets can make your app slower and use up more phone memory.
  2. Readability: Too many widgets inside one another can make your code hard to read, like a book missing paragraphs.
  3. Maintenance: Changing things in a deeply nested widget setup can be like finding a needle in a haystack — tough!

How to fix it?

  1. Extracting Widgets: Imagine you have a bunch of LEGO pieces put together. Taking some out and making a new set can make things simpler. In code, you can take parts of your app and put them into their own widget, then use them in different places. Let me show you:

Before:

Widget build(BuildContext context) {
return Container(
child: Row(
children: [
Text('Hello'),
Text('World'),
],
),
);
}

After:

Widget helloWorldWidget() {
return Row(
children: [
Text('Hello'),
Text('World'),
],
);
}

Widget build(BuildContext context) {
return Container(
child: helloWorldWidget(), // Now using our new widget!
);
}
  1. Custom Widgets: You can create your own LEGO block with a name and rules. Similarly, you can make your custom widget to do a specific job. This makes your code cleaner and easier to understand.
  2. Using Layout Widgets: Just like using a special LEGO piece for a turn or a twist, Flutter has layout widgets that help organize your widgets without making a big mess. Widgets like Row, Column, Flex, or Wrap can help you arrange your widgets neatly.

5. Not Following Best Practices — A Common Pitfall

Sometimes, Flutter developers skip certain guidelines recommended by the Flutter team and the community. While these practices aren’t mandatory, they play a crucial role in writing cleaner code and avoiding common mistakes.

  1. Using const Constructors:
  • Purpose: const constructors create immutable instances of widgets that can be cached by Flutter, enhancing performance and memory efficiency, especially for static widgets.
  • Example:
// Without const constructor
Widget build(BuildContext context) {
return Container(
child: Text('Hello'),
);
}

// With const constructor
Widget build(BuildContext context) {
return Container(
child: const Text('Hello'), // Using const constructor for Text widget
);
}

2. Utilizing Keys:

  • Purpose: Keys are identifiers crucial for tracking and updating widgets in the widget tree. They preserve the state or identity of widgets across rebuilds, aiding in scenarios like animations, lists, or forms.
  • Example:
// Without keys
ListView.builder(
itemCount: items.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(items[index]),
);
},
);

// With keys
ListView.builder(
itemCount: items.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
key: Key(items[index]), // Using keys for list items
title: Text(items[index]),
);
},
);

3. Using Linting and Formatting Tools:

  • Purpose: Linting and formatting tools (e.g., dartfmt, dartanalyzer, pedantic) ensure adherence to the Dart style guide and effective Dart rules, improving code quality, consistency, and readability.
  • Usage: Integrate tools into your workflow to automatically format and analyze code for better quality assurance.

Ready to implement these insights? Let’s ensure your Flutter apps stay predictable and efficient! 🌟✨

If you found this guide helpful, connect with me on LinkedIn and Buy me a Coffee. Happy coding! 🚀

--

--

Muhammad Younas

Experienced mobile developer and team lead with a passion for delivering high-quality applications.