Flutter: Custom Widgets Using Existing Classes

Wilson Hadi
The Startup
Published in
7 min readNov 19, 2020
source: unsplash.com

New in Flutter and want to start making custom widgets? This article will give a simple example of making custom widgets while using the classes available from Flutter.

Before we start talking about the Flutter stuff, let’s talk about refactoring.

NOTE: For those who want to just look into the Flutter part, feel free to skip ahead.

What is Refactoring?

“Refactoring is a controlled technique for improving the design of an existing code base.” — Martin Fowler

Refactoring is done by applying small changes to the code design while preserving their behaviour. It is an iterative practice with the goal of reducing bug potentials, reduce code smells, reduce code duplications, improving reusability and maintainability.

Keep in mind that refactoring is NOT changing nor adding functionality

Most of the time, refactoring is closely related to design patterns. This is because you can refactor the code design into already well-known design patterns, which of course is a good especially when working in a larger team. However, let us talk about some of the many basic methods of refactoring your code:

  • Extract Method
    This is when a method or function looks just too long and is most likely doing something that belongs to another method, or can be split into more than 1 method. Always keep in mind that methods should be responsible in only doing 1 thing. Extract the lines of code that don’t belong in a method’s responsibility into a new method.
// Before
method(int a, int b) {
processedA = doSomething(a);
processedB = doSomething(b);

print('result of a: ' + processedA);
print('result of b: ' + processedB);
}
// After
method(a, b) {
processedA = doSomething(a);
processedB = doSomething(b);

printResults();
}
printResults(a, b) {
print('result of a: ' + a);
print('result of b: ' + b);
}
  • Extract Variable
    This is when a method is too hard to understand. This method takes logic that seem too complicated into a variable, and operates on those variable instead. Keep in mind that the variables should have meaningful names
// Before
method
() {
if ((a.toUpperCase().indexOf(indexA) > -1) &&
(b.toUpperCase().indexOf(indexB) > -1) &&
c > 0 ) {
// do something
}
}
// After
method() {
isSomething = a.toUpperCase().indexOf(indexA) > -1;
isSomethingElse = b.toUpperCase().indexOf(indexB) > -1;
isCValid = c > 0;

if (isSomething && isSomethingElse && isCValid) {
// do something
}
}
  • Extract class
    This is when a class has unnecessary fields that don’t really make sense for it to be there. This method extracts those fields and creates a new class as a field of the first class.
// Before
class Person {
String name
int areaCode
int phoneNumber
getFullPhoneNumber() {
return '$this.areaCode' + '$this.phoneNumber'
}
}
// After
class Person {
String name
PhoneNumber phoneNumber
getPhoneNumber() {
return this.phoneNumber
}
}
class PhoneNumber {
int areaCode
int phoneNumber
getFullPhoneNumber() {
return '$this.areaCode' + '$this.phoneNumber'
}
}

There are many more methods of refactoring, here is a pretty complete link to help you learn more refactoring methods:

Implementation in my project

Since I am working mostly in the front-end, most of the refactoring I do is reducing code duplication. In flutter, many widgets can be similar and have many duplicate codes especially when the same widget classes are used, or the same combination of widgets are used. I found a lot of these cases in my team’s code base. Another example is when functions are simply too long when processing data from user input and/or API retrieval.

An example refactoring I did was mostly extract method for refactoring those functions that are too long:

The function above looks so long and clearly has lines of code that do not belong to its responsibility. After refactoring, the code becomes:

Now the function looks much more concise after extracting and creating new functions.

Another example refactoring I did was creating custom widgets. This allows the same structuring of widgets to create, for example, a custom button design, or a custom container (with specific margin, padding, border radius, etc.) to be reused with significantly less code clutter.

Below is an example process of my implementation in creating a custom widget.

Flutter Part starts here

Prerequisites:

  • Basic Flutter syntax
  • Basic Flutter widget styling (decoration, border, padding, etc.)
  • Basic Flutter testing suite knowledge (such as tester.pumpWidget, expect(Finder, Matcher), etc.)
  • Stateful vs Stateless Widgets

This time, let us make a custom pop up widget that has a description text along with 2 buttons using AlertDialog class. Here is an end result:

Like making any custom widget, we should define what properties the widget will have. These properties will be used as input to the ‘sub-widgets’ inside our custom widget. To make the pop up like what is shown above, we are going to need properties (or inputs) for:

  • the example content text
  • 2 buttons’ text
  • functions for the 2 buttons

(We are creating a stateful widget since pop ups are used in stateful pages as well)

class CustomAlertDialog extends StatefulWidget {
CustomAlertDialog({
this.contentText,
this.leftButtonText,
this.rightButtonText,
this.leftButtonFunction,
this.rightButtonFunction,
});
final String contentText;
final String leftButtonText;
final String rightButtonText;
final Function leftButtonFunction;
final Function rightButtonFunction;

@override
_CustomAlertDialog createState() => _CustomAlertDialog();
}

What this does is that the class CustomAlertDialog has properties that the user can input. In this case, the user can set the content text into any message, the buttons to have any function title (e.g., Cancel-Confirm), and input an onPressed function later on.

Next, we should create the Alert Dialog and use the properties we created earlier as input for the properties of Alert Dialog:

Here, we used the contentText property we defined earlier and use it as input to the Alert Dialog’s title property. We do this by

// example usage: widget.<PROPERTY>widget.contentText

Then we will use the button texts and functions property by using the Flat Button class from flutter and putting it inside a Row class since we want it side by side:

Expanded class is to have the child fill in all available area

As you can see, we did the same action of calling the properties we defined at the start as input to the existing widget classes.

Here is the full build state without any styling:

(Full code at the end of the article)

Testing

To test custom widgets, simply render them in a Stateful Builder that returns a Material App:

Builds a Stateful Widget

In the Material App, have a child to contain your custom widget. For example, I used the Center class:

Then, simply input some test values for the properties as if you are using it:

For the assertions, you can use find.byKey or in our case, find.byText since we did not define a Key property in the custom widget:

This will be enough to have a 100% code coverage, since we did not define any functions as well.

This method of customising a widget is usually done for implementing the DRY principle. This way, codes such as styling codes (padding, color, size, etc.) and calling classes are not duplicated. It will also enable reusability in other pages of your Flutter application.

In conclusion, all custom widgets of this kind have the same basic steps:

  1. define properties
  2. build the additional widget needed using existing Flutter widgets
  3. use the defined properties as input properties to those widgets
  4. test by rendering using a state builder (but of course this should actually be the first step, since TDD is the best practice, check out my TDD article if you want to learn more about it!)

Thank you for reading!
— Wilson

References:

Full Codes

custom_alert_dialog.dart:

import 'package:bisaGo/config/styles.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class CustomAlertDialog extends StatefulWidget {
CustomAlertDialog({
this.contentText,
this.leftButtonText,
this.rightButtonText,
this.leftButtonFunction,
this.rightButtonFunction,
});
final String contentText;
final String leftButtonText;
final String rightButtonText;
final Function leftButtonFunction;
final Function rightButtonFunction;

@override
_CustomAlertDialog createState() => _CustomAlertDialog();
}

class _CustomAlertDialog extends State<CustomAlertDialog> {
@override
Widget build(BuildContext context) {
return AlertDialog(
key: widget.key,
title: Text(widget.contentText, textAlign: TextAlign.center),
titleTextStyle: TextStyle(fontSize: 25.0, color: Colors.black),
titlePadding: EdgeInsets.fromLTRB(0.0, 30.0, 0.0, 20.0),
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
flex: 1,
child: Container(
key: Key('Pop Up Button ' + widget.leftButtonText),
decoration: BoxDecoration(boxShadow: regularShadow),
height: 64.0,
child: FlatButton(
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5.0),
side: BorderSide(color: greenPrimary)),
child: Text(widget.leftButtonText,
style: TextStyle(color: greenPrimary, fontSize: 20.0)),
padding: EdgeInsets.symmetric(vertical: 10.0),
onPressed: widget.leftButtonFunction,
))),
SizedBox(width: 10.0),
Expanded(
flex: 1,
child: Container(
key: Key('Pop Up Button ${widget.rightButtonText}'),
decoration: BoxDecoration(boxShadow: regularShadow),
height: 64.0,
child: FlatButton(
color: greenPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5.0)),
child: Text(widget.rightButtonText,
style: TextStyle(color: Colors.white, fontSize: 20.0)),
padding: EdgeInsets.symmetric(vertical: 10.0),
onPressed: widget.rightButtonFunction,
))),
],
),
contentPadding: EdgeInsets.fromLTRB(25.0, 0.0, 25.0, 30.0),
shape: RoundedRectangleBorder(borderRadius: regularBorderRadius),
);
}
}

custom_alert_dialog_test.dart:

import 'package:demo/custom_alert_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
testWidgets('Renders Pop Up and Buttons', (WidgetTester tester) async {
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MaterialApp(
home: Material(
child: Center(
child: CustomAlertDialog(
contentText: 'Example Text',
leftButtonText: 'Left',
leftButtonFunction: () {},
rightButtonText: 'Right',
rightButtonFunction: () {},
),
),
),
);
},
),
);

expect(find.byType(CustomAlertDialog), findsOneWidget);
expect(find.text('Example Text'), findsOneWidget);
expect(find.text('Left'), findsOneWidget);
expect(find.text('Right'), findsOneWidget);
});
}

--

--

Wilson Hadi
The Startup

3rd year Bachelor’s Degree of Computer Science at University of Indonesia