Understanding Flutter: A Guide to Widgets, Stateless and Stateful Widgets
In Flutter, even the smallest elements, such as Text
and Image
, are considered widgets. These basic widgets serve as the fundamental building blocks of any Flutter application. They are essential components that developers use to display text and images on the screen. When we move one level higher in the widget hierarchy, we encounter composite widgets. These are more complex widgets that can contain other widgets within them. Examples of composite widgets include Column
, Row
, and ElevatedButton
. The Column
and Row
widgets are used to arrange their child widgets in a vertical or horizontal layout, respectively. The ElevatedButton
widget is an interactive component that responds to user actions, such as a button press, through its onPressed()
callback function.
Ascending another level in the widget hierarchy, we find the Scaffold
widget. This widget acts as a framework for implementing the basic material design layout structure of an app. It can hold multiple composite widgets, providing a consistent visual structure for the app's pages. The Scaffold
widget typically includes elements like an app bar, a body, and a floating action button.
At the top level, we have the MaterialApp
widget, which is crucial for any Flutter app that follows material design principles. The MaterialApp
widget encompasses the entire application and contains widgets like Scaffold
. It also provides essential features such as navigation, theming, and localization. Navigation allows users to move between different screens within the app, while theming enables developers to define a consistent look and feel across the app. Localization ensures that the app can support multiple languages and regions, making it accessible to a broader audience.
Understanding runApp:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Demo'),
),
body: Center(
child: Text('Hello, Flutter!'),
),
),
);
}
}
When the runApp
function is called, it creates a widget tree that describes the UI, defining which widget is associated with which parent. Then, it creates an element tree where each widget has its corresponding element node. Finally, it creates a render object tree, which defines exactly what the UI should look like. The Skia 2D engine is used to paint the render object into a complete UI.
What is MaterialApp?: MaterialApp
is a collection of widgets such as Scaffold
, AppBar
, and other styling elements, providing a starting point to create a material UI-based application.
Before we move forward we need to understanad what are widgets . As eberything in flutter is nothing but a widget we need to understand this to have a better understadning of how everything works . so shall we!!
What are Widgets ?
“Widgets are the central class hierarchy in the Flutter framework, serving as immutable descriptions of parts of a user interface. They can be inflated into elements, which manage the underlying render tree.”
According to the official defination of a widget we can see that its immutable which means it cannot be chnaged once create . so if you have to update the value of widget lets say text you have to create a new widget with same configuration .
As the second parts states it can be inflated into elements which then be used to render the ui. We will be knowing more in the article later.
“If the runtimeType
and key
properties of two widgets are equal, the new widget updates the existing element; otherwise, the old element is removed, and a new element is created and inserted into the tree.”
// i have removed debug part as we will be not be learning about it in this article.
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key? key;
@protected
@factory
Element createElement();
@override
@nonVirtual
bool operator ==(Object other) => super == other;
@override
@nonVirtual
int get hashCode => super.hashCode;
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
Building the Tree:
Flutter starts building the widget tree (starting from
runApp
).For each widget (e.g.,
Text("Hello")
), Flutter calls thecreateElement
method, which returns anElement
(likeTextElement
).
When State Changes:
If something changes (like a
setState
in aStatefulWidget
), the framework checks whether the widget should be updated or recreated usingcanUpdate
.The framework invokes
canUpdate
to decide whether the widget and its associatedElement
should be reused or replaced.If
canUpdate
returnstrue
, theElement
is rebuilt. This does not happen through direct calls from theWidget
orElement
; it’s the Flutter framework that manages these processes.
Rebuilding and Rendering:
- After the
Element
is rebuilt, it will update the UI by calling the necessary methods to render the updated widget to the screen.
- After the
Example of How Text
Works:
Text
is a type ofStatelessWidget
, which means:Text
overrides thecreateElement
method inherited fromStatelessWidget
.When Flutter needs to render the
Text
widget, it callsText.createElement()
, which creates aTextElement
(a type ofStatelessElement
).If the text or other properties of the
Text
widget change, the framework usescanUpdate
to decide if the existingTextElement
can be reused.If it can, the
TextElement
is rebuilt with the new properties; otherwise, a newTextElement
is created.
What is Stateless?:
import 'package:flutter/material.dart';
class MyStatelessWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('I am a StatelessWidget');
}
}
Now that we have these widgets, we need to build them, right? We can create a class that extends StatelessWidget
, which provides us with the build
method (an abstract class). We can override it and provide our widgets there. This ensures that whenever we call our StatelessWidget
class, it builds the widgets for us. Remember, every widget should be wrapped (either them or their parent widget) into a stateless or stateful widget to build it as a UI. As the name suggests, it's a stateless widget, meaning it doesn't hold any state because it is immutable.
Understanding in depth ?
abstract class StatelessWidget extends Widget {
const StatelessWidget({ super.key });
@override
StatelessElement createElement() => StatelessElement(this);
@protected
Widget build(BuildContext context);
}
here when we take a depper look into the world of statless widget we found that it takes BuildContext ( which we will talk later in the article ) and it calls the createElement() function that creates an object of StatelessElement !! as we disccused above that we have a element tree this functions creates a coressponsing element of the statefull widget this corresponding element is reponsisble of managing lifecycle of the widget .Intresting what is this statelessElement anyway and why are we creating an instance of it ?
We will understand in much depth how all of this is actually created as an element as renderd in next article.
What is StatefulWidget?:
import 'package:flutter/material.dart';
class MyStatefulWidget extends StatefulWidget {
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment'),
),
],
);
}
}
As the name suggests, it does all the things a stateless widget does but holds state. State is a type of class that holds data. We can change that data, and based on the data, it rebuilds the UI. You can create a state and then reuse it in other stateful widgets too. It's not used much, but it's possible. That's why you don't directly use setState()
in the stateful widget; you first map it to a state. It's like saying, "Hey, stateful widget, use this state," using the state name createState => stateName()
. Similarly, we tell the state, "Hey, state, use this stateful widget as your parent," which can be used to get variables from the stateful widget using widget.variableName
. We do this by State<statefulName>
.