1. Introdução
O Flutter é um kit de ferramentas de IU do Google para criar apps incríveis e nativos para dispositivos móveis, Web e computadores com uma única base de código. Ele funciona com código existente, é usado por desenvolvedores e organizações do mundo todo, não tem custo algum e é de código aberto.
Neste codelab, você vai aprimorar um aplicativo de música do Flutter, transformando-o de chato para lindo. Para isso, este codelab usa ferramentas e APIs introduzidas no Material 3.
O que você vai aprender
- Como criar um app do Flutter que possa ser usado e tenha uma linda aparência em várias plataformas.
- Como projetar texto no app para garantir que ele vá melhorar a experiência do usuário.
- Como escolher as cores certas, personalizar widgets, criar seu próprio tema e implementar o modo escuro de forma rápida e fácil.
- Como criar apps adaptáveis multiplataforma.
- Como criar apps com uma boa aparência em qualquer tela.
- Como adicionar movimento ao seu app Flutter para que ele fique realmente incrível.
Pré-requisitos:
Este codelab presume que você tem alguma experiência com o Flutter. Caso contrário, talvez você queira primeiro aprender o básico. Confira alguns links úteis:
- Faça um tour do framework do widget do Flutter
- Teste o codelab Criar seu primeiro app do Flutter, parte 1
O que você vai criar
Este codelab ajudará você a criar a tela inicial de um app chamado MyArtist, um app de música onde os fãs podem acompanhar seus artistas favoritos. Ele mostra como modificar o design do app para que fique bonito em várias plataformas.
Os GIFs animados a seguir mostram como o app funciona na conclusão deste codelab:
O que você quer aprender com este codelab?
2. Configurar seu ambiente do Flutter
Você precisa de dois softwares para concluir este laboratório: o SDK do Flutter e um editor.
É possível executar o codelab usando qualquer um destes dispositivos:
- Um dispositivo físico Android ou iOS conectado ao seu computador e configurado para o modo de desenvolvedor.
- O simulador para iOS, que exige a instalação de ferramentas do Xcode.
- O Android Emulator, que requer configuração no Android Studio.
- Um navegador (o Chrome é necessário para depuração).
- Como um aplicativo para computador Windows, Linux ou macOS. Você precisa desenvolver na plataforma em que planeja implantar. Portanto, se quiser desenvolver um app para um computador Windows, você terá que desenvolver no Windows para acessar a cadeia de compilação adequada. Há requisitos específicos de cada sistema operacional que são abordados em detalhes em flutter.dev/desktop (link em inglês).
3. Instalar o app inicial do codelab
Clone do GitHub
Para clonar este codelab do GitHub, execute estes comandos:
git clone https://github.com/flutter/codelabs.git cd codelabs/boring_to_beautiful/step_01/
Para garantir que tudo esteja funcionando, execute o aplicativo do Flutter como um aplicativo para computador, conforme mostrado abaixo. Como alternativa, abra esse projeto no seu ambiente de desenvolvimento integrado e use as ferramentas dele para executar o aplicativo.
Pronto. O código inicial da tela inicial do MyArtist será executado. Você verá a tela inicial do MyArtist. Parece bom no computador, mas no celular... Não está legal. Por um lado, ele não faz jus ao nível. Não se preocupe, você corrigirá o problema.
Tour pelo código
Em seguida, faça um tour pelo código.
Abra o lib/src/features/home/view/home_screen.dart
, que contém o seguinte:
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Esse arquivo importa material.dart
e implementa um widget com estado usando duas classes:
- A instrução
import
disponibiliza os componentes do Material Design. - A classe
HomeScreen
representa a página inteira exibida. - O método
build()
da classe_HomeScreenState
cria a raiz da árvore de widgets, o que afeta a criação de todos os widgets na IU.
4. Aproveitar a tipografia
O texto está em todos os lugares. O texto é uma forma útil de se comunicar com o usuário. O app se destina a ser amigável e divertido, ou talvez confiável e profissional? Há uma razão para seu aplicativo bancário favorito não usar a Comic Sans. A forma como o texto é apresentado molda a primeira impressão do usuário sobre seu app. Veja algumas maneiras de usar o texto com mais atenção.
Não diga, mostre
Sempre que possível, mostre ao invés de dizer. Por exemplo, o NavigationRail
no app inicial tem guias para cada trajeto principal, mas os ícones principais são idênticos:
Isso não é útil porque o usuário ainda precisa ler o texto de cada guia. Comece adicionando indicações visuais para que o usuário possa ver rapidamente os ícones principais para encontrar a guia desejada. Isso também ajuda na localização e na acessibilidade.
Em lib/src/shared/router.dart
, adicione ícones diferentes para cada destino de navegação (página inicial, playlist e pessoas):
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',
),
];
Problemas?
Caso o app não esteja executando corretamente, verifique se há erros de digitação. Se necessário, use o código nos links a seguir para colocar tudo de volta nos eixos.
- lib/src/shared/router.dart (link em inglês)
Escolha bem as fontes
As fontes definem a personalidade do seu aplicativo, portanto, escolher a fonte certa é crucial. Ao selecionar uma fonte, considere os seguintes pontos:
- Sans-CRL ou Serif: as fontes Serif têm traços ou decorações no final das letras que são consideradas mais formais. As fontes Sans Serif não têm traços decorativos e tendem a ser consideradas mais informais. Um T maiúsculo em Sans Serif e outro em Serif
- Todas as fontes em letras maiúsculas: usar letras maiúsculas é adequado para chamar a atenção para pequenas quantidades de texto (como títulos). No entanto, quando usado demais, ele pode ser entendido como gritar, fazendo o usuário ignorar o texto.
- Letras maiúsculas no estilo padrão de títulos (em inglês): ao adicionar títulos ou rótulos, recomendamos o uso de letras maiúsculas, como o padrão de títulos, em que a primeira letra de cada palavra é maiúscula ("Este é um Título no Estilo Padrão de Títulos"), é mais formal. Letras maiúsculas no estilo padrão das frases em português, que usam apenas substantivos próprios e a primeira palavra do texto é um título mais informal e parece uma conversa.
- Uso do espaçamento (espaçamento entre as letras), comprimento da linha (largura do texto completo na tela) e altura da linha (qual é a altura de cada linha do texto): muito pouco ou em excesso torna o app menos legível. Por exemplo, é fácil se perder ao ler um bloco de texto grande e ininterrupto.
Com isso em mente, acesse o Google Fonts e escolha uma fonte Sans Serif, como Montserrat, porque o app de música deve ser divertido.
Na linha de comando, extraia o pacote google_fonts
. Isso também atualiza o arquivo pubspec para adicionar as fontes como uma dependência de 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>
Em lib/src/shared/extensions.dart
, importe o novo pacote:
import 'package:google_fonts/google_fonts.dart'; // Add this line.
Defina o Montserrat TextTheme:
TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line
Faça uma recarga dinâmica de para ativar as mudanças. Use o botão no seu ambiente de desenvolvimento integrado ou, na linha de comando, insira r
para fazer uma recarga dinâmica.
Você verá os novos ícones NavigationRail
junto com o texto exibido na fonte Montserrat.
Problemas?
Caso o app não esteja executando corretamente, verifique se há erros de digitação. Se necessário, use o código nos links a seguir para colocar tudo de volta nos eixos.
- lib/src/shared/extensions.dart (link em inglês)
5. Definir o tema
Os temas ajudam a dar um design estruturado e uniformidade para um app especificando um sistema definido de cores e estilos de texto. Os temas permitem que você implemente rapidamente uma IU sem precisar se preocupar com detalhes menores, como especificar a cor exata para cada widget.
Os desenvolvedores do Flutter normalmente criam componentes personalizados de duas maneiras:
- Criar widgets personalizados individuais, cada um com um tema próprio;
- Criar temas com escopo para widgets padrão.
Este exemplo usa um provedor de temas localizado em lib/src/shared/providers/theme.dart
para criar widgets e cores com tema consistente em todo o 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 o provedor, crie uma instância e transmita-a para o objeto do tema com escopo no MaterialApp
, localizado em lib/src/shared/app.dart
. Ele será herdado por qualquer objeto Theme
aninhado:
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,
);
},
),
)),
),
);
}
}
Agora que o tema está configurado, escolha cores para o app.
Escolher o conjunto certo de cores nem sempre é fácil. Você pode ter uma ideia da cor principal, mas é provável que queira que o app tenha mais de uma cor. Que cor deve ser o texto? Título? Conteúdo? Links? E a cor de fundo? O Material Theme Builder é uma ferramenta baseada na Web (introduzida no Material 3) que ajuda a selecionar um conjunto de cores complementares para o app.
Para escolher uma cor de origem para o app, abra o Material Theme Builder e explore cores diferentes para a IU. É importante selecionar uma cor adequada à estética da marca e/ou à sua preferência pessoal.
Depois de criar um tema, clique com o botão direito do mouse no balão de cor Principal. Isso abre uma caixa de diálogo com o valor hexadecimal da cor principal. Copie esse valor. Você também pode definir a cor usando essa caixa de diálogo.
Transmita o valor hexadecimal da cor principal ao provedor de tema. Por exemplo, a cor hexadecimal #00cbe6
é especificada como Color(0xff00cbe6)
. O ThemeProvider
gera um ThemeData
que contém o conjunto de cores complementares que você visualizou no Material Theme Builder:
final settings = ValueNotifier(ThemeSettings(
sourceColor: Color(0xff00cbe6), // Replace this color
themeMode: ThemeMode.system,
));
Faça a inicialização do app. Com a cor principal definida, o app começa a parecer mais expressivo. Acesse todas as novas cores fazendo referência ao tema no contexto e capturando o ColorScheme
:
final colors = Theme.of(context).colorScheme;
Para usar uma cor específica, acesse uma função de cor na colorScheme
. Acesse lib/src/shared/views/outlined_card.dart
e adicione uma borda ao 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.
),
);
}
}
O Material 3 introduz funções de cor diferenciadas que se complementam e podem ser usadas em toda a IU para adicionar novas camadas de expressão. Essas novas funções de cor incluem:
Primary
,OnPrimary
,PrimaryContainer
,OnPrimaryContainer
Secondary
,OnSecondary
,SecondaryContainer
,OnSecondaryContainer
Tertiary
,OnTertiary
,TertiaryContainer
,OnTertiaryContainer
Error
,OnError
,ErrorContainer
,OnErrorContainer
Background
,OnBackground
Surface
,OnSurface
,SurfaceVariant
,OnSurfaceVariant
Shadow
,Outline
,InversePrimary
Além disso, os novos tokens de design dão suporte para temas claro e escuro:
Essas funções de cor podem ser usadas para atribuir significado e ênfase a diferentes partes da IU. Mesmo que um componente não esteja em destaque, ele ainda pode usar a cor dinâmica.
O usuário pode definir o brilho do app nas configurações do sistema do dispositivo. No lib/src/shared/app.dart
, quando o dispositivo estiver no modo escuro, retorne um tema escuro e esse modo para o 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,
);
Clique no ícone de lua no canto superior direito para ativar o modo escuro.
Problemas?
Caso seu app não esteja sendo executado corretamente, use o código do link a seguir para colocar tudo de volta nos eixos.
- lib/src/shared/app.dart (link em inglês)
- lib/src/shared/views/outlined_card.dart (link em inglês)
6. Adicionar design adaptável
Com ele, você pode criar apps que funcionam em praticamente qualquer lugar, mas isso não quer dizer que todos eles vão se comportar da mesma forma em todos os lugares. Os usuários esperam diferentes comportamentos e recursos de diferentes plataformas.
O Material Design oferece pacotes para facilitar o trabalho com layouts adaptáveis. Você pode encontrar esses pacotes do Flutter no GitHub (link em inglês).
Considere as seguintes diferenças de plataforma ao criar um aplicativo adaptativo para várias plataformas:
- Método de entrada: mouse, toque ou gamepad
- Tamanho da fonte, orientação do dispositivo e distância de visualização
- Tamanho e formato da tela: smartphone, tablet, dobrável, computador, Web
O arquivo lib/src/shared/views/adaptive_navigation.dart
contém uma classe de navegação em que você pode fornecer uma lista de destinos e conteúdo para renderizar o corpo. Como você usa esse layout em várias telas, há um layout base compartilhado para ser transmitido para cada filho. As colunas de navegação são boas para computadores e telas grandes. No entanto, o layout é compatível com dispositivos móveis e mostra uma barra de navegação na parte inferior em dispositivos móveis.
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.
},
);
}
}
Nem todas as telas são do mesmo tamanho. Se você tentar exibir a versão para computador do app no smartphone, será necessário fazer uma combinação de apertar e aplicar zoom para ver tudo. Você quer que seu app mude a aparência com base na tela em que é exibido. Com o design responsivo, você garante que seu app tenha uma ótima aparência em telas de todos os tamanhos.
Para tornar o app responsivo, inclua alguns pontos de interrupção adaptáveis. Não confunda esses pontos com a depuração de pontos de interrupção. Esses pontos de interrupção especificam os tamanhos de tela em que o app precisa mudar o layout.
As telas menores não têm a mesma capacidade de exibição que as telas maiores, sem reduzir o conteúdo. Para evitar que o app pareça um app para computador que foi reduzido, crie um layout separado para dispositivos móveis que use guias para dividir o conteúdo. Isso dá ao app uma experiência mais nativa em dispositivos móveis.
Os métodos de extensão a seguir, definidos no projeto MyArtist no lib/src/shared/extensions.dart
, são um bom ponto de partida ao criar layouts otimizados para diferentes destinos.
extension BreakpointUtils on BoxConstraints {
bool get isTablet => maxWidth > 730;
bool get isDesktop => maxWidth > 1200;
bool get isMobile => !isTablet && !isDesktop;
}
Uma tela maior que 730 pixels (na direção mais longa), mas menor que 1.200 pixels, é considerada um tablet. Qualquer dispositivo com mais de 1.200 pixels é considerado um computador. Se um dispositivo não for um tablet nem um computador, ele será considerado dispositivo móvel. Saiba mais sobre pontos de interrupção adaptáveis em material.io (link em inglês). Considere usar o pacote adaptive_breakpoints.
O layout responsivo da tela inicial usa AdaptiveContainer
e AdaptiveColumn
com base na grade de 12 colunas usando os pacotes adaptive_components e adaptive_breakpoints para implementar um layout de grade responsivo no 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,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
Um layout adaptável precisa de dois layouts: um para dispositivos móveis e um layout responsivo para telas maiores. No momento, o LayoutBuilder
retorna apenas um layout de computador. No lib/src/features/home/view/home_screen.dart
, crie o layout para dispositivos móveis como um TabBar
e TabBarView
com quatro guias.
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Problemas?
Caso seu app não esteja sendo executado corretamente, use o código do link a seguir para colocar tudo de volta nos eixos.
- lib/src/shared/views/adaptive_navigation.dart (link em inglês)
- lib/src/features/home/view/home_screen.dart (link em inglês)
Usar espaços em branco
O espaço em branco é uma ferramenta visual importante para o app, criando um intervalo organizacional entre as seções.
É melhor ter muito espaço em branco do que uma quantidade insuficiente. É melhor adicionar mais espaços em branco do que diminuir o tamanho da fonte ou dos elementos visuais para caber mais no espaço.
A falta de espaço em branco pode ser difícil para quem tem problemas de visão. O excesso de espaços em branco pode prejudicar a coesão e deixar a IU mal organizada. Por exemplo, veja as capturas de tela a seguir:
Em seguida, você adicionará um espaço em branco à tela inicial para dar mais espaço. Então, você ajustará ainda mais o layout para melhorar o espaçamento.
Una um widget com um objeto Padding
para adicionar espaços em branco ao redor dele. Aumente todos os valores de preenchimento atuais no lib/src/features/home/view/home_screen.dart
para 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,
),
),
],
),
),
],
),
),
),
],
),
),
);
Faça uma recarga dinâmica do app. Ele deverá ser o mesmo de antes, mas com mais espaço em branco entre os widgets. O padding adicional parece melhor, mas o banner de destaque na parte superior ainda está muito perto das bordas.
No lib/src/features/home/view/home_highlight.dart
, mude o padding no banner para 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'),
),
),
),
],
);
}
}
Faça uma recarga dinâmica do app. As duas playlists na parte inferior não têm espaços em branco entre elas, então parecem pertencer à mesma tabela. Esse não é o caso, e você corrigirá o problema em seguida.
Adicione espaços em branco entre as playlists inserindo um widget de tamanho na Row
que as contém. No lib/src/features/home/view/home_screen.dart
, adicione um SizedBox
com uma largura 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,
),
],
),
),
],
),
),
Faça uma recarga dinâmica do app. O app vai ficar assim:
Agora, há espaço suficiente para o conteúdo da tela inicial, mas tudo parece muito separado e não há coesão entre as seções.
Até agora, você definiu todo o padding (horizontal e vertical) dos widgets na tela inicial como 35 com EdgeInsets.all(35)
, mas também pode definir o padding para cada uma das bordas de maneira independente. Personalize o padding para caber melhor no espaço.
EdgeInsets.LTRB()
define individualmente as teclas para a esquerda, para cima, para a direita e para baixoEdgeInsets.symmetric()
define o padding para que a vertical (superior e inferior) e horizontal (esquerda e direita) sejam equivalentesEdgeInsets.only()
define apenas as bordas especificadas.
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
No lib/src/features/home/view/home_highlight.dart
, defina o padding esquerdo e direito no banner como 35 e os padding superior e inferior como 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'),
),
),
),
],
);
}
}
Faça uma recarga dinâmica do app. O layout e o espaçamento são muito melhores. Para o toque final, adicione movimento e animação.
Problemas?
Caso seu app não esteja sendo executado corretamente, use o código do link a seguir para colocar tudo de volta nos eixos.
- lib/src/features/home/view/home_screen.dart (link em inglês)
- lib/src/features/home/view/home_highlight.dart (link em inglês)
7. Adicionar movimento e animação
Movimentos e animações são ótimas maneiras de dar um toque dinâmico e energia, além de fornecer feedback quando o usuário interage com o app.
Animar entre telas
O ThemeProvider
define uma PageTransitionsTheme
com animações de transição de tela para plataformas móveis (iOS, Android). Os usuários de computador já recebem feedback do clique do mouse ou do trackpad, portanto, uma animação de transição de página não é necessária.
O Flutter oferece as animações de transição de tela que podem ser configuradas pelo app com base na plataforma de destino, conforme mostrado em 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(),
},
);
Transmita o PageTransitionsTheme
para os temas claro e escuro em 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,
);
}
Sem animação no iOS | Com animação no iOS |
Problemas?
Caso seu app não esteja sendo executado corretamente, use o código do link a seguir para colocar tudo de volta nos eixos.
- lib/src/shared/providers/theme.dart (link em inglês)
Adicionar estados ao passar o cursor
Uma forma de adicionar movimento a um app para computador é usando estados ao passar o cursor, em que um widget muda o estado (como cor, forma ou conteúdo) quando o usuário passa o cursor sobre ele.
Por padrão, a classe _OutlinedCardState
(usada para os blocos de playlist "tocados recentemente") retorna um MouseRegion
, que transforma a seta do cursor em um ponteiro ao passar o cursor, mas você pode adicionar mais feedback visual.
Abra lib/src/shared/views/outlined_card.dart e substitua seu conteúdo pela implementação a seguir para introduzir um 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,
),
),
);
}
}
Faça uma recarga dinâmica do app e passe o cursor sobre um dos blocos de playlist tocados recentemente.
A OutlinedCard
muda a opacidade e arredonda os cantos.
Por fim, anime o número da música em uma playlist em um botão de reprodução usando o widget HoverableSongPlayButton
definido em lib/src/shared/views/hoverable_song_play_button.dart
. Em lib/src/features/playlists/view/playlist_songs.dart
, envolva o widget Center
(que contém o número da música) com um 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
Faça uma recarga dinâmica do app e depois passe o cursor sobre o número da música na playlist Top músicas de hoje ou na playlist Lançamentos.
O número será animado em um botão Tocar que tocará a música quando você clicar nele.
Veja o código do projeto final no GitHub.
8. Parabéns!
Você concluiu este codelab. Você aprendeu que há pequenas mudanças que podem ser integradas a um app para deixá-lo mais bonito, acessível, localizável e adequado para várias plataformas. Essas técnicas incluem, entre outras:
- Tipografia: o texto é mais do que apenas uma ferramenta de comunicação. Use o modo como o texto é exibido para produzir um efeito positivo na experiência dos usuários e na percepção do seu app.
- Temas: estabeleça um sistema de design que possa ser usado de maneira confiável, sem precisar tomar decisões de design para todos os widgets.
- Adaptabilidade: considere o dispositivo e a plataforma em que o usuário está executando o app e os recursos dele. Considere o tamanho da tela e como seu app é exibido.
- Movimento e animação: adicionar movimento ao app agrega energia à experiência do usuário e, na prática, fornece feedback para os usuários.
Com alguns pequenos ajustes, seu app pode passar de chato para incrível:
Antes | Depois |
Próximas etapas
Esperamos que você tenha aprendido mais sobre a criação de apps bonitos no Flutter.
Se você usa qualquer uma das dicas ou sugestões mencionadas aqui (ou tem uma dica para compartilhar), adoraríamos saber sua opinião. Entre em contato conosco no Twitter em @rodydavis e @khanhnwin.
Os seguintes recursos também podem ser úteis.
Temas
- Material Theme Builder (ferramenta)
Recursos adaptáveis e responsivos:
- Como decodificar o Flutter em recursos adaptáveis e responsivos (vídeo em inglês)
- Layouts adaptáveis (vídeo do The Boring Flutter Development Show)
- Como criar apps responsivos e adaptáveis (flutter.dev)
- Componentes do Material Design para Flutter (biblioteca no GitHub)
- Cinco coisas que você pode fazer para preparar seu app para telas grandes (vídeo do Google I/O 2021)
Recursos gerais de design:
- As pequenas coisas: se tornar o designer-desenvolvedor místico (vídeo do Flutter Engage)
- Material Design 3 para dispositivos dobráveis (material.io)
Além disso, conecte-se à comunidade do Flutter (em inglês).
Vá em frente e deixe o app mais bonito!