1. Introducción
Flutter es el kit de herramientas de IU de Google diseñado para crear aplicaciones atractivas compiladas de forma nativa que funcionen en dispositivos móviles, la Web y computadoras a partir de una base de código única. Flutter es gratuito y de código abierto; además, funciona con el código existente. Lo utilizan desarrolladores y organizaciones de todo el mundo.
En este codelab, mejorarás una aplicación musical de Flutter, que pasará de ser aburrida a atractiva. Para lograr esto, el codelab usa las herramientas y las API que se presentaron en Material 3.
Qué aprenderás
- Cómo programar una app de Flutter que sea atractiva y se pueda usar en diferentes plataformas
- Cómo diseñar texto en tu app para asegurarte de que contribuya a la experiencia del usuario
- Cómo elegir los colores adecuados, personalizar widgets, crear tu propio tema y, luego, implementar rápida y fácilmente el modo oscuro
- Cómo crear apps multiplataforma adaptables
- Cómo crear apps que se vean bien en cualquier pantalla
- Cómo agregar movimiento a tu app de Flutter para que realmente se destaque
Requisitos previos:
En este codelab, se presupone que tienes experiencia en Flutter. Si no es así, te recomendamos que primero aprendas los conceptos básicos. Los siguientes vínculos son útiles:
- Realiza un recorrido por el framework de widgets de Flutter.
- Revisa el codelab sobre cómo programar tu primera app de Flutter (parte 1).
Qué crearás
En este codelab, te ayudaremos a crear la pantalla principal de una app llamada MyArtist, un reproductor de música en el que los fans pueden mantenerse al tanto de sus artistas favoritos. Se analiza cómo puedes modificar el diseño de la app para que luzca atractiva en todas las plataformas.
En los siguientes GIF animados, se muestra cómo funcionará la app cuando completes el codelab:
¿Qué te gustaría aprender de este codelab?
2. Configura tu entorno de Flutter
Para completar este lab, necesitas dos programas de software: el SDK de Flutter y un editor.
Puedes ejecutar el codelab con cualquiera de estos dispositivos o modalidades:
- Un dispositivo físico Android o iOS conectado a tu computadora y configurado en el Modo de desarrollador
- El simulador de iOS (requiere instalar las herramientas de Xcode)
- Android Emulator (requiere configuración en Android Studio)
- Un navegador (se requiere Chrome para la depuración)
- Como una aplicación para computadoras que ejecuten Windows, Linux o macOS (debes desarrollarla en la plataforma donde tengas pensado realizar la implementación; por lo tanto, si quieres desarrollar una app de escritorio para Windows, debes desarrollarla en ese SO a fin de obtener acceso a la cadena de compilación correcta; encuentra detalles sobre los requisitos específicos del sistema operativo en flutter.dev/desktop)
3. Obtén la app de inicio del codelab
Clónala desde GitHub
Para clonar este codelab desde GitHub, ejecuta los siguientes comandos:
git clone https://github.com/flutter/codelabs.git cd codelabs/boring_to_beautiful/step_01/
A fin de asegurarte de que todo funcione correctamente, ejecuta la aplicación de Flutter como una aplicación para computadoras de la manera que se muestra más adelante. De forma alternativa, abre este proyecto en tu IDE y usa sus herramientas para ejecutar la aplicación.
¡Listo! Se debería ejecutar el código de inicio correspondiente a la pantalla principal de MyArtist, por lo que deberías verla. Luce bien en computadoras, pero no así en dispositivos móviles. Por ejemplo, no toma en consideración la muesca de la pantalla. No te preocupes, ya que corregirás este problema.
Explora el código
A continuación, explora el código.
Abre el archivo lib/src/features/home/view/home_screen.dart
, que contiene lo siguiente:
import 'package:adaptive_components/adaptive_components.dart';
import 'package:flutter/material.dart';
import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
final PlaylistsProvider playlistProvider = PlaylistsProvider();
final List<Playlist> playlists = playlistProvider.playlists;
final Playlist topSongs = playlistProvider.topSongs;
final Playlist newReleases = playlistProvider.newReleases;
final ArtistsProvider artistsProvider = ArtistsProvider();
final List<Artist> artists = artistsProvider.artists;
return LayoutBuilder(
builder: (context, constraints) {
// Add conditional mobile layout
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2), // Modify this line
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(2), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
// Add spacer between tables
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(2), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Este archivo importa material.dart
e implementa un widget con estado mediante dos clases:
- La instrucción
import
hace que los componentes de Material estén disponibles. - La clase
HomeScreen
representa toda la página que se muestra. - El método
build()
de la clase_HomeScreenState
crea la raíz del árbol de widgets, lo que afecta cómo se crean todos los widgets de la IU.
4. Aprovecha la tipografía
El texto está en todas partes y es una forma útil de comunicarse con el usuario. ¿La app es amigable y divertida, o, tal vez, confiable y profesional? Hay una razón por la que tu app de banca favorita no usa Comic Sans. La manera en que se presenta el texto influye en la primera impresión que tiene el usuario de tu app. Estas son algunas maneras de usar el texto con más atención.
Una imagen vale más que mil palabras
Cuando sea posible, usa imágenes en vez de texto. Por ejemplo, el elemento NavigationRail
de la app de inicio tiene pestañas para cada ruta principal, pero los íconos principales son idénticos:
Esto no es útil, ya que el usuario debe leer el texto de cada pestaña de todos modos. Comienza por agregar distintivos visuales para que el usuario pueda mirar rápidamente los íconos principales a fin de encontrar la pestaña deseada. Esto también ayuda con la localización y la accesibilidad.
En lib/src/shared/router.dart
, agrega íconos principales distintos para cada destino de navegación (página principal, lista de reproducción y personas):
const List<NavigationDestination> destinations = [
NavigationDestination(
label: 'Home',
icon: Icon(Icons.home), // Modify this line
route: '/',
),
NavigationDestination(
label: 'Playlists',
icon: Icon(Icons.playlist_add_check), // Modify this line
route: '/playlists',
),
NavigationDestination(
label: 'Artists',
icon: Icon(Icons.people), // Modify this line
route: '/artists',
),
];
¿Tienes problemas?
Si la app no se ejecuta correctamente, comprueba que no haya errores ortográficos. De ser necesario, usa el código que aparece en los siguientes vínculos para volver a empezar.
Elige las fuentes con atención
Las fuentes definen la personalidad de tu aplicación, de modo que es fundamental elegir la fuente correcta. A continuación, se indican algunos aspectos que debes tener en cuenta para seleccionar una fuente:
- Sans serif o serif: Las fuentes serif tienen trazos decorativos o “remates” al final de las letras y se perciben como más formales. Las fuentes sans Serif no tienen trazos decorativos y suelen percibirse como más informales. Una letra T mayúscula en sans serif y en serif
- Fuentes en mayúscula: Las mayúsculas constantes son apropiadas para destacar pequeñas cantidades de texto (como los títulos), pero el uso excesivo se puede percibir como una manera de gritar, lo que puede provocar que el usuario lo pase por alto totalmente.
- Uso de mayúsculas: Ten en cuenta este aspecto cuando agregues títulos o etiquetas. Utilizar mayúsculas en cada palabra se percibe como más formal en idiomas como el inglés, pero podría resultar inadecuado en otros. Utilizar mayúsculas solo en los sustantivos propios y en la primera palabra de cada párrafo se percibe como más informal y conversacional en inglés, y es una regla ortográfica en idiomas como el español.
- Interletraje (espacio entre cada letra), longitud de línea (ancho del texto completo en toda la pantalla) y altura de línea (qué tan alta es cada línea de texto): Los valores muy bajos o muy altos de estas características perjudican la legibilidad de la app. Por ejemplo, es fácil perderse en un bloque de texto grande y no separado.
Con esto en mente, ve a Google Fonts y elige una fuente sans serif, como Montserrat, ya que la app de música debe ser divertida.
Desde la línea de comandos, extrae el paquete google_fonts
. Esta acción también actualiza el archivo pubspec para agregar las fuentes como dependencia de la app.
$ flutter pub add google_fonts
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
// Make sure these lines are present from here...
<key>com.apple.security.network.client</key> //For macOS only
<true/>
// .. To here
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
En lib/src/shared/extensions.dart
, importa el paquete nuevo como se muestra a continuación:
import 'package:google_fonts/google_fonts.dart'; // Add this line.
Configura TextTheme:
con la fuente Montserrat
TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line
Recarga en caliente para activar los cambios (usa el botón del IDE o, desde la línea de comandos, ingresa r
para hacerlo):
Deberías ver los nuevos íconos NavigationRail
y texto con la fuente Montserrat.
¿Tienes problemas?
Si la app no se ejecuta correctamente, comprueba que no haya errores ortográficos. De ser necesario, usa el código que aparece en los siguientes vínculos para volver a empezar.
5. Configura el tema
Los temas ayudan a que una app tenga un diseño estructurado y uniformidad, ya que especifican un sistema determinado de colores y estilos de texto. Los temas te permiten implementar rápidamente una IU sin tener que preocuparte por detalles menores, como especificar el color exacto de cada widget.
Los desarrolladores de Flutter suelen crear componentes de temas personalizados de dos maneras:
- Crear widgets personalizados individuales, cada uno con su propio tema
- Crear temas con alcance para widgets predeterminados
En este ejemplo, se usa un proveedor de temas ubicado en lib/src/shared/providers/theme.dart
para crear widgets y colores con un tema coherente en la app:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
class NoAnimationPageTransitionsBuilder extends PageTransitionsBuilder {
const NoAnimationPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return child;
}
}
class ThemeSettingChange extends Notification {
ThemeSettingChange({required this.settings});
final ThemeSettings settings;
}
class ThemeProvider extends InheritedWidget {
const ThemeProvider(
{Key? key,
required this.settings,
required this.lightDynamic,
required this.darkDynamic,
required Widget child})
: super(key: key, child: child);
final ValueNotifier<ThemeSettings> settings;
final ColorScheme? lightDynamic;
final ColorScheme? darkDynamic;
final pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: NoAnimationPageTransitionsBuilder(),
TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(),
TargetPlatform.windows: NoAnimationPageTransitionsBuilder(),
},
);
Color custom(CustomColor custom) {
if (custom.blend) {
return blend(custom.color);
} else {
return custom.color;
}
}
Color blend(Color targetColor) {
return Color(
Blend.harmonize(targetColor.value, settings.value.sourceColor.value));
}
Color source(Color? target) {
Color source = settings.value.sourceColor;
if (target != null) {
source = blend(target);
}
return source;
}
ColorScheme colors(Brightness brightness, Color? targetColor) {
final dynamicPrimary = brightness == Brightness.light
? lightDynamic?.primary
: darkDynamic?.primary;
return ColorScheme.fromSeed(
seedColor: dynamicPrimary ?? source(targetColor),
brightness: brightness,
);
}
ShapeBorder get shapeMedium => RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
);
CardTheme cardTheme() {
return CardTheme(
elevation: 0,
shape: shapeMedium,
clipBehavior: Clip.antiAlias,
);
}
ListTileThemeData listTileTheme(ColorScheme colors) {
return ListTileThemeData(
shape: shapeMedium,
selectedColor: colors.secondary,
);
}
AppBarTheme appBarTheme(ColorScheme colors) {
return AppBarTheme(
elevation: 0,
backgroundColor: colors.surface,
foregroundColor: colors.onSurface,
);
}
TabBarTheme tabBarTheme(ColorScheme colors) {
return TabBarTheme(
labelColor: colors.secondary,
unselectedLabelColor: colors.onSurfaceVariant,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
color: colors.secondary,
width: 2,
),
),
),
);
}
BottomAppBarTheme bottomAppBarTheme(ColorScheme colors) {
return BottomAppBarTheme(
color: colors.surface,
elevation: 0,
);
}
BottomNavigationBarThemeData bottomNavigationBarTheme(ColorScheme colors) {
return BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: colors.surfaceVariant,
selectedItemColor: colors.onSurface,
unselectedItemColor: colors.onSurfaceVariant,
elevation: 0,
landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
);
}
NavigationRailThemeData navigationRailTheme(ColorScheme colors) {
return const NavigationRailThemeData();
}
DrawerThemeData drawerTheme(ColorScheme colors) {
return DrawerThemeData(
backgroundColor: colors.surface,
);
}
ThemeData light([Color? targetColor]) {
final _colors = colors(Brightness.light, targetColor);
return ThemeData.light().copyWith(
pageTransitionsTheme: pageTransitionsTheme,
colorScheme: _colors,
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(_colors),
bottomAppBarTheme: bottomAppBarTheme(_colors),
bottomNavigationBarTheme: bottomNavigationBarTheme(_colors),
navigationRailTheme: navigationRailTheme(_colors),
tabBarTheme: tabBarTheme(_colors),
drawerTheme: drawerTheme(_colors),
scaffoldBackgroundColor: _colors.background,
useMaterial3: true,
);
}
ThemeData dark([Color? targetColor]) {
final _colors = colors(Brightness.dark, targetColor);
return ThemeData.dark().copyWith(
pageTransitionsTheme: pageTransitionsTheme,
colorScheme: _colors,
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(_colors),
bottomAppBarTheme: bottomAppBarTheme(_colors),
bottomNavigationBarTheme: bottomNavigationBarTheme(_colors),
navigationRailTheme: navigationRailTheme(_colors),
tabBarTheme: tabBarTheme(_colors),
drawerTheme: drawerTheme(_colors),
scaffoldBackgroundColor: _colors.background,
useMaterial3: true,
);
}
ThemeMode themeMode() {
return settings.value.themeMode;
}
ThemeData theme(BuildContext context, [Color? targetColor]) {
final brightness = MediaQuery.of(context).platformBrightness;
return brightness == Brightness.light
? light(targetColor)
: dark(targetColor);
}
static ThemeProvider of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ThemeProvider>()!;
}
@override
bool updateShouldNotify(covariant ThemeProvider oldWidget) {
return oldWidget.settings != settings;
}
}
class ThemeSettings {
ThemeSettings({
required this.sourceColor,
required this.themeMode,
});
final Color sourceColor;
final ThemeMode themeMode;
}
Color randomColor() {
return Color(Random().nextInt(0xFFFFFFFF));
}
// Custom Colors
const linkColor = CustomColor(
name: 'Link Color',
color: Color(0xFF00B0FF),
);
class CustomColor {
const CustomColor({
required this.name,
required this.color,
this.blend = true,
});
final String name;
final Color color;
final bool blend;
Color value(ThemeProvider provider) {
return provider.custom(this);
}
}
Para usar el proveedor, crea una instancia y pásala al objeto de tema específico en MaterialApp
, ubicado en lib/src/shared/app.dart
. Lo heredará cualquier objeto Theme
anidado:
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'playback/bloc/bloc.dart';
import 'providers/theme.dart';
import 'router.dart';
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final settings = ValueNotifier(ThemeSettings(
sourceColor: Colors.pink,
themeMode: ThemeMode.system,
));
@override
Widget build(BuildContext context) {
return BlocProvider<PlaybackBloc>(
create: (context) => PlaybackBloc(),
child: DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) => ThemeProvider(
lightDynamic: lightDynamic,
darkDynamic: darkDynamic,
settings: settings,
child: NotificationListener<ThemeSettingChange>(
onNotification: (notification) {
settings.value = notification.settings;
return true;
},
child: ValueListenableBuilder<ThemeSettings>(
valueListenable: settings,
builder: (context, value, _) {
final theme = ThemeProvider.of(context); // Add this line
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: theme.light(settings.value.sourceColor), // Add this line
routeInformationParser: appRouter.routeInformationParser,
routerDelegate: appRouter.routerDelegate,
);
},
),
)),
),
);
}
}
Ahora que el tema está configurado, elige colores para la aplicación.
Elegir el conjunto de colores adecuado no siempre es fácil. Quizás tengas una idea del color principal, pero es probable que quieras que la app tenga más de uno. ¿Qué colores deben tener el texto, el título, el contenido y los vínculos? ¿Qué ocurre con el color del fondo? Material Theme Builder es una herramienta basada en la Web (lanzada en Material 3) que te ayuda a seleccionar un conjunto de colores complementarios para tu app.
A fin de elegir un color de origen para la aplicación, abre Material Theme Builder y explora los diferentes colores de la IU. Es importante que selecciones un color que se adapte a la estética de la marca o tus preferencias personales.
Después de crear un tema, haz clic con el botón derecho en el cuadro de color Primary. Se abrirá un cuadro de diálogo con el valor hexadecimal del color principal. Copia este valor. También puedes establecer el color mediante este cuadro de diálogo.
Pasa el valor hexadecimal del color principal al proveedor de temas. Por ejemplo, el color hexadecimal #00cbe6
se especifica como Color(0xff00cbe6)
. El ThemeProvider
genera un objeto ThemeData
que contiene el conjunto de colores complementarios de los que obtuviste una vista previa en Material Theme Builder:
final settings = ValueNotifier(ThemeSettings(
sourceColor: Color(0xff00cbe6), // Replace this color
themeMode: ThemeMode.system,
));
Reinicia la app en caliente. Una vez que se aplique el color primario, la app comienza a parecer más expresiva. Para acceder a todos los colores nuevos, haz referencia al tema en el contexto y toma el ColorScheme
:
final colors = Theme.of(context).colorScheme;
Para usar un color en particular, accede a un rol de color en colorScheme
. Ve a lib/src/shared/views/outlined_card.dart
y asígnale un borde a OutlinedCard
:
class _OutlinedCardState extends State<OutlinedCard> {
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: widget.clickable
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: Container(
child: widget.child,
// Add from here...
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
),
// ... To here.
),
);
}
}
Material 3 presenta roles de color matizados que se complementan y pueden usarse en toda la IU para agregar nuevas capas de expresión. Entre estos nuevos roles de color, se incluyen los siguientes:
Primary
,OnPrimary
,PrimaryContainer
yOnPrimaryContainer
Secondary
,OnSecondary
,SecondaryContainer
yOnSecondaryContainer
Tertiary
,OnTertiary
,TertiaryContainer
yOnTertiaryContainer
Error
,OnError
,ErrorContainer
yOnErrorContainer
Background
yOnBackground
Surface
,OnSurface
,SurfaceVariant
yOnSurfaceVariant
InversePrimary
,Shadow
yOutline
Además, los nuevos tokens de diseño admiten los temas claro y oscuro:
Estos roles de color se pueden usar para asignar significado y énfasis a diferentes partes de la IU. Incluso si un componente no se destaca, puedes aprovechar el color dinámico.
El usuario puede establecer el brillo de la app en la configuración del sistema del dispositivo. En lib/src/shared/app.dart
, cuando el dispositivo esté en modo oscuro, muestra un tema oscuro y el modo de tema a la MaterialApp
.
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: theme.light(settings.value.sourceColor),
darkTheme: theme.dark(settings.value.sourceColor), // Add this line
themeMode: theme.themeMode(), // Add this line
routeInformationParser: appRouter.routeInformationParser,
routerDelegate: appRouter.routerDelegate,
);
Haz clic en el ícono de luna ubicado en la esquina superior derecha para habilitar el modo oscuro.
¿Tienes problemas?
Si la app no se ejecuta de manera correcta, usa el código del siguiente vínculo para solucionar el problema.
6. Agrega un diseño adaptable
Con Flutter, puedes crear apps que se ejecuten casi en cualquier plataforma, pero eso no quiere decir que se espera que se comporten igual en todas ellas. Los usuarios esperan diferentes comportamientos y funciones en distintas plataformas.
Material ofrece paquetes de Flutter para facilitar el trabajo con diseños adaptables. Puedes encontrarlos en GitHub.
Ten en cuenta las siguientes diferencias de plataforma cuando compiles una aplicación adaptable y multiplataforma:
- Método de entrada: Mouse, panel táctil o control de juegos
- Tamaño de fuente, orientación del dispositivo y distancia de visualización
- Tamaño de pantalla y factor de forma: Teléfono, tablet, dispositivo plegable, computadora o Web
El archivo lib/src/shared/views/adaptive_navigation.dart
contiene una clase de navegación en la que puedes proporcionar una lista de destinos y contenido para renderizar el cuerpo. Como se usa este diseño en varias pantallas, hay un diseño de base compartido para pasar a cada diseño secundario. Los rieles de navegación son ideales para computadoras y pantallas grandes, pero, en su lugar, muestran una barra de navegación inferior en dispositivos móviles para optimizar el diseño.
import 'package:flutter/material.dart';
class AdaptiveNavigation extends StatelessWidget {
const AdaptiveNavigation({
Key? key,
required this.destinations,
required this.selectedIndex,
required this.onDestinationSelected,
required this.child,
}) : super(key: key);
final List<NavigationDestination> destinations;
final int selectedIndex;
final void Function(int index) onDestinationSelected;
final Widget child;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, dimens) {
// Tablet Layout
if (dimens.maxWidth >= 600) { // Add this line
return Scaffold(
body: Row(
children: [
NavigationRail(
extended: dimens.maxWidth >= 800,
minExtendedWidth: 180,
destinations: destinations
.map((e) => NavigationRailDestination(
icon: e.icon,
label: Text(e.label),
))
.toList(),
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
),
Expanded(child: child),
],
),
);
} // Add this line
// Mobile Layout
// Add from here...
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
destinations: destinations,
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
),
);
// ... To here.
},
);
}
}
No todas las pantallas tienen el mismo tamaño. Si intentas mostrar la versión para computadoras de tu app en un teléfono, tendrías que forzar la vista y hacer zoom para ver todo. Quieres que tu app cambie de aspecto en función de la pantalla donde se muestra. El diseño responsivo te permite garantizar que tu app se vea genial en pantallas de todos los tamaños.
Para que tu app sea responsiva, agrega algunos puntos de interrupción adaptables (que no se deben confundir con los de depuración). Estos puntos de interrupción especifican los tamaños de pantalla en los que tu app debería cambiar su diseño.
Las pantallas más pequeñas no pueden mostrar tanto contenido como las más grandes sin reducir el tamaño del contenido. A fin de evitar que tu app parezca una de escritorio con tamaño reducido, crea un diseño independiente para dispositivos móviles que use pestañas a fin de desglosar el contenido. De esta manera, la app tiene un aspecto más nativo en dispositivos móviles.
Los siguientes métodos de extensión (definidos en el proyecto MyArtist de lib/src/shared/extensions.dart
) son un buen punto de partida si deseas crear diseños optimizados para diferentes destinos.
extension BreakpointUtils on BoxConstraints {
bool get isTablet => maxWidth > 730;
bool get isDesktop => maxWidth > 1200;
bool get isMobile => !isTablet && !isDesktop;
}
Una pantalla de más de 730 píxeles (en el lado más largo), pero menos de 1,200 píxeles, se considera una tablet. Cualquier tamaño superior a 1,200 píxeles se considera una computadora. Si un dispositivo no es una tablet ni una computadora de escritorio, se considerará un dispositivo móvil. Puedes obtener más información sobre los puntos de interrupción adaptables en material.io. Te recomendamos usar el paquete adaptive_breakpoints.
El diseño responsivo de la pantalla principal usa AdaptiveContainer
y AdaptiveColumn
basados en la cuadrícula de 12 columnas mediante los paquetes adaptive_components y adaptive_breakpoints para implementar un diseño de cuadrícula responsivo en Material Design.
return LayoutBuilder(
builder: (context, constraints) {
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 40,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 20,
),
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
Un diseño adaptable necesita dos diseños: uno para dispositivos móviles y otro responsivo para pantallas más grandes. Actualmente, LayoutBuilder
solo muestra un diseño para computadoras. En lib/src/features/home/view/home_screen.dart
, crea el diseño para dispositivos móviles como un elemento TabBar
y TabBarView
con 4 pestañas.
import 'package:adaptive_components/adaptive_components.dart';
import 'package:flutter/material.dart';
import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
final PlaylistsProvider playlistProvider = PlaylistsProvider();
final List<Playlist> playlists = playlistProvider.playlists;
final Playlist topSongs = playlistProvider.topSongs;
final Playlist newReleases = playlistProvider.newReleases;
final ArtistsProvider artistsProvider = ArtistsProvider();
final List<Artist> artists = artistsProvider.artists;
return LayoutBuilder(
builder: (context, constraints) {
// Add from here...
if (constraints.isMobile) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
centerTitle: false,
title: const Text('Good morning'),
actions: const [BrightnessToggle()],
bottom: const TabBar(
isScrollable: true,
tabs: [
Tab(text: 'Home'),
Tab(text: 'Recently Played'),
Tab(text: 'New Releases'),
Tab(text: 'Top Songs'),
],
),
),
body: LayoutBuilder(
builder: (context, constraints) => TabBarView(
children: [
SingleChildScrollView(
child: Column(
children: [
const HomeHighlight(),
HomeArtists(
artists: artists,
constraints: constraints,
),
],
),
),
HomeRecent(
playlists: playlists,
axis: Axis.vertical,
),
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
],
),
),
),
);
}
// ... To here.
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 40,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 20,
),
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
¿Tienes problemas?
Si la app no se ejecuta de manera correcta, usa el código del siguiente vínculo para solucionar el problema.
Usa espacio en blanco
El espacio en blanco es una herramienta visual importante para tu app que crea una interrupción organizacional entre secciones.
Es mejor tener demasiado espacio en blanco que no tener suficiente. Es preferible agregar más espacio en blanco que reducir el tamaño de la fuente o los elementos visuales para que quepan más.
La falta de espacio en blanco puede ser difícil para quienes tienen problemas de visión. Una cantidad excesiva de espacio en blanco puede demostrar falta de coherencia y hacer que la IU luzca mal organizada. Por ejemplo, mira las siguientes capturas de pantalla:
A continuación, agregarás espacio en blanco a la pantalla principal. Luego, ajustarás el diseño aún más para ajustar el espacio.
Une un widget con un objeto Padding
para agregar espacios en blanco alrededor de ese widget. Aumenta todos los valores de relleno que se encuentran actualmente en lib/src/features/home/view/home_screen.dart
a 35:
Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(35), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) => PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
// Add spacer between tables
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(35), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) => PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
Recarga la app en caliente. Debería verse igual que antes, pero con más espacio en blanco entre los widgets. El relleno adicional se ve mejor, pero el banner de resaltado en la parte superior todavía está demasiado cerca de los bordes.
En lib/src/features/home/view/home_highlight.dart
, cambia el relleno del banner a 35:
class HomeHighlight extends StatelessWidget {
const HomeHighlight({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Clickable(
child: SizedBox(
height: 275,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
'assets/images/news/concert.jpeg',
fit: BoxFit.cover,
),
),
),
onTap: () => launch('https://docs.flutter.dev'),
),
),
),
],
);
}
}
Recarga la app en caliente. Las dos listas de reproducción de la parte inferior no tienen espacio en blanco entre ellas, por lo que parecen pertenecer a la misma tabla. No es el caso, así que lo solucionarás a continuación.
Agrega espacio en blanco entre las listas de reproducción insertando un widget de tamaño en el elemento Row
que las contiene. En lib/src/features/home/view/home_screen.dart
, agrega un elemento SizedBox
con un ancho de 35:
Padding(
padding: const EdgeInsets.all(35),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
],
),
),
const SizedBox(width: 35), // Add this line
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
],
),
),
],
),
),
Recarga la app en caliente. Debería verse de la siguiente manera:
Ahora hay suficiente espacio para el contenido de la pantalla principal, pero todo está demasiado separado y no hay cohesión entre las secciones.
Hasta ahora, configuraste todos los rellenos (horizontales y verticales) de los widgets en la pantalla principal en 35 con EdgeInsets.all(35)
, pero también puedes configurar el relleno para cada uno de los bordes. Personaliza el relleno para que se adapte mejor al espacio.
EdgeInsets.LTRB()
se configura de forma individual a la izquierda, derecha y arriba y abajo.EdgeInsets.symmetric()
establece que el relleno vertical (partes inferior y superior) sea equivalente y que el horizontal (izquierda y derecha) también lo sea.EdgeInsets.only()
establece solo los bordes especificados.
Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 25, 20, 10), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
), // Modify this line
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 8, bottom: 8), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 8, bottom: 8), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
En lib/src/features/home/view/home_highlight.dart
, establece el relleno izquierdo y derecho del banner en 35, y los rellenos en las partes inferior y superior en 5:
class HomeHighlight extends StatelessWidget {
const HomeHighlight({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Padding(
// Modify this line
padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 5),
child: Clickable(
child: SizedBox(
height: 275,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
'assets/images/news/concert.jpeg',
fit: BoxFit.cover,
),
),
),
onTap: () => launch('https://docs.flutter.dev'),
),
),
),
],
);
}
}
Recarga la app en caliente. El diseño y el espacio se ven mucho mejor. Como toque final, agrega movimiento y animación.
¿Tienes problemas?
Si la app no se ejecuta de manera correcta, usa el código del siguiente vínculo para solucionar el problema.
7. Agrega movimiento y animación
El movimiento y la animación son formas excelentes de agregar desplazamiento y energía, y de proporcionar una respuesta cuando el usuario interactúa con la app.
Agrega animaciones entre pantallas
El ThemeProvider
define un PageTransitionsTheme
con animaciones de transición de pantalla para plataformas móviles (iOS y Android). Los usuarios de computadoras ya reciben respuestas del clic del mouse o del panel táctil, por lo que no se necesita una animación de transición de página.
Flutter brinda las animaciones de transición de pantalla que puedes configurar para tu app en función de la plataforma de destino, como se muestra en lib/src/shared/providers/theme.dart
:
final pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: NoAnimationPageTransitionsBuilder(),
TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(),
TargetPlatform.windows: NoAnimationPageTransitionsBuilder(),
},
);
Pasa PageTransitionsTheme
a los temas claro y oscuro en lib/src/shared/providers/theme.dart.
ThemeData light([Color? targetColor]) {
final _colors = colors(Brightness.light, targetColor);
return ThemeData.light().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: ColorScheme.fromSeed(
seedColor: source(targetColor),
brightness: Brightness.light,
),
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(),
tabBarTheme: tabBarTheme(_colors),
scaffoldBackgroundColor: _colors.background,
);
}
ThemeData dark([Color? targetColor]) {
final _colors = colors(Brightness.dark, targetColor);
return ThemeData.dark().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: ColorScheme.fromSeed(
seedColor: source(targetColor),
brightness: Brightness.dark,
),
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(),
tabBarTheme: tabBarTheme(_colors),
scaffoldBackgroundColor: _colors.background,
);
}
Sin animación en iOS | Con animación en iOS |
¿Tienes problemas?
Si la app no se ejecuta de manera correcta, usa el código del siguiente vínculo para solucionar el problema.
Agrega estados cuando se coloca el cursor sobre un elemento
Una forma de agregar movimiento a una app de escritorio es con los estados de desplazamiento, donde un widget cambia su estado (como el color, la forma o el contenido) cuando el usuario coloca el cursor sobre él.
De forma predeterminada, la clase _OutlinedCardState
(que se usa para los mosaicos de listas de reproducción “Recently played”) muestra un elemento MouseRegion
, que convierte la flecha del cursor en una mano cuando se coloca el cursor sobre un elemento, pero puedes agregar más respuestas visuales.
Abre lib/src/shared/views/outlined_card.dart y reemplaza su contenido por la siguiente implementación para agregar un estado _hovered
.
import 'package:flutter/material.dart';
class OutlinedCard extends StatefulWidget {
const OutlinedCard({
Key? key,
required this.child,
this.clickable = true,
}) : super(key: key);
final Widget child;
final bool clickable;
@override
State<OutlinedCard> createState() => _OutlinedCardState();
}
class _OutlinedCardState extends State<OutlinedCard> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final borderRadius = BorderRadius.circular(_hovered ? 20 : 8);
const animationCurve = Curves.easeInOut;
return MouseRegion(
onEnter: (_) {
if (!widget.clickable) return;
setState(() {
_hovered = true;
});
},
onExit: (_) {
if (!widget.clickable) return;
setState(() {
_hovered = false;
});
},
cursor: widget.clickable ? SystemMouseCursors.click : SystemMouseCursors.basic,
child: AnimatedContainer(
duration: kThemeAnimationDuration,
curve: animationCurve,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
borderRadius: borderRadius,
),
foregroundDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurface.withOpacity(
_hovered ? 0.12 : 0,
),
borderRadius: borderRadius,
),
child: TweenAnimationBuilder<BorderRadius>(
duration: kThemeAnimationDuration,
curve: animationCurve,
tween: Tween(begin: BorderRadius.zero, end: borderRadius),
builder: (context, borderRadius, child) => ClipRRect(
clipBehavior: Clip.antiAlias,
borderRadius: borderRadius,
child: child,
),
child: widget.child,
),
),
);
}
}
Recarga la app en caliente y, luego, coloca el cursor sobre uno de los mosaicos de la lista de reproducción que se reprodujeron recientemente.
OutlinedCard
cambia la opacidad y redondea las esquinas.
Por último, anima el número de canción de una lista de reproducción en un botón de reproducción con el widget HoverableSongPlayButton
definido en lib/src/shared/views/hoverable_song_play_button.dart
. En lib/src/features/playlists/view/playlist_songs.dart
, une el widget Center
(que contiene el número de canción) con un HoverableSongPlayButton
:
HoverableSongPlayButton( // Add this line
hoverMode: HoverMode.overlay, // Add this line
song: playlist.songs[index], // Add this line
child: Center( // Modify this line
child: Text(
(index + 1).toString(),
textAlign: TextAlign.center,
),
),
), // Add this line
Recarga la app en caliente y coloca el cursor sobre el número de la canción en las listas de reproducción Top Songs Today o New Releases.
El número comienza con un botón de reproducción que reproduce la canción cuando haces clic en ella.
Consulta el código final del proyecto en GitHub.
8. ¡Felicitaciones!
Completaste este codelab. Aprendiste que hay muchos cambios pequeños que puedes integrar en una app a fin de hacerla más atractiva, accesible, localizable y adecuada para múltiples plataformas. Estas técnicas incluyen, entre otras, las siguientes:
- Tipografía: El texto es más que una simple herramienta de comunicación. Usa la manera en que se muestra el texto para generar un efecto positivo en la experiencia de los usuarios y la percepción de tu app.
- Temas: Establece un sistema de diseño que puedas usar de manera confiable sin tener que tomar decisiones de diseño para cada widget.
- Adaptabilidad: Considera el dispositivo y la plataforma en los que se ejecuta tu app y sus funciones. Ten en cuenta el tamaño de la pantalla y cómo se muestra tu app.
- Movimiento y animación: Cuando agregas movimiento a tu app, se agrega energía a la experiencia del usuario y, con mayor práctica, se proporcionan comentarios para los usuarios.
Con algunos pequeños ajustes, tu app pasará de ser aburrida a atractiva:
Antes | Después |
Próximos pasos
Esperamos que hayas aprendido más sobre cómo crear apps atractivas en Flutter.
Si aplicas algunas de las sugerencias o trucos que se mencionan aquí (o tienes una sugerencia propia para compartir), nos encantaría conocer tu opinión. Comunícate con nosotros en Twitter mediante los identificadores @rodydavis y @khanhnwin.
Los siguientes recursos también pueden resultarte útiles.
Temas
- Material Theme Builder (herramienta)
Recursos adaptables y responsivos:
- Adaptativo versus responsivo | Decoding Flutter (video)
- Adaptive layouts (video de The Boring Flutter Development Show)
- Creating responsive and adaptive apps (flutter.dev)
- Adaptive Material components for Flutter (biblioteca de GitHub)
- 5 things you can do to prepare your app for large screens (video de Google I/O 2021)
Recursos generales de diseño:
- Pequeños detalles: Cómo convertirse en un mítico diseñador-desarrollador (video de Flutter Engage)
- Material Design 3 for Foldable Devices (material.io)
Además, puedes conectarte con la comunidad de Flutter.
¡Sigue adelante y mejora el atractivo del ecosistema de apps!