Deixe seu app do Flutter lindo, não chato

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:

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:

4a0f6509a18aaf30.gif 1557a5d9dab19d75.gif

O que você quer aprender com este codelab?

Ainda não conheço bem o assunto e quero ter uma boa visão geral. Conheço um pouco sobre esse assunto, mas quero me atualizar. Estou procurando exemplos de código para usar no meu projeto. Estou procurando uma explicação de algo específico.

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.

a3c16fc17be25f6c.png Executar o app.

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.

9ebe486bc7dfa36b.png 1b30e16df3cde215.png.

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:

86c5f73b3aa5fd35.png

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.

a3c16fc17be25f6c.png 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',
  ),
];

23278e4f4610fbf4.png

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.

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. 34bf54e4cad90101.png 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.

a3c16fc17be25f6c.png 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>

a3c16fc17be25f6c.png Em lib/src/shared/extensions.dart, importe o novo pacote:

import 'package:google_fonts/google_fonts.dart';  // Add this line.

a3c16fc17be25f6c.png Defina o Montserrat TextTheme:

TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line

a3c16fc17be25f6c.png Faça uma recarga dinâmica de 7f9a9e103c7b5e5.png 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.

ff6f09f4cc39c21e.png

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.

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);
 }
}

a3c16fc17be25f6c.pngPara 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.

a3c16fc17be25f6c.pngPara 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.

a6201933c4be275c.gif

a3c16fc17be25f6c.pngTransmita 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;

a3c16fc17be25f6c.pngPara 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:

7b51703ed96196a4.png.

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.

a3c16fc17be25f6c.png 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.

60ad6e64df0c5957.gif

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.

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

a3c16fc17be25f6c.png 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.
      },
    );
  }
}

d1825d6f0d23314.png

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,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );

a3c16fc17be25f6c.pngUm 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,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
     },
   );
 }
}

2e4115a01d76e7ae.png

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.

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:

f50d2fe899e57e42.png

cdf5a34a7658a15e.png

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.

a3c16fc17be25f6c.png 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,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );

a3c16fc17be25f6c.png 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.

a3c16fc17be25f6c.png 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'),
            ),
          ),
        ),
      ],
    );
  }
}

a3c16fc17be25f6c.png 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.

df1d9af97d039cc8.png

a3c16fc17be25f6c.png 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,
            ),
          ],
        ),
      ),
    ],
  ),
),

a3c16fc17be25f6c.png Faça uma recarga dinâmica do app. O app vai ficar assim:

89411cc17daf641b.png

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.

a3c16fc17be25f6c.png 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 baixo
  • EdgeInsets.symmetric() define o padding para que a vertical (superior e inferior) e horizontal (esquerda e direita) sejam equivalentes
  • EdgeInsets.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,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );

a3c16fc17be25f6c.png 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'),
            ),
          ),
        ),
      ],
    );
  }
}

a3c16fc17be25f6c.png 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.

2776abfa6ca738af.png

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.

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(),
  },
);

a3c16fc17be25f6c.png 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.

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.

a3c16fc17be25f6c.png 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,
        ),
      ),
    );
  }
}

a3c16fc17be25f6c.png Faça uma recarga dinâmica do app e passe o cursor sobre um dos blocos de playlist tocados recentemente.

61c08e46a5926e10.gif

A OutlinedCard muda a opacidade e arredonda os cantos.

a3c16fc17be25f6c.png 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

a3c16fc17be25f6c.png 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.

82587ceb5452eedf.gif

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

Recursos adaptáveis e responsivos:

Recursos gerais de design:

Além disso, conecte-se à comunidade do Flutter (em inglês).

Vá em frente e deixe o app mais bonito!