Flutter has been getting more and more popular lately. You can use it to build complex applications that work smoothly on MacOS, Windows, and Linux.

But building these applications is not always a simple process. You often have to refactor your code to maintain the app’s performance.

One such refactoring technique is extracting duplicated code and components and reusing them in multiple places.

In this tutorial, you'll learn how to replace a duplicated component by building a custom widget in Flutter.

What is a Custom Widget?

In Flutter, a custom widget refers to a user-defined widget that encapsulates a specific set of functionalities or visual representations.

Custom widgets are the building blocks of a Flutter application. They allow developers to create reusable UI components that can be used throughout the application.

If you're switching from React Native, you can think about custom widgets as custom React components. And what we call props in React are called parameters in Flutter.

Why Use Custom Widgets?

Custom widgets help you encapsulate complex UI elements. They also promote code re-usability and enhance the maintainability of your Flutter applications.

There are a number of reasons to build build custom widgets in Flutter. Let's look at some of them.

Code Reusability

Custom widgets allow developers to encapsulate complex functionality and appearance into reusable components.

Once created, custom widgets can be used multiple times throughout the application, reducing code duplication and promoting a modular development approach.

Maintainability

Custom widgets contribute to the maintainability of the codebase. By encapsulating specific functionality or visual representation, custom widgets create a separation of concerns. This separation makes it easier to locate, modify, and debug code related to a particular UI component.

Consistent UI

They also enable developers to define a consistent and unified UI design across their application.

Abstraction

And finally, custom widgets provide a level of abstraction that hides the implementation details and complexity of a particular UI element.

You can create high-level widgets that expose a simplified interface and handle the internal logic. This allows other developers to use the widget without worrying about its internal workings. This abstraction promotes modularity, making it easier to understand, test, and maintain the code.

How to Build a Custom Widget in Flutter

Let’s start building our custom widget.

Clone the Repo

Instead of starting from the scratch, I’ve created a Flutter app in GitHub and added duplicated code/components in that repo. Let’s begin from there.

Pull the code from GitHub by running the below command:

git clone https://github.com/5minslearn/Flutter-Custom-Widget.git

or

git clone git@github.com:5minslearn/Flutter-Custom-Widget.git
Clone the repo
image-67
Clone the Flutter Custom Widget repo from GitHub

By default, it’ll be in the master branch. I’m switching to a refactor branch (you don’t need to) because I want you all to have a look at my initial and final code. The initial code will be in the master branch and the final code will be in the refactor branch.

Run the following command to install all the dependencies:

cd Flutter-Custom-Widget/
flutter pub get
Install app dependencies
image-68
Install Flutter dependencies

Run the App

Open the repo in Visual Studio Code and spin up your emulator (you may connect your mobile device, too). Once your emulator is up and running, press F5 to run the app in the emulator.

Here’s the view of your app on the first run.

image-69
Initial app run screen

If you’ve come this far, that’s great.

Analyze the Code

Let’s look at the code. Open the lib/main.dart file.

We have a MyApp class called at the beginning. This in-turn calls the MyHomePage class.

This is our code for the entire UI which is defined in _MyHomePageState class:

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Welcome to Flutter Refactoring Tutorial',
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
            const SizedBox(height: 16),
            const Text('Press the below button to follow me on Twitter'),
            ElevatedButton(
              onPressed: () {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(
                    content: Text("Pressed Follow on Twitter button"),
                    duration: Duration(seconds: 1),
                  ),
                );
                // Open Twitter app
              },
              child: const Text("Follow on Twitter"),
            ),
            const SizedBox(height: 16),
            const Text('Press the below button to follow me on Instagram'),
            ElevatedButton(
              onPressed: () {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(
                    content: Text("Pressed Follow on Instagram button"),
                    duration: Duration(seconds: 1),
                  ),
                );
                // Open Instagram app
              },
              child: const Text("Follow on Instagram"),
            ),
          ],
        ),
      ),
    );
  }
}
Code for the app UI

And so you can reference the line numbers, here's a visual:

image-72
Code for the app UI

If you’re someone who loves writing clean code, you would definitely say that this is ugly code.

Here’s the reason for it. Look at the code carefully – lines 44 to 56 and lines 58 to 70 are completely duplicated except for a very few handpicked words. For example, the word “Twitter” has been replaced with the word “Instagram”.

The clean coder will definitely refactor this code before working on adding new features/functionalities. Let's follow those clean coding practices now, too.

Refactor the Code and Build a Custom Widget

We have to extract the text and button into a separate component. This component should accept the platform and onPressed as its parameters. We can template out the common text from them.

So, our code to build the custom widget looks like this:

class CustomButton extends StatelessWidget {
  final String platform;
  final VoidCallback onPressed;
  const CustomButton(
      {super.key, required this.platform, required this.onPressed});
  @override
  Widget build(BuildContext context) {
    return Center(
        child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
      Text("Press the below button to follow me on $platform"),
      ElevatedButton(
        onPressed: () {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text("Pressed Follow on $platform button"),
              duration: const Duration(seconds: 1),
            ),
          );
          onPressed();
        },
        child: Text("Follow on $platform"),
      )
    ]));
  }
}
Create a custom widget

As we discussed above, the template text and accept platform and onPressed parameters. We replaced platform wherever we need and call the onPressed method as the extension of showing a snack bar.

Add the above code at the very end of the main.dart file.

Integrate the Custom Widget

Let’s integrate our custom widget into our code.

Pick the first block of code from the line 44 to 56 as shown below

            const Text('Press the below button to follow me on Twitter'),
            ElevatedButton(
              onPressed: () {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(
                    content: Text("Pressed Follow on Twitter button"),
                    duration: Duration(seconds: 1),
                  ),
                );
                // Open Twitter app
              },
              child: const Text("Follow on Twitter"),
            ),
Refactor the first block of code

Replace it with the following code:

CustomButton(
  platform: 'Twitter',
  onPressed: () {
    // Open Twitter App
  },
),
Use our custom widget for Twitter button

Similarly, pick the next block of code from the line 58 to 70 as shown below

            const Text('Press the below button to follow me on Instagram'),
            ElevatedButton(
              onPressed: () {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(
                    content: Text("Pressed Follow on Instagram button"),
                    duration: Duration(seconds: 1),
                  ),
                );
                // Open Instagram app
              },
              child: const Text("Follow on Instagram"),
            ),
Refactor second block of code

Replace it with the following code:

CustomButton(
  platform: 'Instagram',
  onPressed: () {
    // Open Instagram App
  },
),
Use our custom widget for Instagram button

Here's the final code of _MyHomePageState class after we complete our refactoring process.

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Welcome to Flutter Refactoring Tutorial',
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
            const SizedBox(height: 16),
            CustomButton(
              platform: 'Twitter',
              onPressed: () {
                // Open Twitter App
              },
            ),
            const SizedBox(height: 16),
            CustomButton(
              platform: 'Instagram',
              onPressed: () {
                // Open Instagram App
              },
            ),
          ],
        ),
      ),
    );
  }
}
After refactoring your code

And again, here's the screenshot for line number reference:

image-73
After refactoring your code

Run your app now.

Unfortunately, you’ll not notice any change in the UI. But your underlying code has changed. That’s exactly what refactoring is.

Quoting from Martin Fowler,

Refactoring is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior. – https://refactoring.com/

image-74
Final app

You may be wondering something after looking at the above code. Lines 43 and 50 also contain the same code (const SizedBox(height: 16),). So why don’t we include that into the component?

That’s great if you had this question.

There is no need for the custom widget component to include the SizedBox component. This is because the SizedBox component is added in the Home page to give some space between each component. But it's not necessary that whenever we use this button, we give a space at the top/bottom of the widget.

Still, if such cases arise, you can add the SizedBox widget inside your custom widget.

Why Build a Custom Widget?

You might not see a direct benefit right away. But you may experience it in the future. Here’s a quick example for you.

Let’s assume you’ve built this app for a client. It has become a complex app and you’ve used this custom widget around 20 places in your app. The app is released and people enjoy using it.

About 6 months later, your client come back to you with the next version of changes. One of the items in the huge list is, “We’re coming up with a slight change in theme. Replace all the social media referral buttons so that they're an outlined shape and change the color to green”.

It is one simple configuration change in the custom widget. But imagine if you hadn't built the custom widget and had to copy/pasted the same code in all the 20 places. Then you'd have to carefully look at each place and replace each instance with care without touching other pieces of code.

These are the only 2 lines we have to change in our custom widget in this example:

OutlinedButton(
        style: OutlinedButton.styleFrom(foregroundColor: Colors.green),
Code change to be done on custom widget for the above requirement
image-75
Changes in our custom widget

But if you hadn't refactored your code, you'd have to make this change in 20 places.

image-76
Small change reflects everywhere

I’ve pushed my code to the same GitHub repo. Refer to the master branch for the non-refactored code and the refactor branch for the refactored code.

Use Cases for Custom Widgets

Always use custom widgets for their specific use cases. For example, in our case, it is for Social media redirects. This widget should not be used in places which are unrelated to its context.

If you do, remember the above case where the client requirement was to change the design of only the social media referral buttons...but our change would be applied to all the other places where this widget was used. This would lead to unexpected bugs.

You should always write unit test cases for Custom Widgets which will help you mitigate any bugs earlier.

One more tip is to name your component in a more readable way. This helps other developers know what the widget does just by reading its name.

In our case, I've named it CustomButton which makes no sense. Instead, some good alternatives would be SocialMediaButton, SocialButton, and so on which fit into our use case.

Conclusion

In this tutorial, you learned about building a custom widget by removing duplicated code/components.

Building custom widgets in Flutter promotes code reusability, maintainability, consistency, abstraction, flexibility, and community collaboration.

Custom widgets are a powerful tool in the Flutter developer’s toolkit, enabling you to create beautiful and functional user interfaces while maximizing efficiency and maintainability.

If you wish to learn more about Flutter, subscribe to my email newsletter (https://5minslearn.gogosoon.com/) and follow me on social media.