Flutter notes, part 2
Introduction
Having laid the groundwork for Flutter development in our "Flutter Notes" series, it's time to dive into the crucial topics that take our development skills to the next level: Widgets and Navigation and Routing. In this second part, we'll explore how Widgets, those essential components of Flutter, form the backbone of any user interface, allowing us to build visual applications from simple to complex with surprising ease. In addition, we'll tackle Navigation and Routing, the system Flutter uses to move between screens and manage the flow of data, ensuring a smooth and cohesive user experience. Get ready to dive deep into these fundamental tools that will transform the way you develop mobile apps with Flutter.
Widgets in Flutter
Widgets in Flutter are the basic user interface elements in applications developed with this framework. Flutter is based on an "everything is a widget" approach, which means that almost everything from a text element to a container to an entire page is a widget. Widgets describe what your appearance should look like given your current configuration and state.
There are two main types of widgets in Flutter:
Immutable state widgets (StatelessWidget): these are widgets that do not store state; that is, they have no internal data that changes during the lifetime of the widget. An example of a StatelessWidget is a text widget that does not change after being displayed on the screen.
StatefulWidget: These are widgets that can change their state. They have an associated State object that stores state and can be modified during the lifetime of the widget. An example could be an interactive checkbox or a form where the user can enter data.
Each widget in Flutter is a Dart class that can have properties (for configuration) and methods (for behavior). Widgets are organized in a tree, where you have parent widgets that contain child widgets, thus creating the UI structure of the application. This modular approach allows for great flexibility and code reuse, as you can build complex widgets from simpler widgets.
Flutter also provides an extensive library of pre-built widgets that cover many common use cases, such as buttons, lists, cards, etc., making it easy to create rich and responsive user interfaces.
🔍 Basic example of StatelessWidget:
import 'package:flutter/material.dart';
class MyStatelessWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('Hello, Flutter!');
}
}
🔍Basic example of StatefulWidget:
import 'package:flutter/material.dart';
class MyStatefulWidget extends StatefulWidget {
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
bool _isChecked = false;
void _toggleChecked() {
setState(() {
_isChecked = !_isChecked;
});
}
@override
Widget build(BuildContext context) {
return CheckboxListTile(
title: Text('Check me!'),
value: _isChecked,
onChanged: (bool? value) {
_toggleChecked();
},
);
}
}
Code Explanation:
In these examples, MyStatelessWidget is a simple widget that displays a text, and MyStatefulWidget is a widget that maintains the state of whether a checkbox is checked or unchecked, and changes its state when the user interacts with it.
Widget Tree
The organization of widgets in a tree is fundamental to understanding how user interfaces are built and managed. This tree structure allows Flutter to be highly efficient in rendering the UI and facilitates the creation of complex and dynamic interfaces. Here I explain how it works and why it is important to understand this structure:
Widget Tree Structure
Widget Tree: All widgets in Flutter are organized in a hierarchical tree structure. At the top of this tree, you generally have an App widget (such as MaterialApp or CupertinoApp), which sets the context and overall configuration of your application. From there, each widget can have child widgets, thus creating a descendant structure that defines the user interface.
Parent and Child Widgets: Widgets are divided into parent and child widgets, where a parent widget can contain one or more child widgets. For example, a Column widget can contain several Text widgets as children, arranging them vertically on the screen.
Element Tree and Render Tree: When a Flutter application runs, the widget tree becomes an element tree, which in turn is used to create a render tree. The rendering tree is what Flutter ultimately uses to calculate and render the UI on the screen. This separation allows Flutter to be highly efficient, as only widgets that need to be redrawn are marked as "dirty" and rebuilt, rather than rebuilding the entire UI.
Importance to User Interface Design
Performance: Understanding how Flutter organizes widgets in a tree is crucial to designing efficient user interfaces. By minimizing the depth of the tree and using widgets appropriately, you can improve the performance of your application, as Flutter can reconstruct and render the UI more efficiently.
Widget reuse: The tree structure encourages widget reuse. You can create custom widgets (composed of other simpler widgets) and reuse them in different parts of your application, keeping your code DRY (Don't Repeat Yourself) and making maintenance easier.
State Management: Understanding the tree structure helps you manage the state of your application more effectively. For example, knowing where to place a StatefulWidget in your widget tree can influence how and when a widget is rebuilt, which is essential for creating a smooth and responsive user experience.
Navigation and Layout: The tree organization determines how the navigation and layout is structured within your application. Understanding this concept allows you to design interfaces that are intuitive for the user and take full advantage of Flutter's capabilities, such as animations and transitions between screens.
🔍Example Widget Tree
To illustrate how Flutter organizes widgets in a tree and the importance of understanding this structure for designing efficient user interfaces, let's create a simple code example. This example will show a basic tree structure with Flutter widgets, including a Scaffold, AppBar, Column, and some Text and FlatButton widgets for interaction.
This code is an example of how you could structure a simple screen in a Flutter application:
Recommended by LinkedIn
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Tree Structure Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
// Scaffold is the main layout for the main screen.
return Scaffold(
appBar: AppBar(
title: Text('Flutter Tree Structure Demo'),
),
body: Center(
// Column organizes your children vertically
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
Code Explanation:
MyApp: This is the root widget of the application, which uses MaterialApp to set the title and home screen of the application.
MyHomePage: A StatefulWidget that represents the main screen of the application. This widget contains a state that can change, in this case, the _counter counter.
Scaffold: Provides the basic structure of the application screen, including an AppBar and a Body. Inside the body, a Column widget is used to organize other widgets vertically.
Column: Contains a list of child widgets, which are organized vertically on the screen. In this example, it contains two Text widgets, one showing a static message and the other showing the counter value, which is updated when the button is pressed.
FloatingActionButton: A button that, when pressed, increments the counter value through the _incrementCounter function.
This example demonstrates how widgets are organized in a hierarchical tree to build user interfaces. Each widget in the tree has a specific function and contributes to the overall layout of the screen. Understanding this structure is key to designing effective interfaces in Flutter.
Navigation and Routing
Navigation Management:
Navigation between different screens and widgets is handled mainly through the Navigator, which allows you to move between screens (also known as paths) in an application. The Navigator works with a stack to manage the application's paths: you can "push" new screens onto the stack to navigate to them, and "pop" screens off the stack to return to the previous screen.
Basic Concepts:
Paths: Application screens or pages are identified by paths. In Flutter, each path is usually associated with a widget that defines the content of the screen.
Navigator: This is the widget that manages the stack of routes. It uses methods such as Navigator.push to navigate to a new screen and Navigator.pop to return to the previous screen.
🔍Navigation Example:
Next, we will look at a basic example of how to implement navigation between two screens in Flutter. The first screen will have a button that, when pressed, will take the user to the second screen. From the second screen, the user will be able to return to the first screen.
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
home: FirstScreen(),
));
}
// First screen with a button to navigate to second screen
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Primera Pantalla'),
),
body: Center(
child: ElevatedButton(
child: Text('Ir a la segunda pantalla'),
onPressed: () {
// Navegar a la segunda pantalla
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
},
),
),
);
}
}
// Second screen with a button to return to the first screen
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Segunda Pantalla'),
),
body: Center(
child: ElevatedButton(
child: Text('Regresar a la primera pantalla'),
onPressed: () {
// Regresar a la pantalla anterior
Navigator.pop(context);
},
),
),
);
}
}
Code Explanation:
MaterialApp: It is the root widget of the application that configures the environment required for navigation, including the Navigator.
FirstScreen: It is the first screen of the application. It contains an ElevatedButton that, when pressed, uses Navigator.push to navigate to SecondScreen. A new MaterialPageRoute is created with SecondScreen as its child widget.
SecondScreen: It is the second screen, accessible from FirstScreen. It contains another ElevatedButton that, when pressed, uses Navigator.pop to return to the previous screen (in this case, FirstScreen).
Use of Named Paths:
Using named paths in Flutter makes it easier to manage navigation within larger apps, making the code cleaner, more organized, and easier to maintain. Instead of creating new instances of pages directly in the navigation code, you can reference them by name, which simplifies changing and updating your app's navigation structure.
Defining Named Paths:
To use named routes, you must define them in the routes property of the MaterialApp widget in your app. Each route is associated with a name (a text string) and a screen constructor (a widget).
🔍Named Paths Example:
Let's create an example with three screens: a home screen, a details screen, and a settings screen, to demonstrate how to implement and use named routes.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// Defines the initial screen
initialRoute: '/',
// Map of named routes. Each entry associates a route name with a screen constructor.
routes: {
'/': (context) => HomeScreen(),
'/details': (context) => DetailsScreen(),
'/settings': (context) => SettingsScreen(),
},
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Pantalla de Inicio'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
child: Text('Ir a Detalles'),
onPressed: () {
// Navega a la pantalla de detalles
Navigator.pushNamed(context, '/details');
},
),
ElevatedButton(
child: Text('Ir a Ajustes'),
onPressed: () {
// Navega a la pantalla de ajustes
Navigator.pushNamed(context, '/settings');
},
),
],
),
),
);
}
}
class DetailsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Pantalla de Detalles'),
),
body: Center(
child: ElevatedButton(
child: Text('Regresar'),
onPressed: () {
// Regresa a la pantalla anterior
Navigator.pop(context);
},
),
),
);
}
}
class SettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Pantalla de Ajustes'),
),
body: Center(
child: ElevatedButton(
child: Text('Regresar'),
onPressed: () {
// Regresa a la pantalla anterior
Navigator.pop(context);
},
),
),
);
}
}
Conclusion
In this second part of "Flutter Notes," we've broken down the fundamentals and advanced techniques of Widgets and Navigation and Routing, key building blocks for creating dynamic, navigable apps in Flutter. By mastering these concepts, you equip yourself with the tools necessary to design intuitive user interfaces and smooth navigation experiences, essential in modern mobile app development. With this knowledge, you are one step closer to becoming an expert Flutter developer.