Understanding Flutter: A Guide to Widgets, Stateless and Stateful Widgets

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 the createElement method, which returns an Element (like TextElement).

  • When State Changes:

    • If something changes (like a setState in a StatefulWidget), the framework checks whether the widget should be updated or recreated using canUpdate.

    • The framework invokes canUpdate to decide whether the widget and its associated Element should be reused or replaced.

    • If canUpdate returns true, the Element is rebuilt. This does not happen through direct calls from the Widget or Element; 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.

Example of How Text Works:

  • Text is a type of StatelessWidget, which means:

    • Text overrides the createElement method inherited from StatelessWidget.

    • When Flutter needs to render the Text widget, it calls Text.createElement(), which creates a TextElement (a type of StatelessElement).

    • If the text or other properties of the Text widget change, the framework uses canUpdate to decide if the existing TextElement can be reused.

    • If it can, the TextElement is rebuilt with the new properties; otherwise, a new TextElement 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>.