Flutter para desarrolladores de Jetpack Compose
Aprende cómo aplicar los conocimientos de desarrollador de Jetpack Compose al compilar aplicaciones Flutter.
Flutter es un framework para compilar aplicaciones multiplataforma que utiliza el lenguaje de programación Dart.
Tus conocimientos y experiencia en Jetpack Compose son sumamente valiosos al compilar con Flutter.
Este documento se puede utilizar como referencia para saltar de un lado a otro y encontrar las preguntas que sean más relevantes para tus necesidades. Esta guía incorpora código de ejemplo. Al usar el botón "Open in DartPad" que aparece al pasar el cursor o enfocar, puedes abrir y ejecutar algunos de los ejemplos en DartPad.
Resumen
#El código de Flutter y Jetpack Compose describe cómo se ve y funciona la UI. Los desarrolladores llaman a este tipo de código un framework declarativo.
Si bien existen diferencias clave, especialmente cuando se trata de interactuar con código heredado de Android, existen muchas similitudes entre ambos frameworks.
Composables vs. Widgets
#Jetpack Compose representa componentes de UI como funciones composables, denominadas más adelante en este documento como composables. Los composables se pueden modificar o decorar mediante el uso de objetos Modifier.
Text("Hello, World!",
modifier: Modifier.padding(10.dp)
)
Text("Hello, World!",
modifier = Modifier.padding(10.dp))
Flutter representa los componentes de UI como widgets.
Tanto los composables como los widgets solo existen hasta que necesitan cambiar.
Estos lenguajes llaman a esta propiedad inmutabilidad.
Jetpack Compose modifica las propiedades de los componentes de la UI utilizando una
propiedad opcional modifier respaldada por un objeto Modifier.
Por el contrario, los widgets de Flutter configuran sus propiedades directamente
a través de los parámetros del constructor.
Padding( // <-- This is a Widget
padding: EdgeInsets.all(10.0), // <-- a parameter to Padding
child: Text("Hello, World!"), // <-- This is also a Widget
);
Para componer diseños, tanto Jetpack Compose como Flutter anidan componentes de UI
entre sí.
Jetpack Compose anida Composables mientras que Flutter anida Widgets.
Proceso de diseño
#Jetpack Compose y Flutter manejan el diseño de manera similar. Ambos distribuyen la UI en una sola pasada y los elementos padres proporcionan restricciones de diseño a sus hijos. Específicamente:
- El padre se mide a sí mismo y a sus hijos de forma recursiva, proporcionando cualquier restricción del padre al hijo.
- Los hijos intentan dimensionarse a sí mismos utilizando los métodos anteriores y proporcionan a sus propios hijos tanto sus restricciones como las que puedan aplicarse de sus nodos ancestros.
- Al encontrar un nodo hoja (un nodo sin hijos), el tamaño y las propiedades se determinan en función de las restricciones proporcionadas y el elemento se coloca en la UI.
- Una vez que todos los hijos están dimensionados y colocados, los nodos raíz pueden determinar su medida, tamaño y ubicación.
Tanto en Jetpack Compose como en Flutter, el componente padre puede sobrescribir o restringir el tamaño deseado del hijo. El widget no puede tener el tamaño que quiera. Tampoco puede usualmente conocer o decidir su posición en la pantalla, ya que su padre toma esa decisión.
Para forzar a un widget hijo a renderizarse a un tamaño específico, el padre debe establecer restricciones estrictas (tight constraints). Una restricción se vuelve estricta cuando el valor de tamaño mínimo de su restricción es igual a su valor de tamaño máximo.
Para aprender cómo funcionan las restricciones en Flutter, visita Entendiendo las restricciones (Understanding constraints).
Sistema de diseño
#Debido a que Flutter apunta a múltiples plataformas, tu aplicación no necesita ajustarse a ningún sistema de diseño. Aunque esta guía presenta widgets de Material, tu aplicación Flutter puede usar muchos sistemas de diseño diferentes:
- Widgets de Material personalizados
- Widgets creados por la comunidad
- Tus propios widgets personalizados
Si estás buscando una excelente aplicación de referencia que cuente con un sistema de diseño personalizado, consulta Wonderous.
Conceptos básicos de UI
#Esta sección cubre los conceptos básicos del desarrollo de UI en Flutter y cómo se compara con Jetpack Compose. Esto incluye cómo comenzar a desarrollar tu aplicación, mostrar texto estático, crear botones, reaccionar a eventos de presión, mostrar listas, cuadrículas y más.
Primeros pasos
#Para las aplicaciones de Compose, tu punto de entrada principal será be Activity o uno de sus descendientes, generalmente ComponentActivity.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
SampleTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
Para iniciar tu aplicación de Flutter, pasa una instancia de tu aplicación a
la función runApp.
void main() {
runApp(const MyApp());
}
App es un widget. Su método build describe la parte de la
interfaz de usuario que representa.
Es común comenzar tu aplicación con una clase WidgetApp,
como MaterialApp.
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: HomePage(),
);
}
}
El widget utilizado en la HomePage podría comenzar con la clase Scaffold.
Scaffold implementa una estructura de diseño básica para una aplicación.
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text(
'Hello, World!',
),
),
);
}
}
Nota cómo Flutter utiliza el widget Center.
Compose tiene una serie de valores predeterminados heredados de las Views de Android ancestrales. A menos que se especifique lo contrario, la mayoría de los componentes ajustan ("wrap") su tamaño al contenido, lo que significa que solo ocupan el espacio necesario cuando se renderizan. Ese no es siempre el caso con Flutter.
Para centrar el texto, envúelo en un widget Center.
Para obtener información sobre diferentes widgets y sus comportamientos predeterminados, consulta
el Catálogo de widgets (Widget catalog).
Agregar botones
#En Compose, utilizas el composable Button o una de sus variantes
para crear un botón. Button es un alias para FilledTonalButton
cuando se utiliza un tema de Material.
Button(onClick = {}) {
Text("Do something")
}
Para lograr el mismo resultado en Flutter,
utiliza la clase FilledButton:
FilledButton(
onPressed: () {
// This closure is called when your button is tapped.
},
const Text('Do something'),
),
Flutter te da acceso a una variedad de botones con estilos predefinidos.
Alinear componentes horizontal o verticalmente
#Jetpack Compose y Flutter manejan colecciones horizontales y verticales de elementos de manera similar.
El siguiente fragmento de código de Compose agrega una imagen del globo terráqueo y
texto en contenedores tanto Row y Column con el centrado de los elementos:
Row(horizontalArrangement = Arrangement.Center) {
Image(Icons.Default.Public, contentDescription = "")
Text("Hello, world!")
}
Column(verticalArrangement = Arrangement.Center) {
Image(Icons.Default.Public, contentDescription = "")
Text("Hello, world!")
}
Flutter también utiliza Row
y Column, pero hay algunas pequeñas diferencias al especificar los
widgets hijos y la alineación. El siguiente ejemplo es equivalente al de Compose.
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.public),
Text('Hello, world!'),
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(MaterialIcons.globe),
Text('Hello, world!'),
],
)
Row y Column requieren un List<Widget> en el parámetro children.
La propiedad mainAxisAlignment le indica a Flutter cómo colocar a los hijos
si hay espacio adicional. MainAxisAlignment.center coloca a los hijos en el
centro del eje principal. Para Row, el eje principal es el eje horizontal;
por el contrario, para Column, el eje principal es el eje vertical.
::: note
Mientras que Row y Column de Flutter tienen MainAxisAlignment
y CrossAxisAlignment para controlar cómo se colocan los elementos, las propiedades que
controlan la ubicación en Jetpack Compose son una propiedad vertical y otra horizontal
de las siguientes: verticalArrangement, verticalAlignment,
horizontalAlignment y horizontalArrangement. El truco para determinar
cuál es el MainAxis es buscar la propiedad que termina en arrangement.
El CrossAxis será la propiedad que termine en alignment.
:::
Mostrar una vista de lista
#En Compose, tienes un par de formas de crear una lista basada en
el tamaño de la lista que necesitas mostrar. Para una cantidad pequeña de elementos
que se pueden mostrar todos a la vez, puedes iterar sobre una colección
dentro de una Column o Row.
Para una lista con una gran cantidad de elementos, LazyList tiene mejor
rendimiento. Solo distribuye los componentes que serán visibles
en lugar de todos ellos.
data class Person(val name: String)
val people = arrayOf(
Person(name = "Person 1"),
Person(name = "Person 2"),
Person(name = "Person 3")
)
@Composable
fun ListDemo(people: List<Person>) {
Column {
people.forEach {
Text(it.name)
}
}
}
@Composable
fun ListDemo2(people: List<Person>) {
LazyColumn {
items(people) { person ->
Text(person.name)
}
}
}
Para construir una lista de forma diferida en Flutter, ....
class Person {
String name;
Person(this.name);
}
var items = [
Person('Person 1'),
Person('Person 2'),
Person('Person 3'),
];
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index].name),
);
},
),
);
}
}
Flutter tiene algunas convenciones para las listas:
-
El widget
ListViewtiene un método builder. Esto funciona como la clausuraitemdentro de unaLazyListde Compose. -
El parámetro
itemCountdelListViewestablece cuántos elementos muestra elListView. -
El
itemBuildertiene un parámetro index que estará entre cero y uno menos que el itemCount.
El ejemplo anterior devolvió un widget ListTile
para cada elemento.
El widget ListTile incluye propiedades como height y
font-size.
Estas propiedades ayudan a construir una lista. Sin embargo, Flutter te permite devolver
casi cualquier widget que represente tus datos.
Mostrar una cuadrícula
#Construir una cuadrícula en Compose es similar a una
LazyList (LazyColumn o LazyRow). Puedes usar la
misma clausura items. Hay propiedades en cada
tipo de cuadrícula para especificar cómo organizar los elementos,
si se debe usar o no un diseño adaptativo o fijo,
entre otras.
val widgets = arrayOf(
"Row 1",
Icons.Filled.ArrowDownward,
Icons.Filled.ArrowUpward,
"Row 2",
Icons.Filled.ArrowDownward,
Icons.Filled.ArrowUpward
)
LazyVerticalGrid (
columns = GridCells.Fixed(3),
contentPadding = PaddingValues(8.dp)
) {
items(widgets) { i ->
if (i is String) {
Text(i)
} else {
Image(i as ImageVector, "")
}
}
}
Para mostrar cuadrículas en Flutter, utiliza el widget GridView
.
Este widget tiene varios constructores. Cada constructor tiene
un objetivo similar, pero utiliza diferentes parámetros de entrada.
El siguiente ejemplo utiliza el inicializador .builder():
const widgets = [
Text('Row 1'),
Icon(Icons.arrow_downward),
Icon(Icons.arrow_upward),
Text('Row 2'),
Icon(Icons.arrow_downward),
Icon(Icons.arrow_upward),
];
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisExtent: 40,
),
itemCount: widgets.length,
itemBuilder: (context, index) => widgets[index],
),
);
}
}
El delegado SliverGridDelegateWithFixedCrossAxisCount determina
varios parámetros que la cuadrícula utiliza para distribuir sus componentes.
Esto incluye el crossAxisCount que dicta la cantidad de elementos
mostrados en cada fila.
Los componentes LazyHorizontalGrid, LazyVerticalGrid de Jetpack Compose y el widget GridView de Flutter
son algo
similares. GridView utiliza un delegado para decidir cómo la cuadrícula
debe distribuir sus componentes. Las propiedades rows, columns y otras
asociadas en LazyHorizontalGrid / LazyVerticalGrid
sirven para el mismo propósito.
Crear una vista de desplazamiento
#LazyColumn y LazyRow en Jetpack Compose tienen soporte
incorporado para desplazamiento.
Para crear una vista con desplazamiento, Flutter utiliza SingleChildScrollView.
En el siguiente ejemplo, la función mockPerson simula instancias
de la clase Person para crear el widget PersonView personalizado.
SingleChildScrollView(
child: Column(
children: mockPersons
.map(
(person) => PersonView(
person: person,
),
)
.toList(),
),
),
Diseño responsivo y adaptativo
#El diseño adaptativo (Adaptive Design) en Compose es un tema complejo con muchas soluciones viables:
- Usar un diseño personalizado
- Usar únicamente
WindowSizeClass - Usar
BoxWithConstraintspara controlar lo que se muestra en función del espacio disponible - Usar la biblioteca adaptativa de Material 3 que utiliza
WindowSizeClassjunto con diseños composables especializados para diseños comunes
Por esa razón, se te anima a investigar directamente las opciones de Flutter y ver qué se adapta a tus requisitos en lugar de intentar encontrar algo que sea una traducción uno a uno.
Para crear vistas relativas en Flutter, puedes usar una de estas dos opciones:
- Obtener el objeto
BoxConstraintsen la claseLayoutBuilder. - Usar
MediaQuery.of()en tus funciones de construcción para obtener el tamaño y la orientación de tu aplicación actual.
Para obtener más información, consulta Creación de aplicaciones responsivas y adaptativas (Creating responsive and adaptive apps).
Gestionar el state
#Compose almacena el state con la API remember y los descendientes
de la interfaz MutableState.
Scaffold(
content = { padding ->
var _counter = remember { mutableIntStateOf(0) }
Column(horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize().padding(padding)) {
Text(_counter.value.toString())
Spacer(modifier = Modifier.height(16.dp))
FilledIconButton (onClick = { -> _counter.intValue += 1 }) {
Text("+")
}
}
}
)
Flutter gestiona el state local utilizando un StatefulWidget.
Implementa un widget stateful con las siguientes dos clases:
- una subclase de
StatefulWidget - una subclase de
State
El objeto State almacena el state del widget.
Para cambiar el state de un widget, llama a setState() desde la subclase State
para indicarle al framework que vuelva a dibujar el widget.
El siguiente ejemplo muestra parte de una aplicación de contador:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$_counter'),
TextButton(
onPressed: () => setState(() {
_counter++;
}),
child: const Text('+'),
),
],
),
),
);
}
}
Para conocer más formas de gestionar el state, consulta Gestión del state (State management).
Dibujar en la pantalla
#En Compose, utilizas el composable Canvas para dibujar
formas, imágenes y texto en la pantalla.
Flutter tiene una API basada en la clase Canvas,
con dos clases que te ayudan a dibujar:
-
CustomPaintque requiere un pintor (painter):dartCustomPaint( painter: SignaturePainter(_points), size: Size.infinite, ), -
CustomPainterque implementa tu algoritmo para dibujar en el canvas.dartclass SignaturePainter extends CustomPainter { SignaturePainter(this.points); final List<Offset?> points; @override void paint(Canvas canvas, Size size) { final Paint paint = Paint() ..color = Colors.black ..strokeCap = StrokeCap.round ..strokeWidth = 5; for (int i = 0; i < points.length - 1; i++) { if (points[i] != null && points[i + 1] != null) { canvas.drawLine(points[i]!, points[i + 1]!, paint); } } } @override bool shouldRepaint(SignaturePainter oldDelegate) => oldDelegate.points != points; }
Temas, estilos y multimedia
#Puedes estilizar las aplicaciones de Flutter con poco esfuerzo. El estilo incluye cambiar entre temas claros y oscuros, modificar el diseño de tu texto y componentes de UI, y más. Esta sección cubre cómo dar estilo a tus aplicaciones.
Usar el modo oscuro
#En Compose, puedes controlar el modo claro y oscuro en cualquier
nivel arbitrario envolviendo un componente con
un composable Theme.
En Flutter, puedes controlar el modo claro y oscuro a nivel de aplicación.
Para controlar el modo de brillo (brightness), utiliza la propiedad theme
de la clase App:
const MaterialApp(
theme: ThemeData(
brightness: Brightness.dark,
),
home: HomePage(),
);
Estilizar texto
#En Compose, utilizas las propiedades en Text para uno o dos
atributos o construyes un objeto TextStyle para establecer varios a la vez.
Text("Hello, world!", color = Color.Green,
fontWeight = FontWeight.Bold, fontSize = 30.sp)
Text("Hello, world!",
style = TextStyle(
color = Color.Green,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
),
)
Para estilizar texto en Flutter, agrega un widget TextStyle como el valor
del parámetro style del widget Text.
Text(
'Hello, world!',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
Estilizar botones
#En Compose, modificas los colores de un botón usando
la propiedad colors. Si se dejan sin modificar,
utilizan los valores predeterminados del tema actual.
Button(onClick = {},
colors = ButtonDefaults.buttonColors().copy(
containerColor = Color.Yellow, contentColor = Color.Blue,
)) {
Text("Do something", fontSize = 30.sp, fontWeight = FontWeight.Bold)
}
Para estilizar widgets de botón en Flutter, de manera similar estableces el estilo de su hijo o modificas las propiedades en el propio botón.
FilledButton(
onPressed: (){},
style: FilledButton.styleFrom(backgroundColor: Colors.amberAccent),
child: const Text(
'Do something',
style: TextStyle(
color: Colors.blue,
fontSize: 30,
fontWeight: FontWeight.bold,
)
)
)
Empaquetar assets para usar en Flutter
#Comúnmente existe la necesidad de empaquetar recursos para usarlos en tu aplicación. Pueden ser animaciones, gráficos vectoriales, imágenes, fuentes u otros archivos generales.
A diferencia de las aplicaciones nativas de Android que esperan una estructura de directorio fija bajo /res/<calificador>/
donde el calificador podría indicar el tipo de archivo, una orientación específica
o la versión de Android, Flutter no requiere una ubicación específica siempre
que los archivos referenciados estén listados en el archivo pubspec.yaml. A continuación se muestra un extracto
de un pubspec.yaml que hace referencia a varias imágenes y un archivo de fuente.
flutter:
assets:
- assets/my_icon.png
- assets/background.png
fonts:
- family: FiraSans
fonts:
- asset: fonts/FiraSans-Regular.ttf
Usar fuentes
#En Compose, tienes dos opciones para usar fuentes en tu aplicación. Puedes usar un servicio en tiempo de ejecución para recuperarlas Google Fonts. Alternativamente, pueden estar empaquetadas en archivos de recursos.
Flutter tiene métodos similares para usar fuentes, analicemos ambos en línea.
Usar fuentes empaquetadas
#Los siguientes son códigos aproximadamente equivalentes de Compose y Flutter para usar un archivo de fuente en el directorio /res/
o fonts
como se indicó anteriormente.
// Font files bundled with app
val firaSansFamily = FontFamily(
Font(R.font.firasans_regular, FontWeight.Normal),
// ...
)
// Usage
Text(text = "Compose", fontFamily = firaSansFamily, fontWeight = FontWeight.Normal)
Text(
'Flutter',
style: TextStyle(
fontSize: 40,
fontFamily: 'FiraSans',
),
),
Usar un proveedor de fuentes (Google Fonts)
#Un punto de diferencia es el uso de fuentes de un proveedor de fuentes como Google Fonts. En Compose, la instanciación se realiza en línea con el mismo código aproximado para hacer referencia a un archivo local.
Después de instanciar un proveedor que hace referencia a las cadenas de texto especiales para el servicio de fuentes,
utilizarías la misma declaración FontFamily.
// Font files bundled with app
val provider = GoogleFont.Provider(
providerAuthority = "com.google.android.gms.fonts",
providerPackage = "com.google.android.gms",
certificates = R.array.com_google_android_gms_fonts_certs
)
val firaSansFamily = FontFamily(
Font(
googleFont = GoogleFont("FiraSans"),
fontProvider = provider,
)
)
// Usage
Text(text = "Compose", fontFamily = firaSansFamily, fontWeight = FontWeight.Light)
Para Flutter, esto lo proporciona el plugin google_fonts utilizando el nombre de la fuente.
import 'package:google_fonts/google_fonts.dart';
//...
Text(
'Flutter',
style: GoogleFonts.firaSans(),
// or
//style: GoogleFonts.getFont('FiraSans')
),
Usar imágenes
#En Compose, normalmente colocas los archivos de imagen en el directorio drawable
en los recursos /res/drawable y utilizas el composable Image para mostrar
las imágenes. Los assets se referencian utilizando el localizador de recursos
con el estilo de R.drawable.<nombre de archivo> sin la extensión del archivo.
En Flutter, la ubicación del recurso está listada en pubspec.yaml, como se muestra en el siguiente fragmento de código.
flutter:
assets:
- images/Blueberries.jpg
Después de agregar tu imagen, puedes mostrarla utilizando el constructor
.asset() del widget Image. Este constructor:
Para revisar un ejemplo completo, consulta la documentación de Image.
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-05. Ver código fuente oreportar un problema.