Designing Better Flutter UIs with Custom Widgets: Quick Tips and Tricks

Akash Jaiswal
7 min readMay 3, 2023

--

Hey there adventure seekers, buckle up because we’re about to take a wild ride into the world of Flutter widgets! If you’re like me, you’ve probably found yourself scratching your head and wondering why it’s so important to use widgets in Flutter. Well, let me tell you, my friend, it’s a big deal.

When I first started learning Flutter, I dove headfirst into coding without giving much thought to the structure of my UI. I worked on a ton of projects with code that was difficult to understand, handle, and modify. It felt like I was wandering aimlessly through a jungle of confusing software architecture.

But then I had a breakthrough. I realized that the reason so many people struggle with widgets in Flutter is that they’re coming at it from a background in HTML, CSS, and XML. It’s easy to fall into the trap of building a Flutter UI the same way you would build a web page.

For example, let’s say you’re tasked with building two squares in HTML. You’d probably use the following code:

<div class="squarestyle"></div>
<div class="squarestyle"></div>

Now, if you were asked to do the same thing in Flutter, you might be tempted to write something like this:

Column(
children: [
Container(
//code for square
),
Container(
//code for square
),
],
)

And that’s where things start to go wrong. You see, the problem with this approach is that it’s based on the flawed assumption that Flutter widgets are similar to HTML tags or elements. In reality, all Flutter widgets are classes. Flutter is essentially a group of pre-built classes that we can import and inherit from to create our own custom widgets.

So, let’s put all this technical philosophy aside and look at a simple example of how we can build a Flutter UI with our own custom widget. By using OOP concepts properly, we can take full advantage of all the awesome features that Flutter has to offer.

Now, let’s take a look at a practical example of how we can use custom widgets to create a more readable and maintainable UI.

Consider the following code for an example screen:

// example_screen.dart

class ExampleScreen extends StatefulWidget {
const ExampleScreen({super.key});

@override
State<ExampleScreen> createState() => _ExampleScreenState();
}

class _ExampleScreenState extends State<ExampleScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
height: 150,
width: 150,
color: Colors.amber,
),
Container(
height: 150,
width: 150,
color: Colors.red,
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
height: 150,
width: 150,
color: Colors.blue,
),
Container(
height: 150,
width: 150,
color: Colors.green,
),
],
),
],
),
);
}
}

At first glance, this code might not seem too difficult to understand. But as the complexity of the UI grows, so does the complexity of the code. It becomes harder and harder to modify and maintain the code as it grows.

Let’s take a look at a modified version of the code that utilizes custom widgets to make the UI more readable and maintainable:

// example_screen.dart
class ExampleScreen extends StatefulWidget {
const ExampleScreen({super.key});

@override
State<ExampleScreen> createState() => _ExampleScreenState();
}


class _ExampleScreenState extends State<ExampleScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body:
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
PairOfColoredBoxWidget(
boxColorFirst: Colors.amber,
boxColorSecond: Colors.red,
),
PairOfColoredBoxWidget(
boxColorFirst: Colors.blue,
boxColorSecond: Colors.green
)
]),
);
}
}

// pair_of_colored_box_widget.dart
class PairOfColoredBoxWidget extends StatelessWidget {
const PairOfColoredBoxWidget({
super.key,
required this.boxColorFirst,
required this.boxColorSecond,
});
final Color boxColorFirst;
final Color boxColorSecond;

@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ColoredBoxWidget(
boxColor: boxColorFirst,
),
ColoredBoxWidget(boxColor: boxColorSecond)
],
);
}
}

// colored_box_widget.dart
class ColoredBoxWidget extends StatelessWidget {
const ColoredBoxWidget({super.key, required this.boxColor});
final Color boxColor;

@override
Widget build(BuildContext context) {
return Container(
height: 150,
width: 150,
color: boxColor,
);
}
}

In the modified code, we’ve leveraged custom widgets to make the code more readable and maintainable. Custom widgets like ‘PairOfColoredBoxWidget’ and ‘ColoredBoxWidget’ are reusable components that can be used throughout the app.

By using custom widgets, we can simplify the code and make it easier to understand and modify. In addition, custom widgets promote code reuse, which can potentially improve performance by minimizing unnecessary widget rebuilding. In Flutter, whenever a widget is rebuilt, all of its child widgets are also rebuilt. Therefore, using custom widgets can reduce unnecessary widget rebuilding and improve performance.

Here’s how the UI output looks like when rendered on a device:

Are you still hesitant to dive into custom widgets because you think they’re too complicated and make your UI harder to read? I understand where you’re coming from, but let me tell you, custom widgets are the backbone of Flutter development. Not only do they promote code reuse, but they also make your code easier to maintain and understand in the long run.

But I know you might still have some lingering doubts about custom widgets, like how to determine the right level of widget granularity, how to track them across your project, and what properties to pass in. Don’t fret! The best way to get over these doubts is by working on a real-world, large-scale project that’s used by actual users.

To further illustrate the benefits of custom widgets, let’s take a scenario where you have to modify the code for making four squares without custom widgets. Imagine you get a new requirement that all the squares should have rounded corners.

To fulfill this requirement, you modified the code which is not using custom widgets like this:

// example_screen.dart

class ExampleScreen extends StatefulWidget {
const ExampleScreen({super.key});

@override
State<ExampleScreen> createState() => _ExampleScreenState();
}

class _ExampleScreenState extends State<ExampleScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
height: 150,
width: 150,
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.circular(10)),
),
Container(
height: 150,
width: 150,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10)),
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
height: 150,
width: 150,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10)),
),
Container(
height: 150,
width: 150,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(10)),
)
],
),
]),
);
}
}

You just have to modify each container one by one, which may seem like an easy copy-paste task. However, if you have similar designs on 10 to 20 screens, it will become difficult to keep track of all the changes, and testing will become more challenging. Additionally, manual testing of each screen will be necessary, and new test cases will have to be written for each of these changes.

Now, let’s do the same thing with the code which uses custom widgets:

// example_screen.dart

class ExampleScreen extends StatefulWidget {
const ExampleScreen({super.key});

@override
State<ExampleScreen> createState() => _ExampleScreenState();
}

class _ExampleScreenState extends State<ExampleScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body:
Column(mainAxisAlignment: MainAxisAlignment.center,
children: const [
PairOfColoredBoxWidget(
boxColorFirst: Colors.amber,
boxColorSecond: Colors.red,
),
PairOfColoredBoxWidget(
boxColorFirst: Colors.blue, boxColorSecond: Colors.green)
]),
);
}
}

// pair_of_colored_box_widget.dart
class PairOfColoredBoxWidget extends StatelessWidget {
const PairOfColoredBoxWidget({
super.key,
required this.boxColorFirst,
required this.boxColorSecond,
});
final Color boxColorFirst;
final Color boxColorSecond;

@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ColoredBoxWidget(
boxColor: boxColorFirst,
),
ColoredBoxWidget(boxColor: boxColorSecond)
],
);
}
}

// colored_box_widget.dart
class ColoredBoxWidget extends StatelessWidget {
const ColoredBoxWidget({super.key, required this.boxColor});
final Color boxColor;

@override
Widget build(BuildContext context) {
return Container(
height: 150,
width: 150,
decoration: BoxDecoration(
color: boxColor, borderRadius: BorderRadius.circular(10)),
);
}
}

Here we just have to make a single change in class ColoredBoxWidget class that’s it whichever others classes use ColoredBoxWidget will be automatically got changed it’s now doesn’t matter 20 screen or 100 screen using this widget the change will automatically will be there.

Here’s how the UI output looks like when rendered on a device:

In the world of Flutter, creating custom widgets is like forging your own weapons and armor for the battle ahead. As a Flutter developer, it’s an essential skill that will help you conquer the most complex UI challenges and create stunning, reusable components that will stand the test of time.

Imagine you’re facing a horde of widgets, each with its own properties and behaviors. Without custom widgets, you’ll have to fight them one by one, trying to keep track of all the changes and testing them manually. But with custom widgets, you can create your own powerful arsenal of UI components, each with a unique set of abilities and styles, ready to take on any challenge.

And the best part? Once you craft your custom widget, it will become your faithful companion throughout your app, always ready to adapt to your needs and evolve with your app. Whether you need to add a new feature, fix a bug, or simply improve the design, your custom widget will be there, waiting for your command.

But beware, young adventurer, for the path to mastering custom widgets is not an easy one. You’ll need to learn advanced techniques like Inherited Widget and SOLID principles, and practice, practice, practice until you reach the highest level of skill.

But fear not, for the rewards of mastering custom widgets are great. You’ll be able to create stunning UIs with ease, organize your codebase like a true master, and scale your app to new heights.

So, are you ready to embark on this epic journey? Gather your tools, sharpen your skills, and let’s create some custom widgets that will change the face of Flutter forever! Thank you for reading this blog, and happy coding!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Akash Jaiswal
Akash Jaiswal

Responses (1)

Write a response