Widgets, the cornerstone of Flutter
In the previous chapter, we explored the origins of the Flutter framework, who created it, a bit …
read moreIn the previous chapters of our series on Flutter, we’ve laid down a solid theoretical and practical foundation that allows us to delve deeper into the fascinating world of cross-platform application development. We began our journey with an Introduction to Flutter, where we discovered what Flutter is and why it has gained so much popularity among developers. Then, in our second chapter, we explored the concept of Widgets, the fundamental building blocks of any Flutter application, learning how they are organized into a tree to build interactive and attractive user interfaces. More recently, in the third chapter, we delved into Dart, the programming language optimized by Google for development with this framework, getting familiar with its syntax, features, and how it leverages this powerful language to create efficient and natively compiled applications.
Now, in this chapter, we are ready to take a significant step: creating our first complete Flutter project. Together, we will not only understand the hierarchical structure that underlies a Flutter project and how to organize our files and code to maximize efficiency and scalability, but we will also put into practice everything learned by developing a basic project: a palindrome checker.
Through this project, we will explore creating user interfaces, handling user input, and applying logic in Dart to determine whether a word or phrase is a palindrome, meaning it reads the same forwards and backwards. It’s an excellent opportunity to see interactive widgets in action, learn more about managing state in Flutter, and understand how an idea is transformed into a functional application that you can run on your own device.
Before diving into the development of our application, it’s crucial to have our development environment correctly set up. We will use Android Studio, complemented with the Flutter and Dart plugins, to provide us with a comprehensive development experience. These plugins are available in the Android Studio Marketplace (accessible from its configuration panel) and are essential for working with Flutter.
When it comes to starting a new Flutter project, you have the flexibility to choose the method you prefer. You can opt for the speed and simplicity of the command line, where a simple command sets up everything necessary to get started:
flutter create palindrome_validator
Or we can also use Android Studio itself, which in its latest versions allows us to create our Flutter project using an
intuitive graphical interface from the File > New... > New Flutter Project...
option, which will guide you through the
initial setup. Now that we have laid the groundwork for starting our Flutter project, we are ready to first examine the
project structure.
We can observe a basic directory and file structure that resembles the following:
android: Contains Android-specific files for your project, build configurations, manifest, and resources.
ios: Stores iOS-specific files, including Xcode project files and Objective-C/Swift source code.
lib: Directory for your Flutter application’s Dart source code.
linux: Contains Linux-specific files to compile your application for Linux desktop.
macos: Similar to linux
and ios
, but for MacOS desktop applications.
test: Directory for Dart test files of your application. We will add a small set of tests at the end.
web: Specific files for the web version of your Flutter application, including the HTML entry file and resources.
windows: Windows-specific files to compile your desktop application.
pubspec.yaml: Configuration file for your project, where dependencies, application name, version, and other metadata are specified.
Now that we are familiar with each component of a Flutter project, we are ready to start building our super palindrome
checker app. As mentioned before, the main folder for our application’s source code is the lib
directory. This is
where we will begin our work. We will already have an example application stored in it, which is added when creating the
project, located in the main.dart
file. This file contains a Stateless Widget as the main widget, which will allow us
to display the number of button presses that we can run from either the IDE or the console with:
flutter run
The first step, aside from the research and planning phase, in building our palindrome checker app is to create the user
interface and the code. It will feature a text field for users to enter the word or phrase they want to verify, a button
for users to submit the word or phrase, and a text area to display the verification result. We could start by modifying
the MyApp
class found in the main.dart
file, but to make it more modular, we are going to separate the different
parts into multiple files. In the end, our main
file will look like this:
import 'package:flutter/material.dart';
import 'package:palindrome_validator/views/my_home_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
final String appTitle = 'Palindrome Validator';
const MyApp({super.key});
// Este Widget es la raíz de la aplicación.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: appTitle,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightGreen),
useMaterial3: true,
),
home: MyHomePage(title: appTitle),
);
}
}
This Widget is the main entry point of our application, usually few changes are made in this part as it is responsible
for initializing the application along with its related libraries and displaying the first Widget that will be seen. It
launches a Flutter application that uses the principles of Material 3, the latest update to
the Material Design language, offering a more modern appearance than its predecessors. The
application starts with the main()
function that runs the MyApp
widget, which acts as the root of the application.
Within MyApp
, a MaterialApp
widget is used to set up global visual and navigation aspects of the application,
including the title and theme.
The application’s theme is defined using ThemeData
, where the use of Material 3 is specified with the useMaterial3
parameter, and a custom color scheme is set with ColorScheme.fromSeed(seedColor: Colors.lightGreen)
. This way of
defining the theme leverages a functionality of Material 3 that allows generating a consistent color palette from a seed
color, in this case, Colors.lightGreen
. Finally, it points to MyHomePage
as the home page, passing the application’s
title to this page for use.
In our case, the MyHomePage
Widget is located in the my_home_page.dart
file, within the views
directory, which
would be responsible for displaying the user interface:
import 'package:flutter/material.dart';
import '../controllers/palindrome_checker.dart';
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _HomePageState();
}
class _HomePageState extends State<MyHomePage> {
final TextEditingController _controller = TextEditingController();
String _result = '';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: _controller,
decoration: InputDecoration(
labelText: 'Ingresa un texto',
),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
setState(() {
_result = PalindromeChecker().isPalindrome(_controller.text)
? 'A palindrome'
: 'Not a palindrome';
});
},
child: Text('Verificar'),
),
SizedBox(height: 20),
Text(_result, style: TextStyle(fontSize: 20)),
],
),
),
);
}
}
This fragment implements a main page MyHomePage
, which inherits from StatefulWidget
, a stateful Widget that will
allow the user interface to react to state changes, such as updating the result after analyzing the phrase. The state is
managed through _HomePageState
, where a TextEditingController
_controller
is initialized to capture the user’s
input and a variable _result
to store the verdict on whether the entered text is a palindrome or not.
Inside the build
method of _HomePageState
, the user interface is structured using a Scaffold
, which provides
an AppBar
application bar to display the title and a body that organizes the internal widgets with Padding
and
a Column
. The latter vertically aligns the widgets in the center, including a TextField
for the user’s input,
followed by an ElevatedButton
that, when pressed, invokes the palindrome verification
through PalindromeChecker().isPalindrome
. The result of this verification is assigned to _result
and displayed below
the button in a Text
widget.
The use of setState
within the onPressed
of the button ensures that the user interface is updated with the new
result each time the user presses “Verify”. This code effectively demonstrates how interactive user interfaces can be
built in Flutter, processing and displaying user input dynamically.
As we discussed in the first introduction article to Flutter, stateless Widgets are those that do not change over time, while stateful Widgets do, in this case, the application’s state changes each time the user presses the verify button to update the result in the corresponding text.
The PalindromeChecker
class we see next is responsible for performing the logic related to the analysis of the entered
text, basically, whether it reads the same forwards and backwards:
import 'package:diacritic/diacritic.dart';
class PalindromeChecker {
bool isPalindrome(String string) {
String formattedString = removeDiacritics(string.toLowerCase()).replaceAll(RegExp(r'[^a-zA-Z0-9]'), '').replaceAll(' ', '');
return formattedString == formattedString.split('').reversed.join('');
}
}
This source code defines a PalindromeChecker
class with an isPalindrome
method that determines if a string of text
is a palindrome. It ignores uppercase, lowercase, spaces, punctuation marks, and accents. The method first converts the
string to lowercase and uses the removeDiacritics
function from the diacritic package. Then, it removes all
non-alphanumeric characters and spaces. Finally, it compares the formatted string with its reversed version to check if
it’s a palindrome, returning true if it is, or false otherwise.
We use the diacritic
package to remove accents from words and phrases, thus only considering the characters themselves
to determine if the entered word is truly a palindrome. To add this package to our project, we can use the command line
or Android Studio itself; in this case, we use the command line:
flutter pub add diacritic
We could modify the pubspec.yaml
file directly to add the package, but it’s safer to do so through Flutter’s pub
command as it automatically takes care of syncing the project with the newly added libraries. If we modify it manually,
we would have to synchronize it afterward by hand. Additionally, it will automatically determine the current version and
add it to the list of dependencies.
Once the parts of our project are integrated, we can run it and see how it works. The usage is simple: enter a word or phrase in the text field and, after pressing the verify button, it will show whether it’s a palindrome or not. Even though it’s a very simple application, it has served to demonstrate how a Flutter project is structured, how files are organized, and how different parts of the application can be separated to make it more modular and easier to maintain.
Now that we have created our application, let’s take a step further and add a suite of tests to ensure that the logic of
our application is correct. For this, we could add a file named palindrome_checker_test.dart
(the file name should be
the same as its counterpart in the project followed by the suffix _test
and stored in a folder in the same place in
the hierarchy) in the test
directory (if we have created a controllers
folder, we also need to create it in the
tests section) with the following content:
import 'package:flutter_test/flutter_test.dart';
import 'package:palindrome_validator/controllers/palindrome_checker.dart';
void main() {
final palindromeChecker = PalindromeChecker();
group('PalindromeChecker', () {
test('should return true for a palindrome', () {
expect(palindromeChecker.isPalindrome('A man, a plan, a canal: Panama'), true);
});
test('should return false for a non-palindrome', () {
expect(palindromeChecker.isPalindrome('Not a palindrome'), false);
});
test('should return true for a palindrome with mixed case', () {
expect(palindromeChecker.isPalindrome('Able , was I saw eLba'), true);
});
test('should return true for a palindrome with symbols', () {
expect(palindromeChecker.isPalindrome('Madam, in Eden, I’m Adam.'), true);
});
test('should return true for single word palindromes', () {
var words = ['Ana', 'Anilina', 'Arenera', 'Menem', 'Oso', 'Radar', 'Reconocer', 'Salas', 'Somos'];
for (var word in words) {
expect(palindromeChecker.isPalindrome(word), true);
}
});
test('should return true for phrase palindromes', () {
var phrases = [
'Anita lava la tina',
'A mamá Roma le aviva el amor a papá y a papá Roma le aviva el amor a mamá',
'Amo la pacífica paloma',
'La ruta nos aportó otro paso natural',
'No subas, abusón'
];
for (var phrase in phrases) {
expect(palindromeChecker.isPalindrome(phrase), true);
}
});
});
}
A small suite of basic tests with words and phrases in both Spanish and English in which, in two different ways, we
check whether the word or phrase is a palindrome or not, doing so in two distinct ways serves an illustrative purpose.
We can run our suite of tests by right-clicking on the test
folder and selecting the option Run tests in test
. If
everything has finished correctly, we should see a message at the end of the test execution indicating that all tests
have been successfully executed. If not, it will indicate which ones have failed and why. We will not delve deeper into
the topic of testing as it is a subject that deserves its own chapter, but it’s important to keep in mind that it is a
fundamental part of software development.
In this chapter, we’ve taken a significant step in our series on Flutter, creating our first application, which, while not the most useful in the world, has allowed us to explore the structure of a Flutter project, learned how to organize our files and code to maximize efficiency and scalability. We’ve experimented with creating user interfaces, handled user input, and applied logic in Dart to determine whether a word or phrase is a palindrome. We’ve seen interactive Widgets in action, learned more about managing state in Flutter, and understood how an idea is transformed into a functional application that you can run on your device.
In the next chapter, we’ll continue our adventure through the worlds of Flutter, delving deeper into cross-platform application development and exploring how we can leverage the features it provides to create more complex and appealing applications. Until then, I encourage you to play with the application we’ve just created, experiment with its code, and share your ideas and questions in the comments section. And remember, Happy Coding!
That may interest you
In the previous chapter, we explored the origins of the Flutter framework, who created it, a bit …
read moreFlutter is a mobile app development framework created by Google. It utilizes the Skia rendering …
read moreIn the world of mobile application development, Flutter has stood out as one of the most innovative …
read moreConcept to value