Cómo personalizar el color de un flutter_spinbox
En un post anterior discutí sobre mi primera colaboración al open source, y vimos que no salió precisamente como quería pero aprendí bastante. Por eso hoy voy a revisar la solución que se aplicó al issue que traté de resolver.
Por si no lo recuerdan, o no fueron a ver mi anterior post, básicamente el issue trataba de aplicar el tema a los botones del spinbox en flutter porque éste sólo era aplicado cuando el widget estaba con el focus. Hay que recalcar que el issue solo aplicaba al material widget y no al de cupertino.
La solución aplicada no solamente resuelve este problema en particular, pero también prevé otros escenarios. Primero usando Material State Property y luego utilizando un Inherited Theme para complementar esta funcionalidad, pero ya me estoy adelantando. Comencemos preparando una app sencilla para probar estas adiciones.
Preparando la app
Ejecutamos flutter create flutter_spinbox_examples en la consola para generar una nueva aplicación de flutter.
flutter create flutter_spinbox_examples
Lenguaje del código: Bash (bash)
Luego borramos todo lo que no necesitamos y creamos 3 spinbox que serán nuestros widgets para comparar.
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget{
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Spinbox Example'),
),
body: ListView(
children: [
SpinBox(
value: 10,
decoration: const InputDecoration(labelText: 'Control'),
),
SpinBox(
value: 10,
decoration: const InputDecoration(labelText: 'Material State Property'),
),
SpinBox(
value: 10,
decoration: const InputDecoration(labelText: 'Inherited Theme'),
),
],
),
);
}
}
Lenguaje del código: Dart (dart)
Creo que podemos hacer algo mejor que esto, añadamos padding alrededor de nuestros spinbox.
body: ListView(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: SpinBox(
value: 10,
decoration: const InputDecoration(labelText: 'Control'),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: SpinBox(
value: 10,
decoration: const InputDecoration(labelText: 'Material State Property'),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: SpinBox(
value: 10,
decoration: const InputDecoration(labelText: 'Inherited Theme'),
),
),
],
),
Lenguaje del código: Dart (dart)
Mucho mejor.
Podemos ver que mientras el spinbox se encuentra unfocused está completamente gris, como si estuviera deshabilitado. En algunos casos esto no es problema, pero cuando tenemos varios spinbox juntos podría verse mejor si mantenemos el color uniforme independientemente del estado.
Por ello vamos a probar las dos formas que fueron añadidas.
Material State Property
Con Material State Property puedes resolver características cómo el color de los widgets, utilizando el método resolveWith.
En MaterialState tienes los estados: disabled, dragged, error, focused, hovered, pressed, scrolledUnder y selected. En el caso del spinbox, los estados que proporciona son solamente disabled, focused y error.
Vamos a resolver estos 3 estados en nuestro segundo spinbox para el atributo iconColor.
Padding(
padding: const EdgeInsets.all(16.0),
child: SpinBox(
value: 10,
decoration: const InputDecoration(labelText: 'Material State Property'),
iconColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.disabled)) {
return Colors.grey;
}
if (states.contains(MaterialState.error)) {
return Colors.red;
}
if (states.contains(MaterialState.focused)) {
return Colors.green;
}
}
),
),
),
Lenguaje del código: Dart (dart)
Pero lo único que cambió fue que los íconos ahora son negros. Esto es porque en este momento el spinbox no está en ningún estado. Si tocas el spinbox, los íconos de los botones se vuelven verdes, pero tan pronto como cambias el focus, el color vuelve a ser negro.
Lo que nos falta es agregar lo siguiente después de los if para resolver el color cuando el spinbox no está en ninguno de los estados.
return Colors.blue;
Lenguaje del código: Dart (dart)
Ahora el color de los íconos de los botones será siempre azul, y si le das el focus cambiarán a verde.
Para nuestra siguiente opción exploraremos cómo podemos hacer esto mismo, pero para varios spinbox a la vez.
Inherited Theme
Ahora que sabemos utilizar MaterialStateProperty para cambiar el color de nuestro spinbox, cuando tengamos varios spinbox en nuestra aplicación probablemente no querremos repetir el mismo código para cada uno de nuestros spinbox.
Para esto el widget nos provee la clase SpinBoxTheme. Que es un InheritedWidget, lo que quiere decir que los datos que definamos en este widget van a poder ser accedidos por todos sus hijos.
Para ver el SpinboxTheme en acción parece ser que necesitaremos agregar un widget spinbox más, así que agreguémoslo y rodeemos ambos spinbox con un Column.
Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: SpinBox(
value: 10,
decoration: const InputDecoration(labelText: 'Inherited Theme 1'),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: SpinBox(
value: 10,
decoration: const InputDecoration(labelText: 'Inherited Theme 2'),
),
),
]
)
Lenguaje del código: Dart (dart)
Ahora rodeemos el Column con un SpinBoxTheme, y verás que nos pide que definamos el atributo data, este atributo debe ser de la clase SpinBoxThemeData que tiene un atributo llamado iconColor al igual que nuestro Spinbox. En este atributo definiremos nuestro MaterialStateProperty.resolveWith.
SpinBoxThemeData(
iconColor: MaterialStateProperty.resolveWith((states) {
return Colors.red;
})
)
Lenguaje del código: Dart (dart)
Cómo ya vimos los estados en el subtítulo anterior, vamos a mantenerlo simple y retornaremos siempre el color rojo. El resultado final será el siguiente:
SpinBoxTheme(
data: SpinBoxThemeData(
iconColor: MaterialStateProperty.resolveWith((states) {
return Colors.red;
})
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: SpinBox(
value: 10,
decoration: const InputDecoration(labelText: 'Inherited Theme 1'),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: SpinBox(
value: 10,
decoration: const InputDecoration(labelText: 'Inherited Theme 2'),
),
),
]
)
),
Lenguaje del código: Dart (dart)
Orden de precedencia
Para finalizar, quiero agregar que dentro de un Spinbox existe un orden de precedencia para poder utilizar estos dos métodos simultáneamente.
- MaterialStateProperty directo en el atributo iconColor.
- MaterialStateProperty del SpinBoxTheme más cercano en el árbol de widgets.
- Colores del tema: que sólo se utilizan cuando el widget tiene el focus.
Si tienes más de un SpinBoxTheme en tu aplicación, el spinbox va a resolver el más cercano en el widget tree. Es decir, si tienes un SpinBoxTheme definido para toda tu aplicación y tienes otro definido más abajo, un spinbox bajo este último va a resolver su iconColor usando este último SpinBoxTheme. De esta forma puedes definir un SpinBoxTheme global y tener secciones en las que puedes sobrescribir su comportamiento añadiendo otro SpinBoxTheme.
Por ejemplo puedes hacer la prueba rodeando el widget MaterialApp con un SpinBoxTheme que retorne el color amarillo y verás que sólo el primer spinbox cambiará de color, ahora quita el SpinBoxTheme que define el color rojo más abajo y todos los spinbox cambiarán a color amarillo excepto el segundo que ya tiene definido su iconColor.
@override
Widget build(BuildContext context) {
return SpinBoxTheme(
data: SpinBoxThemeData(
iconColor: MaterialStateProperty.resolveWith((states) => Colors.yellow),
),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
),
);
}
Lenguaje del código: Dart (dart)
Palabras finales
Espero que este artículo te haya sido de utilidad. Como siempre te dejó aquí el código fuente para que lo puedas ver completo.