latest

Introducción a la programación basada en widgets. Stateful widgets. Parte 1

En la lección anterior del curso de Flutter aprendimos como construir nuestros propios StatelessWidget y añadirlos al árbol de widgets de nuestra interfaz de usuario. En esta lección nos centraremos en los StatefulWidgets.

A diferencia de los widgets sin estado (inmutables), los StatefulWidget poseen un estado que es implementado como una estancia de la clase State y que puede cambiar a lo largo del ciclo de vida del widget. Cuando este estado es modificado, internamente, el widget es renderizado nuevamente. Como veremos a continuación, el cambio de estado se notifica al framework empleando el método setState().

El StatefulWidget en sí mismo es inmutable, la instancia de State es la que "se va transformando". Se crea al inicio del ciclo de vida del StatefulWidget (con el método createState()) y persiste durante el ciclo de vida del widget con posibles modificaciones durante ese tiempo que, por ende, fuerzan un nuevo renderizado y cambian el aspecto visual del widget.

Los StatefulWidget permiten que nuestras interfaces de usuario ganen en dinamismo. Este dinamismo se gestiona con la clase State y no desde el propio widget, esto proporciona una mayor homogeneidad con el objetivo de tratar de manera similar a los widgets de las clases StatelessWidget y StatefulWidget.

Bueno, vamos a tratar de explicar lo que se ha dicho en los párrafos anteriores con el siguiente código de ejemplo. Lo primero es identificar, como ya hicimos con los StatelessWidget, cuál es el esqueleto o estructura básica para componer nuestro propio StatefulWidget. El widget con estado, mutable o, simplemente, StatefulWidget se define gracias a la siguiente estructura:

// Nuestro widget extiende de StatefulWidget
class MiStatefulWidget extends StatefulWidget {

  // Propiedades inmutables / estáticas no cambian desde la inicialización
  // por eso se emplea "final"
  final prop1;
  // ...
  final propN;

  // Constructor con propiedades del widget (params. opcionales nombrados)
  MiStatefulWidget({Key key, this.prop1, this.propN}) : super(key: key);

  // Método sobreescrito que crea e inicializa el estado del StatefulWidget
  @override
  _MiStatefulWidgetState createState() => _MiStatefulWidgetState();
}

class _MiStatefulWidgetState extends State<MiStatefulWidget> {

  //Propiedades dinámicas usadas por widgets del árbol 
  var _propDin1;
  // ...
  // var _propDinN = 0;
  
  void _funcionCambioEstado(){
  	//...
     	setState(() {
    		// Actualizar las propiedades dinámicas en este VoidCallback
        	// _propDin1 = ...
        	// ...
     	});
  }

  @override
  Widget build(BuildContext context) {
  	return new Container(
    	// Árbol de widgets
        child: new RaisedButton(onPressed: _funcionCambioEstado, 
        // ...),
        // ...
    );
  }
}

Como podéis comprobar, el código anterior presenta el esquema básico de un StatefulWidget. Comenzamos heredando nuestro MiStatefulWidget de la clase StatefulWidget, prestad atención y observad que la clase MiStatefulWidget no sobreescribe el método build() como si ocurría en los StatelessWidget; en este caso, es la clase derivada de State (State<MiStatefulWidget>) la que implementará dicho método y construirá la estructura jerárquica de widgets.

Volviendo a la definición de MiStatefulWidget, lo relevante son las propiedades inmutables que definen el widget, declaradas como final. Además, se implementa un constructor que puede usarse para inicializar dichas propiedades. En esta lección no vamos a entrar en detalles en la propiedad key, simplemente indicar que es una propiedad heredada de StatefulWidget y que sirve como un identificador del widget. Por último, la clase MiStatefulWidget sobreescribe el método createState(), en el cuerpo de este método se inicializa una instancia de la clase _MiStatefulWidgetState (que hereda de State<MiStatefulWidget>).

Entrando ahora en detalle sobre la clase que hereda de State denominada _MiStatefulWidgetState, debemos remarcar que suele implementarse como una clase de acceso protegido, por eso emplea el guión bajo anteponiendo el nombre de la clase. Cuenta con una serie de propiedades dinámicas que son modificables y definen el estado del widget. Estas propiedades son usadas por los widgets del árbol construido en el método sobreescrito build().  Para modificar las propiedades dinámicas y, por tanto, el estado del MiStatefulWidget se invocan uno o varios métodos de callback. En nuestro ejemplo de esqueleto básico para un StatefulWidget hemos implementado un único método de callback denominado _funcionCambioEstado(). En el cuerpo de dicho método se llama a setState(VoidCallback) y es en el VoidCallback recibido como argumento donde se actualizan las propiedades dinámicas.

Tras la ejecución de setState() automáticamente se vuelve a renderizar el widget. En este ejemplo de esquema básico de StatefulWidget, el callback funcionCambioEstado() es invocado desde un botón rectangular de clase RaisedButton cuando éste es presionado (onPressed:).

Para ver un ejemplo real / práctico de StatefulWidget que use este esquema básico en su definición, basta con fijarnos en él código por defecto que se genera en main.dart cuando creamos un nuevo proyecto en Flutter.

Como ya vimos con anterioridad, un nuevo proyecto Flutter construye una aplicación por defecto que sigue el estilo material design y que consiste en un simple contador de las veces que se pulsa un botón flotante.

El código de esta aplicación es el siguiente:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final title;

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

class _MyHomePageState extends State<MyHomePage> {
  var _counter = 0;

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke "debug painting" (press "p" in the console, choose the
          // "Toggle Debug Paint" action from the Flutter Inspector in Android
          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

Vamos a explicar brevemente el código, ahora que contamos con el suficiente conocimiento para entenderlo. En la base de nuestro árbol de widgets tenemos un StatelessWidget que se denomina MyApp. En su método sobreescrito build(), se implementa un widget MaterialApp (la interfaz de usuario sigue el estilo visual material design) y en la ruta inicial de la aplicación (propiedad home) se añade una instancia del StatefulWidget denominado MyHomePage.

MyHomePage tiene una propiedad (estática) title, que usaremos en el AppBar posteriormente. También podemos observar en el código el constructor que inicializa esa propiedad, así como una propiedad key opcional. Por último, se invoca el método createState() en cuyo cuerpo se inicializa el estado asociado al StatefulWidget, creando una instancia de _MyHomePageState.

_MyHomePageState extiende de State<MyHomePage> , observad que se aprovecha del uso de los genéricos en la declaración de herencia para determinar la clase particular de StatefulWidget asociada a este State. Prestando atención al método sobreescrito build() podéis ver el árbol de widgets a renderizar cuando se modifica el estado. Específicamente, hay un Scafold del que se usan las propiedades opcionales nombradas appBar, body y floatingActionButton.

En la propiedad title del AppBar se añade el texto del la propiedad title del MyHomePage, accesible directamente desde el objeto denominado widget, gracias a haber extendido del genérico (State<MyHomePage>).

Por su parte, en la propiedad body se integran una serie de widgets que ya conocemos de lecciones anteriores (Center, Column y Text). Mientras que el widget Center posiciona los widgets que contiene centrados en el eje horizontal; la propiedad de Column mainAxisAlignment nos permite el centrado vertical de los widgets de la columna con MainAxisAlignment.center. El segundo widget Text de la columna emplea como texto la propiedad dinámica _counter definida en _MyHomePageState y que está asociada al estado del widget pero, ¿Cómo se modifica esta propiedad y, por tanto, el estado del widget MyHomePage? la clave están en el FloatingActionButton añadido al Scafold al final del código. A parte de un icono ("+") y un tooltipcuenta con la propiedad onPressed a la que se asocia el callback _incrementCounter.

El método de callback _incrementCounter() está definido dentro de la clase _MyHomePageState , viene a ser lo mismo que la _funcionCambioEstado() que se presentaba en el esqueleto básico de definición de un StatefulWidget. Es muy importante, invocar setState((){}) para que el renderizado del widget se produzca de manera automática al pulsar sobre el FloatingActionButton. Dentro del VoidCallback que recibe como argumento setState() realizamos las modificaciones necesarias sobre las propiedades dinámicas, en este caso únicamente incrementar _counter.

Así pues ya podemos decir que hemos visto como construir un StatefulWidget básico. En la siguiente lección realizaremos una adaptación, usando widgets con estado, de la interfaz de usuario que vimos cuando estudiábamos los StatelessWidget.

Author image
Iván González is postdoctoral researcher at the Castilla-La Mancha University.
Ciudad Real (Spain)