Nuevos botones y temas de botones
Las clases básicas de botones de material han sido reemplazadas.
Resumen
#Se ha añadido un nuevo conjunto de Widgets y temas de botones de material básicos a Flutter. Las clases originales se han puesto en desuso y eventualmente se eliminarán. El objetivo general es hacer que los botones sean más flexibles y fáciles de configurar mediante parámetros de constructor o temas.
Los Widgets FlatButton, RaisedButton y OutlineButton han sido
reemplazados por TextButton, ElevatedButton y OutlinedButton
respectivamente. Cada nueva clase de botón tiene su propio tema:
TextButtonTheme, ElevatedButtonTheme y
OutlinedButtonTheme. La clase original ButtonTheme ya no se
utiliza. La apariencia de los botones se especifica mediante un objeto ButtonStyle,
en lugar de un gran conjunto de parámetros y propiedades del Widget.
Esto es aproximadamente comparable a la forma en que se define la apariencia
del texto con un objeto TextStyle. Los nuevos temas de botones
también se configuran con un objeto ButtonStyle. Un ButtonStyle
es
en sí mismo solo una colección de propiedades visuales. Muchas de estas
propiedades se definen con MaterialStateProperty, lo que significa que
su valor puede depender del State del botón.
Contexto
#En lugar de intentar evolucionar las clases de botones existentes y su tema in-situ, hemos introducido nuevos Widgets de botones de reemplazo y temas. Además de librarnos del laberinto de compatibilidad hacia atrás que implicaría evolucionar las clases existentes in-situ, los nuevos nombres vuelven a sincronizar Flutter con la especificación de Material Design, la cual utiliza los nuevos nombres para los componentes de botones.
| Widget antiguo | Tema antiguo | Widget nuevo | Tema nuevo |
|---|---|---|---|
FlatButton |
ButtonTheme |
TextButton |
TextButtonTheme |
RaisedButton |
ButtonTheme |
ElevatedButton |
ElevatedButtonTheme |
OutlineButton |
ButtonTheme |
OutlinedButton |
OutlinedButtonTheme |
Los nuevos temas siguen el patrón "normalizado" que Flutter adoptó para los nuevos Widgets de Material hace aproximadamente un año. Las propiedades del tema y los parámetros del constructor del Widget son nulos de forma predeterminada. Las propiedades del tema y los parámetros del Widget que no son nulos especifican una invalidación del valor predeterminado del componente. Implementar y documentar los valores predeterminados es responsabilidad exclusiva de los Widgets de componentes de botones. Los valores predeterminados en sí se basan principalmente en el colorScheme y textTheme generales del tema.
Visualmente, los nuevos botones se ven un poco diferentes, porque coinciden con la especificación actual de Material Design y porque sus colores están configurados en términos del ColorScheme del Theme general. Hay otras pequeñas diferencias en padding, radios de esquinas redondeadas y el feedback de hover/focus/pressed.
Muchas aplicaciones podrán simplemente sustituir los nombres de las clases antiguas
por los nuevos. Las aplicaciones con pruebas de imágenes golden o con botones cuya
apariencia se haya configurado con parámetros del constructor o con el
ButtonTheme original pueden necesitar consultar la guía de migración y el
material de introducción que sigue.
Cambio de API: ButtonStyle en lugar de propiedades de estilo individuales
#Excepto para casos de uso simples, las APIs de las nuevas clases de botones
no son compatibles con las clases antiguas. Los atributos visuales de los nuevos
botones y temas se configuran con un único objeto ButtonStyle,
simular a cómo se puede configurar un Widget TextField o Text con un
objeto TextStyle. La mayoría de las propiedades de ButtonStyle se definen con
MaterialStateProperty, de modo que una sola propiedad puede representar
diferentes valores dependiendo del State presionado/enfocado/con el cursor encima/etc.
del botón.
El ButtonStyle de un botón no define las propiedades visuales del botón,
sino que define invalidaciones de las propiedades visuales predeterminadas del botón,
donde las propiedades predeterminadas son calculadas por el propio Widget de botón
mismo. Por ejemplo, para invalidar el color de primer plano
(texto/icono) predeterminado de un TextButton para todos los States, uno podría escribir:
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all<Color>(Colors.blue),
),
onPressed: () { },
child: Text('TextButton'),
)
Este tipo de invalidación es común; sin embargo, en muchos casos lo que también
se necesita son invalidaciones para los colores de superposición que el botón de texto utiliza
para indicar su State de pasar el cursor/foco/presionar. Esto se puede hacer
agregando la propiedad overlayColor al ButtonStyle.
TextButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all<Color>(Colors.blue),
overlayColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered))
return Colors.blue.withOpacity(0.04);
if (states.contains(MaterialState.focused) ||
states.contains(MaterialState.pressed))
return Colors.blue.withOpacity(0.12);
return null; // Defer to the widget's default.
},
),
),
onPressed: () { },
child: Text('TextButton')
)
Una propiedad de color MaterialStateProperty solo necesita devolver un valor para los
colores cuyo valor predeterminado deba invalidarse. Si devuelve null, se utilizará
el valor predeterminado del Widget en su lugar. Por ejemplo, para invalidar únicamente
el color de superposición de foco del botón de texto:
TextButton(
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.focused))
return Colors.red;
return null; // Defer to the widget's default.
}
),
),
onPressed: () { },
child: Text('TextButton'),
)
Los métodos de utilidad styleFrom() de ButtonStyle
#
La especificación de Material Design define los colores de primer plano y superposición de los botones en
términos del color primario del esquema de colores. El color primario se
renderiza con diferentes opacidades, según el State del botón. Para
simplificar la creación de un estilo de botón que incluya todas las propiedades
que dependen de los colores del esquema de color, cada clase de botón incluye un
método estático styleFrom() que construye un ButtonStyle a partir de un conjunto simple
de valores, incluidos los colores de ColorScheme de los que depende.
Este ejemplo crea un botón que sobrescribe su color de primer plano, así como su color de superposición, utilizando el color primario especificado y las opacidades de la especificación de Material Design.
TextButton(
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
),
onPressed: () { },
child: Text('TextButton'),
)
La documentación de TextButton indica que el color de primer plano cuando
el botón está inhabilitado se basa en el color disabledForegroundColor
del esquema de colores. Para invalidar eso también, utilizando styleFrom():
TextButton(
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
disabledForegroundColor: Colors.red,
),
onPressed: null,
child: Text('TextButton'),
)
Utilizar el método styleFrom() es la forma preferida de crear un
ButtonStyle si estás intentando crear una variación de Material Design.
El enfoque más flexible es definir un ButtonStyle
directamente, con valores de MaterialStateProperty para los States cuya
apariencia deseas invalidar.
Valores predeterminados de ButtonStyle
#Los Widgets como las nuevas clases de botones calculan sus valores predeterminados
basados en el colorScheme y el textTheme generales del tema, así como en el
State actual del botón. En algunos casos, también consideran si el
esquema de colores del tema general es claro u oscuro. Cada botón tiene un
método protegido que calcula su estilo predeterminado según sea necesario. Aunque
las aplicaciones no llamarán a este método directamente, su documentación de la API explica cuáles
son todos los valores predeterminados. Cuando un botón o tema de botón especifica un
ButtonStyle, solo las propiedades no nulas del estilo del botón invalidan los
valores predeterminados calculados. El parámetro style del botón invalida las
propiedades no nulas especificadas por el tema del botón correspondiente. Por ejemplo, si la
propiedad foregroundColor del estilo de un TextButton no es nula, esta
invalida la misma propiedad para el estilo de TextButtonTheme.
Como se explicó anteriormente, cada clase de botón incluye un método estático
llamado styleFrom el cual construye un ButtonStyle a partir de un simple conjunto de
valores, incluidos los colores de ColorScheme de los que depende. En muchos casos
comunes, usar styleFrom para crear un ButtonStyle único que
invalide los valores predeterminados es lo más simple. Esto es particularmente cierto cuando
el objetivo del estilo personalizado es invalidar uno de los colores del esquema de colores,
como primary o onPrimary de los que depende el estilo predeterminado.
Para otros casos, puedes crear un objeto ButtonStyle directamente.
Hacerlo te permite controlar el valor de las propiedades visuales, como los colores,
para todos los States posibles del botón, como presionado, con el cursor encima,
inhabilitado y enfocado.
Guía de migración
#Utiliza la siguiente información para migrar tus botones a la nueva API.
Restaurar el aspecto visual original de los botones
#En muchos casos, es posible simplemente cambiar de la clase de botón antigua a la nueva. Eso asumiendo que los pequeños cambios en tamaño/forma y el cambio probablemente mayor en los colores no sean un problema.
Para preservar la apariencia de los botones originales en estos casos, uno puede
definir estilos de botones que coincidan con los originales tanto como se
desee. Por ejemplo, el siguiente estilo hace que un TextButton se vea
como un FlatButton predeterminado:
final ButtonStyle flatButtonStyle = TextButton.styleFrom(
foregroundColor: Colors.black87,
minimumSize: Size(88, 36),
padding: EdgeInsets.symmetric(horizontal: 16),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(2)),
),
);
TextButton(
style: flatButtonStyle,
onPressed: () { },
child: Text('Looks like a FlatButton'),
)
Del mismo modo, para hacer que un ElevatedButton se vea como un RaisedButton predeterminado:
final ButtonStyle raisedButtonStyle = ElevatedButton.styleFrom(
foregroundColor: Colors.black87,
backgroundColor: Colors.grey[300],
minimumSize: Size(88, 36),
padding: EdgeInsets.symmetric(horizontal: 16),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(2)),
),
);
ElevatedButton(
style: raisedButtonStyle,
onPressed: () { },
child: Text('Looks like a RaisedButton'),
)
El estilo de OutlineButton para OutlinedButton es un poco más
complicado porque el color del contorno cambia al color primario
cuando se presiona el botón. La apariencia del contorno está definida por un
BorderSide y utilizarás una MaterialStateProperty para definir el color
del contorno presionado:
final ButtonStyle outlineButtonStyle = OutlinedButton.styleFrom(
foregroundColor: Colors.black87,
minimumSize: Size(88, 36),
padding: EdgeInsets.symmetric(horizontal: 16),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(2)),
),
).copyWith(
side: MaterialStateProperty.resolveWith<BorderSide?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 1,
);
}
return null;
},
),
);
OutlinedButton(
style: outlineButtonStyle,
onPressed: () { },
child: Text('Looks like an OutlineButton'),
)
Para restaurar la apariencia predeterminada de los botones en toda una aplicación, puedes configurar los nuevos temas de botones en el tema de la aplicación:
MaterialApp(
theme: ThemeData.from(colorScheme: ColorScheme.light()).copyWith(
textButtonTheme: TextButtonThemeData(style: flatButtonStyle),
elevatedButtonTheme: ElevatedButtonThemeData(style: raisedButtonStyle),
outlinedButtonTheme: OutlinedButtonThemeData(style: outlineButtonStyle),
),
)
Para restaurar la apariencia predeterminada de los botones en parte de una
aplicación, puedes envolver un subárbol de Widgets con TextButtonTheme,
ElevatedButtonTheme o OutlinedButtonTheme. Por ejemplo:
TextButtonTheme(
data: TextButtonThemeData(style: flatButtonStyle),
child: myWidgetSubtree,
)
Migrar botones con colores personalizados
#Las siguientes secciones cubren el uso de los siguientes parámetros de color de FlatButton,
RaisedButton y OutlineButton:
textColor
disabledTextColor
color
disabledColor
focusColor
hoverColor
highlightColor*
splashColor
Las nuevas clases de botones no admiten un color de resaltado separado porque ya no forma parte de Material Design.
Migrar botones con colores de primer plano y fondo personalizados
#Dos personalizaciones comunes para las clases de botones originales son un color de
primer plano personalizado para FlatButton, o colores personalizados de primer plano y fondo
para RaisedButton. Producir el mismo resultado con las nuevas
clases de botones es simple:
FlatButton(
textColor: Colors.red, // foreground
onPressed: () { },
child: Text('FlatButton with custom foreground/background'),
)
TextButton(
style: TextButton.styleFrom(
foregroundColor Colors.red,
),
onPressed: () { },
child: Text('TextButton with custom foreground'),
)
En este caso, el color de primer plano (texto/icono) del TextButton, así como
sus colores de superposición al pasar el cursor/foco/presionar se basarán en
Colors.red. Por defecto, el color de relleno de fondo de TextButton es
transparente.
Migrar un RaisedButton con colores de primer plano y fondo personalizados:
RaisedButton(
color: Colors.red, // background
textColor: Colors.white, // foreground
onPressed: () { },
child: Text('RaisedButton with custom foreground/background'),
)
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
onPressed: () { },
child: Text('ElevatedButton with custom foreground/background'),
)
En este caso, el uso que hace el botón del color primario del esquema de colores se
invierte en relación con el TextButton: primary es el color de relleno de
fondo del botón y onPrimary es el color de primer plano (texto/icono).
Migrar botones con colores de superposición personalizados
#Invalidar los colores predeterminados de foco, paso del cursor, resaltado o splash
de un botón es menos común. Las clases FlatButton, RaisedButton y
OutlineButton
tienen parámetros individuales para estos colores que dependen del State.
En su lugar, las nuevas clases TextButton, ElevatedButton y OutlinedButton
utilizan un único parámetro MaterialStateProperty<Color>. Los nuevos
botones permiten especificar valores que dependen del State para todos los
colores, mientras que los botones originales solo admitían especificar lo que ahora
se denomina "overlayColor".
FlatButton(
focusColor: Colors.red,
hoverColor: Colors.green,
splashColor: Colors.blue,
onPressed: () { },
child: Text('FlatButton with custom overlay colors'),
)
TextButton(
style: ButtonStyle(
overlayColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.focused))
return Colors.red;
if (states.contains(MaterialState.hovered))
return Colors.green;
if (states.contains(MaterialState.pressed))
return Colors.blue;
return null; // Defer to the widget's default.
}),
),
onPressed: () { },
child: Text('TextButton with custom overlay colors'),
)
La nueva versión es más flexible aunque menos compacta. En la
versión original, la precedencia de los diferentes States es
implícita (y no está documentada) y fija; en la nueva versión, es
explícita. Para una aplicación que especificaba estos colores con frecuencia, la
ruta de migración más fácil sería definir uno o más ButtonStyles
que coincidan con el ejemplo anterior (y simplemente usar el parámetro style) o
definir un Widget contenedor Stateless que encapsule los tres parámetros de
color.
Migrar botones con colores inhabilitados personalizados
#Esta es una personalización relativamente rara. Las clases FlatButton,
RaisedButton y OutlineButton tienen parámetros disabledTextColor
y
disabledColor que definen los colores de fondo y primer plano
cuando la llamada de retorno onPressed del botón es nula.
Por defecto, todos los botones utilizan el color disabledForegroundColor
del esquema de colores, con una opacidad de 0.38 para el color de primer plano inhabilitado. Solo
ElevatedButton tiene un color de fondo no transparente y su valor
predeterminado es el color disabledForegroundColor con una opacidad de 0.12. Por lo tanto, en muchos casos,
uno simplemente puede usar el método styleFrom para invalidar los colores inhabilitados:
RaisedButton(
disabledColor: Colors.red.withOpacity(0.12),
disabledTextColor: Colors.red.withOpacity(0.38),
onPressed: null,
child: Text('RaisedButton with custom disabled colors'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(disabledForegroundColor: Colors.red),
onPressed: null,
child: Text('ElevatedButton with custom disabled colors'),
)
Para un control completo sobre los colores inhabilitados, uno debe definir el
estilo de ElevatedButton explícitamente, en términos de
MaterialStateProperties:
RaisedButton(
disabledColor: Colors.red,
disabledTextColor: Colors.blue,
onPressed: null,
child: Text('RaisedButton with custom disabled colors'),
)
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return Colors.red;
return null; // Defer to the widget's default.
}),
foregroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return Colors.blue;
return null; // Defer to the widget's default.
}),
),
onPressed: null,
child: Text('ElevatedButton with custom disabled colors'),
)
Al igual que en el caso anterior, existen formas obvias de hacer que la nueva versión sea más compacta en una aplicación donde esta migración ocurra a menudo.
Migrar botones con elevaciones personalizadas
#Esta también es una personalización relativamente rara. Normalmente, solo los
ElevatedButtons (originalmente llamados RaisedButtons)
incluyen cambios de elevación. Para elevaciones que son proporcionales
a una elevación de referencia (según la especificación de Material Design),
uno puede invalidar todas de forma muy sencilla.
Por defecto, la elevación de un botón inhabilitado es 0, y los States restantes se definen en relación con una línea de base de 2:
disabled: 0
hovered or focused: baseline + 2
pressed: baseline + 6
Por lo tanto, para migrar un RaisedButton para el cual se han
definido todas las elevaciones:
RaisedButton(
elevation: 2,
focusElevation: 4,
hoverElevation: 4,
highlightElevation: 8,
disabledElevation: 0,
onPressed: () { },
child: Text('RaisedButton with custom elevations'),
)
ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 2),
onPressed: () { },
child: Text('ElevatedButton with custom elevations'),
)
Para invalidar arbitrariamente solo una elevación, como la elevación presionada:
RaisedButton(
highlightElevation: 16,
onPressed: () { },
child: Text('RaisedButton with a custom elevation'),
)
ElevatedButton(
style: ButtonStyle(
elevation: MaterialStateProperty.resolveWith<double?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed))
return 16;
return null;
}),
),
onPressed: () { },
child: Text('ElevatedButton with a custom elevation'),
)
Migrar botones con formas y bordes personalizados
#Las clases originales FlatButton, RaisedButton y OutlineButton proporcionan
todas un parámetro shape que define tanto la forma del botón como
la apariencia de su contorno. Las nuevas clases correspondientes y sus
temas admiten especificar la forma del botón y su borde
por separado, con los parámetros OutlinedBorder shape y BorderSide side.
En este ejemplo, la versión original de OutlineButton especifica el mismo
color para el borde en su State resaltado (presionado) que para otros
States.
OutlineButton(
shape: StadiumBorder(),
highlightedBorderColor: Colors.red,
borderSide: BorderSide(
width: 2,
color: Colors.red
),
onPressed: () { },
child: Text('OutlineButton with custom shape and border'),
)
OutlinedButton(
style: OutlinedButton.styleFrom(
shape: StadiumBorder(),
side: BorderSide(
width: 2,
color: Colors.red
),
),
onPressed: () { },
child: Text('OutlinedButton with custom shape and border'),
)
La mayoría de los parámetros de estilo del Widget OutlinedButton, incluyendo
su forma y borde, se pueden especificar con valores de MaterialStateProperty,
lo que equivale a decir que pueden tener diferentes valores dependiendo
del State del botón. Para especificar un color de borde diferente cuando el
botón está presionado, haz lo siguiente:
OutlineButton(
shape: StadiumBorder(),
highlightedBorderColor: Colors.blue,
borderSide: BorderSide(
width: 2,
color: Colors.red
),
onPressed: () { },
child: Text('OutlineButton with custom shape and border'),
)
OutlinedButton(
style: ButtonStyle(
shape: MaterialStateProperty.all<OutlinedBorder>(StadiumBorder()),
side: MaterialStateProperty.resolveWith<BorderSide>(
(Set<MaterialState> states) {
final Color color = states.contains(MaterialState.pressed)
? Colors.blue
: Colors.red;
return BorderSide(color: color, width: 2);
}
),
),
onPressed: () { },
child: Text('OutlinedButton with custom shape and border'),
)
Timeline
#Llegó en la versión: 1.20.0-0.0.pre
En la versión estable: 2.0.0
Referencias
#Documentación de la API:
-
ButtonStyle -
ButtonStyleButton -
ElevatedButton -
ElevatedButtonTheme -
ElevatedButtonThemeData -
OutlinedButton -
OutlinedButtonTheme -
OutlinedButtonThemeData TextButton-
TextButtonTheme -
TextButtonThemeData
PRs relevantes:
A menos que se indique lo contrario, la documentación de este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 2026-05-11. Ver código fuente oreportar un problema.