Crear widgets
Aprende sobre los widgets stateless y cómo construir los tuyos propios.
Aprende a crear widgets personalizados y a usar los widgets más comunes de la SDK como Container, Center y Text.
Qué lograrás
Pasos
1
Antes de comenzar
Antes de comenzar
Esta aplicación se basa en un poco de lógica de juego que no está relacionada con la UI y, por lo tanto, queda fuera del alcance de este tutorial. Antes de continuar, debes agregar esta lógica a tu aplicación.
-
Descarga el siguiente archivo Dart y guárdalo como
lib/game.darten el directorio de tu proyecto.game.dartdart/// Game logic and supporting types for Birdle, /// a five-letter word-guessing game similar to Wordle. /// /// Defines the [Game] state machine and the /// [Word], [Letter], and [HitType] data model used to /// represent guesses and their evaluation against a hidden word. library; import 'dart:collection'; import 'dart:math'; /// The result of evaluating a [Letter] of a guess against the hidden word. enum HitType { /// The letter hasn't yet been evaluated. none, /// The letter matches the hidden word's letter at the same position. hit, /// The letter is in the hidden word, but at a different position. partial, /// The letter doesn't appear in the hidden word. miss, } /// A single character paired with its [HitType] against the hidden word. typedef Letter = ({String char, HitType type}); /// Every word that can be legally entered as a guess. const List<String> allLegalGuesses = [...legalWords, ...legalGuesses]; /// Words that can be chosen as the hidden word. const List<String> legalWords = ['aback', 'abase', 'abate', 'abbey', 'abbot']; /// Additional words accepted as guesses beyond those in [legalWords]. const List<String> legalGuesses = [ 'aback', 'abase', 'abate', 'abbey', 'abbot', 'abhor', 'abide', 'abled', 'abode', 'abort', ]; /// Game state of a single round of Birdle, /// a five-letter word-guessing game similar to Wordle. /// /// Exposes the state and methods a UI needs to /// evaluate guesses and track progress, /// but doesn't advance play on its own. /// /// Clients drive each round by calling [guess] to submit an attempt and /// [resetGame] to start over. class Game { /// The default maximum number of guesses allowed in a [Game]. static const int defaultMaxGuesses = 5; /// Creates a new game with [maxGuesses] guesses allowed. /// /// If [seed] is provided, the hidden word is /// chosen deterministically from [legalWords], /// otherwise it is selected at random. Game({this.maxGuesses = defaultMaxGuesses, this.seed}) : _wordToGuess = _generateInitialWord(seed), _guesses = List<Word>.filled(maxGuesses, Word.empty()); /// The maximum number of guesses allowed in this game. final int maxGuesses; /// The seed used to choose the hidden word, /// or `null` if it was selected at random. final int? seed; /// The current hidden word, exposed publicly through [hiddenWord]. Word _wordToGuess; /// Backing storage for [guesses]. /// /// Holds every guess slot in order, /// with unfilled slots represented by empty [Word]s. List<Word> _guesses; /// The word the player is trying to guess. Word get hiddenWord => _wordToGuess; /// An unmodifiable view of every guess slot, including those still empty. UnmodifiableListView<Word> get guesses => UnmodifiableListView(_guesses); /// The most recently submitted guess, /// or an empty [Word] if no guesses have been made. Word get previousGuess { final index = _guesses.lastIndexWhere((word) => word.isNotEmpty); return index == -1 ? Word.empty() : _guesses[index]; } /// The index of the next empty guess slot, or `-1` if every slot is full. int get activeIndex => _guesses.indexWhere((word) => word.isEmpty); /// The number of guesses still available to the player. int get guessesRemaining { if (activeIndex == -1) return 0; return maxGuesses - activeIndex; } /// Whether the most recent guess matches the hidden word. bool get didWin { if (_guesses.first.isEmpty) return false; for (final letter in previousGuess) { if (letter.type != HitType.hit) return false; } return true; } /// Whether all allowed guesses have been used without winning. bool get didLose => guessesRemaining == 0 && !didWin; /// Picks a new hidden word and clears every submitted guess. void resetGame() { _wordToGuess = _generateInitialWord(seed); _guesses = List<Word>.filled(maxGuesses, Word.empty()); } /// Evaluates [guess] against the hidden word, /// records the result in [guesses], and returns it. /// /// For finer control, use [isLegalGuess] to validate input or /// [matchGuessOnly] to evaluate without recording the result. Word guess(String guess) { final result = matchGuessOnly(guess); addGuessToList(result); return result; } /// Whether [guess] is a legal word to guess. /// /// UIs can call this method before [guess] to /// show players a message when they enter an invalid word. bool isLegalGuess(String guess) => Word.fromString(guess).isLegalGuess; /// Evaluates [guess] against the hidden word without advancing the game. Word matchGuessOnly(String guess) => Word.fromString(guess).evaluateGuess(_wordToGuess); /// Stores [guess] in the next empty slot of [guesses]. void addGuessToList(Word guess) { final guessIndex = activeIndex; if (guessIndex == -1) { throw StateError('No guesses remaining.'); } _guesses[guessIndex] = guess; } /// Returns the starting hidden word for a new round. /// /// Picks a deterministic word from [legalWords] when [seed] is provided, /// or one at random otherwise. static Word _generateInitialWord(int? seed) => seed == null ? Word.random() : Word.fromSeed(seed); } /// A five-letter word made up of [Letter]s, each tracking its [HitType]. class Word with IterableMixin<Letter> { /// Creates a word backed by the specified list of [Letter]s. Word(this._letters); /// Creates a word with five blank letters of [HitType.none]. factory Word.empty() => Word(List<Letter>.filled(5, (char: '', type: HitType.none))); /// Creates a [Word] from [guess]. /// /// Each character is lowercased, /// every [Letter] starts as [HitType.none]. factory Word.fromString(String guess) { if (guess.length != 5) { throw ArgumentError.value( guess, 'guess', 'Must be exactly 5 characters long.', ); } final letters = guess .toLowerCase() .split('') .map((char) => (char: char, type: HitType.none)) .toList(); return Word(letters); } /// Creates a word chosen at random from [legalWords]. factory Word.random() { final random = Random(); final nextWord = legalWords[random.nextInt(legalWords.length)]; return Word.fromString(nextWord); } /// Creates a word chosen from [legalWords] using [seed] as an index. factory Word.fromSeed(int seed) => Word.fromString(legalWords[seed % legalWords.length]); /// An unmodifiable list of [Letter]s that make up this word. final List<Letter> _letters; @override Iterator<Letter> get iterator => _letters.iterator; /// Whether every [Letter] in this word has no character. @override bool get isEmpty => every((letter) => letter.char.isEmpty); @override int get length => _letters.length; /// The [Letter] at index [i] in word. Letter operator [](int i) => _letters[i]; @override String toString() => _letters.map((letter) => letter.char).join().trim(); /// Returns a multi-line string showing each [Letter] alongside its [HitType]. /// /// Used to play the game from the command line. String toStringVerbose() => _letters .map((letter) => '${letter.char} - ${letter.type.name}') .join('\n'); } /// Validation and guess-evaluation logic on [Word]. extension WordUtils on Word { /// Whether this word appears in [allLegalGuesses]. bool get isLegalGuess => allLegalGuesses.contains(toString()); /// Compares this [Word] against the specified [hiddenWord] /// and returns a new [Word] with the same letters, /// but where each [Letter] has new a [HitType] of /// [HitType.hit], [HitType.partial], or [HitType.miss]. Word evaluateGuess(Word hiddenWord) { assert(isLegalGuess); final result = List<Letter>.filled(length, (char: '', type: HitType.none)); // Counts hidden-word letters that can still be claimed as partial matches. final unmatchedHiddenLetterCounts = <String, int>{}; // Reserve exact matches before scoring partial matches. for (var i = 0; i < length; i++) { final guessChar = this[i].char; final hiddenChar = hiddenWord[i].char; if (guessChar == hiddenChar) { result[i] = (char: guessChar, type: HitType.hit); } else { // Track non-hit hidden letters for the partial-match pass. final unmatchedCount = unmatchedHiddenLetterCounts[hiddenChar] ?? 0; unmatchedHiddenLetterCounts[hiddenChar] = unmatchedCount + 1; } } // Spend each remaining hidden letter only once for partial matches. for (var i = 0; i < length; i++) { if (result[i].type == HitType.hit) continue; final guessChar = this[i].char; final unmatchedCount = unmatchedHiddenLetterCounts[guessChar] ?? 0; final isPartial = unmatchedCount > 0; if (isPartial) { // Use one available hidden letter for this partial match. unmatchedHiddenLetterCounts[guessChar] = unmatchedCount - 1; } result[i] = ( char: guessChar, type: isPartial ? HitType.partial : HitType.miss, ); } return Word(result); } } -
Para permitir el acceso a los tipos definidos en la biblioteca
game.dart, agrega una importación a ella desde tu archivolib/main.dart:main.dartdartimport 'package:flutter/material.dart'; import 'game.dart';
2
Anatomía de un widget stateless
Anatomía de un widget stateless
Un Widget es una clase Dart que extiende una de las clases de widgets de Flutter,
en este caso StatelessWidget.
Abre tu archivo main.dart y agrega este código debajo de la clase MainApp,
el cual define un nuevo widget llamado Tile.
class Tile extends StatelessWidget {
const Tile(this.letter, this.hitType, {super.key});
final String letter;
final HitType hitType;
@override
Widget build(BuildContext context) {
return Container();
}
}
Constructor
#La clase Tile tiene un constructor que define
qué datos deben pasarse al widget para renderizarlo.
En este caso, el constructor acepta dos parámetros:
- Un
Stringque representa la letra intentada de la casilla. - Un
valor de enumHitType que representa el resultado del intento y se usa para determinar el color de la casilla. Por ejemplo,HitType.hitda como resultado una casilla verde.
Pasar datos a los constructores de los widgets es fundamental para hacer que los widgets sean reutilizables.
Método build
#Finalmente, está el importantísimo método build, que debe definirse en
cada widget y siempre devolverá otro widget.
class Tile extends StatelessWidget {
const Tile(this.letter, this.hitType, {super.key});
final String letter;
final HitType hitType;
@override
Widget build(BuildContext context) {
// TODO: Replace Container with widgets.
return Container();
}
}
3
Usar el widget personalizado
Usar el widget personalizado
Cuando la aplicación esté terminada,
habrá 25 instancias de este widget en la pantalla.
Por ahora, sin embargo, muestra solo uno para que puedas ver las actualizaciones a medida que se realizan.
En el método MainApp.build, reemplaza el widget Text con el siguiente:
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Tile('A', HitType.hit), // NEW
),
),
);
}
}
En este momento, tu aplicación estará en blanco,
porque el widget Tile devuelve un Container vacío,
el cual no muestra nada por defecto.
4
El widget Container
El widget Container
El widget Tile consta de tres de los widgets principales más comunes:
Container, Center, y Text.
Container
es un widget de conveniencia que envuelve varios widgets de estilo principales,
como Padding,
ColoredBox,
SizedBox, y
DecoratedBox.
Dado que la UI terminada contiene 25 widgets Tile en columnas y filas ordenadas,
debería tener un tamaño explícito.
Configura las propiedades de ancho y alto en el Container.
(También podrías hacer esto con un widget SizedBox, pero a continuación usaremos
más propiedades del Container).
class Tile extends StatelessWidget {
const Tile(this.letter, this.hitType, {super.key});
final String letter;
final HitType hitType;
@override
Widget build(BuildContext context) {
// NEW
return Container(
width: 60,
height: 60,
// TODO: Add needed widgets
);
}
}
5
BoxDecoration
BoxDecoration
A continuación, agrega un Border
a la caja con el siguiente código:
class Tile extends StatelessWidget {
const Tile(this.letter, this.hitType, {super.key});
final String letter;
final HitType hitType;
@override
Widget build(BuildContext context) {
// NEW
return Container(
width: 60,
height: 60,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
// TODO: add background color
),
);
}
}
BoxDecoration es un objeto que sabe cómo
agregar cualquier cantidad de decoraciones a un widget, desde
el color de fondo hasta bordes, sombras de caja y más.
En este caso, has agregado un borde.
Cuando realices un hot reload, debería haber
un borde de color claro alrededor del cuadrado blanco.
Cuando este juego esté terminado, el color de la casilla dependerá del intento del usuario. La casilla será verde cuando el usuario haya acertado, amarilla cuando la letra sea correcta pero la posición sea incorrecta, y gris si el intento es incorrecto en ambos aspectos.
La siguiente figura muestra las tres posibilidades.
Para lograr esto en la UI, usa una switch expression
para
establecer el color del BoxDecoration.
class Tile extends StatelessWidget {
const Tile(this.letter, this.hitType, {super.key});
final String letter;
final HitType hitType;
@override
Widget build(BuildContext context) {
return Container(
width: 60,
height: 60,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
color: switch (hitType) {
HitType.hit => Colors.green,
HitType.partial => Colors.yellow,
HitType.miss => Colors.grey,
_ => Colors.white,
},
// TODO: add children
),
);
}
}
6
Widgets hijos
Widgets hijos
Finalmente, agrega los widgets Center y Text a la propiedad Container.child.
La mayoría de los widgets en el SDK de Flutter tienen una propiedad child o children que está
destinada a recibir un widget o una lista de widgets, respectivamente.
Es una buena práctica usar la misma convención de nomenclatura en
tus propios widgets personalizados.
class Tile extends StatelessWidget {
const Tile(this.letter, this.hitType, {super.key});
final String letter;
final HitType hitType;
@override
Widget build(BuildContext context) {
return Container(
width: 60,
height: 60,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
color: switch (hitType) {
HitType.hit => Colors.green,
HitType.partial => Colors.yellow,
HitType.miss => Colors.grey,
_ => Colors.white,
},
),
child: Center(
child: Text(
letter.toUpperCase(),
style: Theme.of(context).textTheme.titleLarge,
),
),
);
}
}
Haz un hot reload y aparecerá una caja verde. Para alternar el color,
actualiza y haz hot reload del HitType pasado al Tile
que creaste:
// main.dart line ~16
// green
Tile('A', HitType.hit);
// grey
Tile('A', HitType.miss);
// yellow
Tile('A', HitType.partial);
Pronto, esta pequeña caja será uno de los muchos widgets en la pantalla. En la próxima lección, comenzarás a construir la cuadrícula del juego en sí.
7
Revisión
Revisión
Qué lograste
Aquí tienes un resumen de lo que construiste y aprendiste en esta lección.Construiste un StatelessWidget personalizado
Creaste un nuevo widget Tile extendiendo StatelessWidget. Cada widget tiene un constructor para aceptar datos y un
build método que devuelve otros widgets. Este patrón es fundamental para construir interfaces de usuario con Flutter.
Hiciste que los widgets sean reutilizables con parámetros de constructor
Al aceptar letter y hitType como parámetros del constructor, tu widget Tile
puede mostrar contenido y colores diferentes. Pasar datos a través de constructores es cómo puedes crear componentes flexibles y reutilizables.
Aplicaste estilos a los widgets usando Container y BoxDecoration
Usaste Container para establecer el tamaño del widget y BoxDecoration para agregar bordes y colores de fondo. Luego, para aplicar estilos condicionales al color de la casilla, usaste una switch expression sobre el valor
hitType.
8
Ponte a prueba
Ponte a prueba
Cuestionario sobre Fundamentos de Widgets
1 / 2build method return?Otro widget.
¡Así es!
El método
buildsiempre devuelve otro widget, el cual forma parte del árbol de widgets.Un booleano que indica éxito o fallo.
No exactamente.
Los widgets no indican éxito; devuelven otros widgets para ser renderizados.
Null si no hay nada que mostrar.
No exactamente.
El método
buildno puede devolver null; debe devolver un widget válido.Un String que describe el widget.
No exactamente.
El método
builddevuelve un widget, no un String.
EdgeInsets
No exactamente.
EdgeInsets sirve para especificar el padding o margen, no decoraciones visuales.
ThemeData
No exactamente.
ThemeData sirve para aplicar estilos a nivel de aplicación, no para decoraciones de contenedores individuales.
TextStyle
No exactamente.
TextStyle sirve para dar formato al texto, no para decoraciones de contenedores.
BoxDecoration
¡Así es!
BoxDecoration puede agregar bordes, colores de fondo, gradientes, sombras y más a un Container.
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.