Skip to main content

Pasar a una nueva pila de desarrollo te hace consciente de tus prioridades. Cerca de la parte superior de mi lista están estos tres:

  • Los conceptos sólidos abordan eficazmente la complejidad al proporcionar formas simples y relevantes de estructurar pensamientos, lógica o datos.
  • El código claro nos permite expresar esos conceptos de manera limpia, sin distraernos por problemas de lenguaje, repetición excesiva o detalles auxiliares.
  • La iteración rápida es clave para la experimentación y el aprendizaje, y los equipos de desarrollo de software aprenden para ganarse la vida: cuáles son realmente los requisitos y cuál es la mejor manera de cumplirlos con conceptos expresados ​​en código.

Flutter es una nueva plataforma para desarrollar aplicaciones de Android e iOS desde una única base de código, escrita en Dart. Dado que nuestros requisitos hablaban de una interfaz de usuario bastante compleja que incluía gráficos animados, la idea de construirla solo una vez parecía muy atractiva. Mis tareas implicaron el ejercicio de las herramientas CLI de Flutter, algunos widgets preconstruidos y su motor de renderización en 2D, además de escribir un montón de código Dart para modelar y animar gráficos. A continuación, comparto algunos aspectos conceptuales destacados de mi experiencia de aprendizaje y proporciono un punto de partida para su propia evaluación de la pila de Flutter / Dart.

Esta es la primera parte de una introducción en dos partes a Flutter y sus conceptos ‘widget’ y ‘tween’. Ilustraré la fortaleza de estos conceptos utilizándolos para mostrar y animar gráficos como el que se muestra arriba. Las muestras de código completo deberían proporcionar una impresión del nivel de claridad del código que se puede lograr con Dart. E incluiré suficientes detalles que podrá seguir en su propia computadora portátil (y emulador o dispositivo) y experimentar la duración del ciclo de desarrollo de Flutter.

El punto de partida es una nueva instalación de Flutter . correr

$ flutter doctor

para verificar la configuración:

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter
    (Channel beta, v0.5.1, on Mac OS X 10.13.6 17G65, locale en-US)
[✓] Android toolchain - develop for Android devices
    (Android SDK 28.0.0)
[✓] iOS toolchain - develop for iOS devices (Xcode 9.4)
[✓] Android Studio (version 3.1)
[✓] IntelliJ IDEA Community Edition (version 2018.2.1)
[✓] Connected devices (1 available)
• No issues found!

 

Aquí me tardé un poco configurando los distintos requisitos que pedía el doctor de Flutter, pero con suficientes marcas de verificación, puedes continuar creando una aplicación Flutter. Llamémoslo charts:

$ flutter create charts

Eso debería darle un directorio del mismo nombre:

charts
  android
  ios
  lib
    main.dart

Se han generado unos sesenta archivos, que constituyen una aplicación de muestra completa que puede instalarse tanto en Android como en iOS. Haremos todos nuestros main.dartarchivos de codificación y hermanos, sin necesidad de tocar ninguno de los otros archivos o directorios.

Debe verificar que puede iniciar la aplicación de muestra. Inicie un emulador o conecte un dispositivo, luego ejecute

$ flutter run

en el directorio charts debería ver una aplicación de conteo simple en su emulador o dispositivo. Utiliza widgets de Material Design, lo cual es bueno, pero opcional. Como la capa superior de la arquitectura Flutter, esos widgets son completamente reemplazables.

Comencemos reemplazando los contenidos de main.dartcon el siguiente código, un punto de partida simple para jugar con animaciones de gráficos.

import 'dart:math';

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: ChartPage()));
}

class ChartPage extends StatefulWidget {
  @override
  ChartPageState createState() => ChartPageState();
}

class ChartPageState extends State<ChartPage> {
  final random = Random();
  int dataSet;

  void changeData() {
    setState(() {
      dataSet = random.nextInt(100);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('Data set: $dataSet'),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.refresh),
        onPressed: changeData,
      ),
    );
  }
}

 

Guarde los cambios, luego reinicie la aplicación. Puede hacer eso desde la terminal, presionando R. Esta operación de “reinicio completo” descarta el estado de la aplicación y luego reconstruye la interfaz de usuario. Para las situaciones donde el estado de la aplicación existente todavía tiene sentido después del cambio de código, se puede presionar rpara realizar una “recarga en caliente”, que solo reconstruye la interfaz de usuario. También hay un plugin de Flutter para IntelliJ IDEA que proporciona la misma funcionalidad integrada con un editor de Dart:

 

Una vez reiniciado, la aplicación muestra una etiqueta de texto centrada que dice Data set: nully un botón de acción flotante para actualizar los datos. Sí, humildes comienzos.

Para tener una idea de la diferencia entre la recarga en caliente y el reinicio completo, intente lo siguiente: Después de presionar el botón de acción flotante varias veces, anote el número del conjunto de datos actual, luego reemplace Icons.refreshcon Icons.addel código, guarde, y hacer una recarga caliente. Observe que el botón cambia, pero que el estado de la aplicación se conserva; todavía estamos en el mismo lugar en la secuencia aleatoria de números. Ahora deshaga el ícono de cambio, guarde y reinicie por completo. El estado de la aplicación se ha restablecido y volvemos a Data set: null.

Nuestra sencilla aplicación muestra dos aspectos centrales del concepto del widget Flutter en acción:

  • La interfaz de usuario está definida por un árbol de widgets inmutablesque se construye a través de un foxtrot de llamadas de constructor (donde se configuran widgets) y buildmétodos (donde las implementaciones de widgets pueden decidir cómo se ven sus subárboles). La estructura de árbol resultante para nuestra aplicación se muestra a continuación, con el papel principal de cada widget entre paréntesis. Como puede ver, aunque el concepto de widget es bastante amplio, cada tipo de widget concreto suele tener una responsabilidad muy específica.
  • Con un árbol inmutable de widgets inmutables que definen la interfaz de usuario, la única forma de cambiar esa interfaz es reconstruir el árbol. Flutter se encarga de eso, cuando el próximo fotograma se vence. Todo lo que tenemos que hacer es decirle a Flutter que ha cambiado algún estado del que depende un subárbol. La raíz de dicho subárbol dependiente del estado debe ser a StatefulWidget. Como cualquier widget decente, a StatefulWidgetno es mutable, pero su subárbol lo crea un Stateobjeto que sí lo es. Flutter conserva los Stateobjetos en las reconstrucciones de árboles y los une a sus respectivos widgets en el nuevo árbol durante la construcción. A continuación, determinan cómo se construye el subárbol de ese widget. En nuestra aplicación, ChartPagees un StatefulWidgetcon ChartPageStatecomo es State. Cada vez que el usuario presiona el botón, ejecutamos algún código para cambiarChartPageState.Hemos demarcado el cambio setStatepara que Flutter pueda hacer su mantenimiento y programar el árbol de widgets para la reconstrucción. Cuando eso suceda, ChartPageStatese compilará un subárbol ligeramente diferente enraizado en la nueva instancia de ChartPage.

Los widgets inmutables y los subárboles dependientes del estado son las herramientas principales que Flutter pone a nuestra disposición para abordar las complejidades de la administración estatal en UI elaboradas que responden a eventos asincrónicos como pulsaciones de botones, marcas de temporizador o datos entrantes. Desde mi experiencia de escritorio, diría que esta complejidad es muy real. Evaluar la fuerza del enfoque de Flutter es, y debería ser, un ejercicio para el lector: pruébelo en algo no trivial.

Nuestra aplicación de gráficos se mantendrá simple en términos de estructura de widgets, pero haremos un poco de gráficos personalizados animados. El primer paso es reemplazar la representación textual de cada conjunto de datos con un gráfico muy simple. Dado que un conjunto de datos involucra actualmente solo un número en el intervalo 0..100, el gráfico será un gráfico de barras con una sola barra, cuya altura está determinada por ese número. Usaremos un valor inicial de 50para evitar una nullaltura:

import 'dart:math';

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: ChartPage()));
}

class ChartPage extends StatefulWidget {
  @override
  ChartPageState createState() => ChartPageState();
}

class ChartPageState extends State<ChartPage> {
  final random = Random();
  int dataSet = 50;

  void changeData() {
    setState(() {
      dataSet = random.nextInt(100);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: CustomPaint(
          size: Size(200.0, 100.0),
          painter: BarChartPainter(dataSet.toDouble()),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.refresh),
        onPressed: changeData,
      ),
    );
  }
}

class BarChartPainter extends CustomPainter {
  static const barWidth = 10.0;

  BarChartPainter(this.barHeight);

  final double barHeight;

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue[400]
      ..style = PaintingStyle.fill;
    canvas.drawRect(
      Rect.fromLTWH(
        (size.width - barWidth) / 2.0,
        size.height - barHeight,
        barWidth,
        barHeight,
      ),
      paint,
    );
  }

  @override
  bool shouldRepaint(BarChartPainter old) => barHeight != old.barHeight;
}

 

CustomPaintes un widget que delega la pintura en una CustomPainterestrategia. Nuestra implementación de esa estrategia dibuja una barra única.

El siguiente paso es agregar animación. Cada vez que cambia el conjunto de datos, queremos que la barra cambie la altura suavemente en lugar de abruptamente. Flutter tiene un AnimationControllerconcepto para orquestar animaciones, y al registrar un oyente, se nos dice cuando cambia el valor de la animación, un doble que va de cero a uno. Cuando eso sucede, podemos llamar setStatecomo antes y actualizar ChartPageState.

Por razones de exposición, nuestro primer intento será feo:

import 'dart:math';
import 'dart:ui' show lerpDouble;

import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: ChartPage()));
}

class ChartPage extends StatefulWidget {
  @override
  ChartPageState createState() => ChartPageState();
}

class ChartPageState extends State<ChartPage> with TickerProviderStateMixin {
  final random = Random();
  int dataSet = 50;
  AnimationController animation;
  double startHeight;   // Strike one.
  double currentHeight; // Strike two.
  double endHeight;     // Strike three. Refactor.

  @override
  void initState() {
    super.initState();
    animation = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    )..addListener(() {
        setState(() {
          currentHeight = lerpDouble( // Strike one.
            startHeight,
            endHeight,
            animation.value,
          );
        });
      });
    startHeight = 0.0;                // Strike two.
    currentHeight = 0.0;
    endHeight = dataSet.toDouble();
    animation.forward();
  }

  @override
  void dispose() {
    animation.dispose();
    super.dispose();
  }

  void changeData() {
    setState(() {
      startHeight = currentHeight;    // Strike three. Refactor.
      dataSet = random.nextInt(100);
      endHeight = dataSet.toDouble();
      animation.forward(from: 0.0);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: CustomPaint(
          size: Size(200.0, 100.0),
          painter: BarChartPainter(currentHeight),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.refresh),
        onPressed: changeData,
      ),
    );
  }
}

class BarChartPainter extends CustomPainter {
  static const barWidth = 10.0;

  BarChartPainter(this.barHeight);

  final double barHeight;

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue[400]
      ..style = PaintingStyle.fill;
    canvas.drawRect(
      Rect.fromLTWH(
        (size.width - barWidth) / 2.0,
        size.height - barHeight,
        barWidth,
        barHeight,
      ),
      paint,
    );
  }

  @override
  bool shouldRepaint(BarChartPainter old) => barHeight != old.barHeight;
}

 

Ay. ¡La complejidad ya asoma su horrible cabeza, y nuestro conjunto de datos sigue siendo solo un número! El código necesario para configurar el control de animación es una preocupación menor, ya que no se ramifica cuando obtenemos más datos de gráfico. El verdadero problema es las variables startHeightcurrentHeightendHeightque reflejan los cambios realizados en el conjunto de datos y el valor de la animación, y se actualizan en tres lugares diferentes.

Necesitamos un concepto para lidiar con este desastre.

Ingrese tweens . Aunque no son exclusivos de Flutter, son un concepto deliciosamente simple para estructurar el código de animación. Su principal contribución es reemplazar el enfoque imperativo anterior por uno funcional. Una interpolación es un valor . Describe la ruta tomada entre dos puntos en un espacio de otros valores, como los gráficos de barras, ya que el valor de la animación se ejecuta de cero a uno.

Los preadolescentes son genéricos en el tipo de estos otros valores y se pueden expresar en Dart como objetos del tipo Tween<T>:

abstract class Tween<T> {
  final T begin;
  final T end;
  
  Tween(this.begin, this.end);
  
  T lerp(double t);
}

La jerga lerpproviene del campo de los gráficos por computadora y es la abreviatura de interpolación lineal (como sustantivo) e interpolación lineal(como verbo). El parámetro tes el valor de la animación y, por lo tanto, una interpolación debe pasar de begin(cuando tes cero) a end(cuando tes uno).

La Tween<T>clase de SDK de Flutter es muy similar a la anterior, pero es una clase concreta que admite mutación beginend. No estoy del todo seguro de por qué se hizo esa elección, pero probablemente haya buenas razones para ello en áreas del soporte de animación del SDK que aún tengo que explorar. A continuación, usaré Flutter Tween<T>, pero pretendo que es inmutable.

Podemos limpiar nuestro código usando un solo Tween<double>para la altura de la barra:

import 'dart:math';

import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: ChartPage()));
}

class ChartPage extends StatefulWidget {
  @override
  ChartPageState createState() => ChartPageState();
}

class ChartPageState extends State<ChartPage> with TickerProviderStateMixin {
  final random = Random();
  int dataSet = 50;
  AnimationController animation;
  Tween<double> tween;

  @override
  void initState() {
    super.initState();
    animation = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    tween = Tween<double>(begin: 0.0, end: dataSet.toDouble());
    animation.forward();
  }

  @override
  void dispose() {
    animation.dispose();
    super.dispose();
  }

  void changeData() {
    setState(() {
      dataSet = random.nextInt(100);
      tween = Tween<double>(
        begin: tween.evaluate(animation),
        end: dataSet.toDouble(),
      );
      animation.forward(from: 0.0);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: CustomPaint(
          size: Size(200.0, 100.0),
          painter: BarChartPainter(tween.animate(animation)),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.refresh),
        onPressed: changeData,
      ),
    );
  }
}

class BarChartPainter extends CustomPainter {
  static const barWidth = 10.0;

  BarChartPainter(Animation<double> animation)
      : animation = animation,
        super(repaint: animation);

  final Animation<double> animation;

  @override
  void paint(Canvas canvas, Size size) {
    final barHeight = animation.value;
    final paint = Paint()
      ..color = Colors.blue[400]
      ..style = PaintingStyle.fill;
    canvas.drawRect(
      Rect.fromLTWH(
        (size.width - barWidth) / 2.0,
        size.height - barHeight,
        barWidth,
        barHeight,
      ),
      paint,
    );
  }

  @override
  bool shouldRepaint(BarChartPainter old) => false;
}

 

Estamos utilizando Tweenpara empaquetar los puntos finales de la animación de la altura del compás en un solo valor. Se conecta perfectamente con el AnimationControllerCustomPainter, evitando árbol Reproductor reconstruye durante la animación como la infraestructura trémolo ahora marca CustomPaintde repintado en cada tic animación, en lugar de marcar todo el ChartPagesubárbol para reconstruir, con la redistribución, y volver a pintar. Estas son mejoras definitivas. Pero hay más en el concepto de tween; ofrece estructura para organizar nuestros pensamientos y nuestro código, y realmente no nos lo tomamos en serio. El concepto de tween dice:

Animar Ts trazando un camino en el espacio de todos los Ts como el valor de la animación se ejecuta de cero a uno. Modelar el camino con a Tween<T>.

En el código anterior, Tes a double, pero no queremos animar doubles, ¡queremos animar gráficos de barras! Bueno, de acuerdo, barras sueltas por ahora, pero el concepto es fuerte, y escala, si lo permitimos.

(Quizás se pregunte por qué no llevamos ese argumento un paso más e insistimos en animar conjuntos de datos en lugar de sus representaciones como gráficos de barras. Esto se debe a que los conjuntos de datos, en contraste con los gráficos de barras que son objetos gráficos, generalmente no habitan en los espacios Cuando los conjuntos de datos para gráficos de barras normalmente incluyen datos numéricos mapeados contra categorías de datos discretos, pero sin la representación espacial como gráficos de barras, no existe una noción razonable de una ruta fluida entre dos conjuntos de datos que involucran diferentes categorías.

Volviendo a nuestro código, necesitaremos un Bartipo y una BarTweenpara animarlo. Vamos a extraer las clases relacionadas con el bar en su propio bar.dartarchivo al lado de main.dart:

import 'dart:ui' show lerpDouble;

import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';

class Bar {
  Bar(this.height);

  final double height;

  static Bar lerp(Bar begin, Bar end, double t) {
    return Bar(lerpDouble(begin.height, end.height, t));
  }
}

class BarTween extends Tween<Bar> {
  BarTween(Bar begin, Bar end) : super(begin: begin, end: end);

  @override
  Bar lerp(double t) => Bar.lerp(begin, end, t);
}

class BarChartPainter extends CustomPainter {
  static const barWidth = 10.0;

  BarChartPainter(Animation<Bar> animation)
      : animation = animation,
        super(repaint: animation);

  final Animation<Bar> animation;

  @override
  void paint(Canvas canvas, Size size) {
    final bar = animation.value;
    final paint = Paint()
      ..color = Colors.blue[400]
      ..style = PaintingStyle.fill;
    canvas.drawRect(
      Rect.fromLTWH(
        (size.width - barWidth) / 2.0,
        size.height - bar.height,
        barWidth,
        bar.height,
      ),
      paint,
    );
  }

  @override
  bool shouldRepaint(BarChartPainter old) => false;
}

 

Estoy siguiendo una convención de SDK de Flutter aquí para definir BarTween.lerpen términos de un método estático en la Barclase. Esto funciona bien para los tipos simples como BarColorRecty muchos otros, pero tendremos que reconsiderar el enfoque de tipos de gráficos más complicados. No hay double.lerpen el SDK de Dart, por lo que estamos usando la función lerpDoubledel dart:uipaquete para el mismo efecto.

Nuestra aplicación ahora se puede volver a expresar en términos de barras, como se muestra en el siguiente código; He aprovechado la oportunidad para prescindir del dataSetcampo.

import 'dart:math';

import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';

import 'bar.dart';

void main() {
  runApp(MaterialApp(home: ChartPage()));
}

class ChartPage extends StatefulWidget {
  @override
  ChartPageState createState() => ChartPageState();
}

class ChartPageState extends State<ChartPage> with TickerProviderStateMixin {
  final random = Random();
  AnimationController animation;
  BarTween tween;

  @override
  void initState() {
    super.initState();
    animation = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    tween = BarTween(Bar(0.0), Bar(50.0));
    animation.forward();
  }

  @override
  void dispose() {
    animation.dispose();
    super.dispose();
  }

  void changeData() {
    setState(() {
      tween = BarTween(
        tween.evaluate(animation),
        Bar(random.nextDouble() * 100.0),
      );
      animation.forward(from: 0.0);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: CustomPaint(
          size: Size(200.0, 100.0),
          painter: BarChartPainter(tween.animate(animation)),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.refresh),
        onPressed: changeData,
      ),
    );
  }
}

 

La nueva versión es más larga, y el código adicional debería tener su peso. Lo hará, a medida que abordemos el aumento de la complejidad del gráfico en la segunda parte . Nuestros requisitos hablan de barras de colores, barras múltiples, datos parciales, barras apiladas, barras agrupadas, barras apiladas y agrupadas, … todo animado. Manténganse al tanto.