Rendi la tua app Flutter più noiosa a bella

1. Introduzione

Flutter è il toolkit dell'interfaccia utente di Google che consente di creare fantastiche applicazioni compilate in modo nativo per dispositivi mobili, web e computer a partire da un unico codebase. Flutter funziona con codice esistente, è utilizzato da sviluppatori e organizzazioni di tutto il mondo ed è senza costi e open source.

In questo codelab, migliorerai un'applicazione di musica Flutter, trasformandola da noiosa a meravigliosa. A questo scopo, questo codelab utilizza strumenti e API introdotti in Material 3.

Obiettivi didattici

  • Come scrivere un'app Flutter che sia fruibile e accattivante su più piattaforme.
  • Come progettare il testo nella tua app per assicurarti che migliori l'esperienza utente.
  • Come scegliere i colori giusti, personalizzare i widget, creare il tuo tema e implementare la modalità Buio in modo facile e veloce.
  • Come creare app adattive multipiattaforma.
  • Come creare app che abbiano un bell'aspetto su qualsiasi schermo.
  • Come aggiungere movimento all'app Flutter per renderla più straordinaria.

Prerequisiti:

Questo codelab presuppone che tu abbia una certa esperienza con Flutter. In caso contrario, ti consigliamo di apprendere prima le nozioni di base. I seguenti link sono utili:

Cosa creerai

Questo codelab ti guida nella creazione della schermata Home per un'applicazione chiamata MyArtist, un'app di music player in cui i fan possono tenersi aggiornati con i loro artisti preferiti. Descrive come modificare il design dell'app per renderla più accattivante su tutte le piattaforme.

I seguenti video mostrano come funziona l'app al completamento di questo codelab:

Cosa ti piacerebbe imparare da questo codelab?

Non ho mai affrontato questo argomento e vorrei una panoramica completa. So qualcosa su questo argomento, ma vorrei rinfrescarti un po'. Sto cercando un codice di esempio da utilizzare nel mio progetto. Vorrei avere una spiegazione su qualcosa di specifico.

2. Configura l'ambiente di sviluppo di Flutter

Per completare questo lab sono necessari due software: l'SDK Flutter e l'editor.

Puoi eseguire il codelab utilizzando uno di questi dispositivi:

  • Un dispositivo fisico Android o iOS connesso al computer e impostato sulla modalità sviluppatore.
  • Il simulatore iOS (richiede l'installazione degli strumenti Xcode).
  • L'emulatore Android (richiede la configurazione in Android Studio).
  • Un browser (per il debug è richiesto Chrome).
  • Come applicazione desktop Windows, Linux o macOS. Devi svilupparle sulla piattaforma in cui prevedi di eseguire il deployment. Quindi, se vuoi sviluppare un'app desktop per Windows, devi sviluppare su Windows per accedere alla catena di build appropriata. Alcuni requisiti specifici del sistema operativo sono descritti in dettaglio all'indirizzo docs.flutter.dev/desktop.

3. Scarica l'app iniziale del codelab

Clona da GitHub

Per clonare questo codelab da GitHub, esegui questi comandi:

git clone https://github.com/flutter/codelabs.git
cd codelabs/boring_to_beautiful/step_01/

Per assicurarti che tutto funzioni correttamente, esegui l'applicazione Flutter come applicazione desktop come mostrato di seguito. In alternativa, apri questo progetto nel tuo IDE e utilizza i relativi strumenti per eseguire l'applicazione.

a3c16fc17be25f6c.png Esegui l'app.

Operazione riuscita. Deve essere in esecuzione il codice di avvio per la schermata Home di MyArtist. Dovresti visualizzare la schermata Home di MyArtist. Viene visualizzato bene sui computer, ma sui dispositivi mobili... Non ideale. Per una cosa, non rispetta il livello. Non preoccuparti, lo risolverai!

1e67c60667821082.png d1139cde225de452.png

Esplora il codice

Quindi, fai un tour del codice.

Apri lib/src/features/home/view/home_screen.dart, che contiene quanto segue:

lib/src/features/home/view/home_screen.dart

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({super.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,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

Questo file importa material.dart e implementa un widget stateful utilizzando due classi:

  • La dichiarazione import rende disponibili i componenti Material.
  • La classe HomeScreen rappresenta l'intera pagina visualizzata.
  • Il metodo build() della classe _HomeScreenState crea la radice della struttura ad albero dei widget, che influisce sul modo in cui vengono creati tutti i widget nella UI.

4. Sfrutta la tipografia

Il testo è ovunque. Il testo è un modo utile per comunicare con l'utente. La tua app è pensata per essere amichevole e divertente oppure forse affidabile e professionale? Esiste un motivo per cui la tua app di online preferita non usa Comic Sans. Il modo in cui viene presentato il testo determina la prima impressione che l'utente ha della tua app. Ecco alcuni modi per utilizzare il testo in modo più ponderato.

Usa le immagini, non le parole

Dove possibile, "mostra" invece di "tell". Ad esempio, NavigationRail nell'app iniziale presenta schede per ogni percorso principale, ma le icone iniziali sono identiche:

86c5f73b3aa5fd35.png

Non è utile perché l'utente deve comunque leggere il testo di ogni scheda. Inizia aggiungendo segnali visivi in modo che l'utente possa dare rapidamente un'occhiata alle icone iniziali per trovare la scheda desiderata. Questo favorisce anche la localizzazione e l'accessibilità.

a3c16fc17be25f6c.png In lib/src/shared/router.dart, aggiungi icone iniziali distinte per ogni destinazione di navigazione (casa, playlist e persone):

lib/src/shared/router.dart

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

Problemi?

Se l'app non viene eseguita correttamente, cerca gli errori di battitura. Se necessario, utilizza il codice ai seguenti link per tornare in pista.

Scegli i caratteri con cura

I caratteri determinano la personalità della tua applicazione, pertanto è fondamentale scegliere il carattere giusto. Quando selezioni un carattere, tieni in considerazione i seguenti aspetti:

  • Sans Serif o Serif: i caratteri Serif hanno tratti o "code" decorativi. in fondo alle lettere e sono percepiti come più formali. I caratteri Sans Serif non presentano tratti decorativi e tendono a essere percepiti come più informali. 34bf54e4cad90101.png: T maiuscola per Sans Serif e T maiuscola per Serif
  • Caratteri Tutto maiuscolo: l'uso di tutte le lettere maiuscole è appropriato per richiamare l'attenzione su piccole quantità di testo (ad esempio per i titoli), ma se viene fatto un uso eccessivo, si può percepire come un grido che spinge l'utente a ignorarlo completamente.
  • Iniziale maiuscola o maiuscola: quando aggiungi i titoli o le etichette, considera le modalità di utilizzo delle lettere maiuscole. Inizia con l'iniziale maiuscola, in cui la prima lettera di ogni parola è maiuscola ("This Is a Title Case Title"), è più formale. L'uso della maiuscola a inizio frase, che utilizza la maiuscola solo per i nomi propri e la prima parola del testo ("Questo è il titolo di una frase"), è più colloquiale e informale.
  • Kerning (spaziatura tra ogni lettera), lunghezza della riga (larghezza del testo completo sullo schermo) e altezza della riga (l'altezza di ogni riga di testo): una quantità eccessiva o insufficiente di questi elementi rende la tua app meno leggibile. Ad esempio, è facile perdere il punto in cui si trova la lettura di un blocco di testo di grandi dimensioni e ininterrotto.

Tenendo conto di questo, vai su Google Fonts e scegli un font Sans- Serif, come Montserrat, poiché l'app Music è pensata per essere giocosa e divertente.

a3c16fc17be25f6c.png Dalla riga di comando, esegui il pull del pacchetto google_fonts. Inoltre, il file pubspec viene aggiornato per aggiungere i caratteri come dipendenza dall'app.

$ flutter pub add google_fonts

macos/Runner/DebugProfile.entitlements

<?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>
        <true/>
        <!-- To here. -->
        <key>com.apple.security.network.server</key>
        <true/>
</dict>
</plist>

a3c16fc17be25f6c.png In lib/src/shared/extensions.dart, importa il nuovo pacchetto:

lib/src/shared/extensions.dart

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

a3c16fc17be25f6c.png Imposta Montserrat TextTheme:

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

a3c16fc17be25f6c.png Ricarica a caldo 7f9a9e103c7b5e5.png per attivare le modifiche. (Utilizza il pulsante nel tuo IDE o, dalla riga di comando, inserisci r per eseguire il ricaricamento a caldo).

1e67c60667821082.png

Dovresti vedere le nuove icone NavigationRail insieme al testo visualizzato nel carattere Montserrat.

Problemi?

Se l'app non viene eseguita correttamente, cerca gli errori di battitura. Se necessario, utilizza il codice ai seguenti link per tornare in pista.

5. Impostare il tema

I temi aiutano a conferire un design strutturato e un'uniformità a un'app specificando un sistema prestabilito di colori e stili di testo. I temi consentono di implementare rapidamente un'interfaccia utente senza doversi preoccupare di dettagli minori come specificare il colore esatto per ogni singolo widget.

Gli sviluppatori di Flutter in genere creano componenti a tema personalizzato in due modi:

  • Crea singoli widget personalizzati, ciascuno con il proprio tema.
  • Crea temi con ambito per i widget predefiniti.

In questo esempio viene utilizzato un fornitore di temi che si trova in lib/src/shared/providers/theme.dart per creare widget e colori coerenti in tutta l'app:

lib/src/shared/providers/theme.dart

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(
     {super.key,
     required this.settings,
     required this.lightDynamic,
     required this.darkDynamic,
     required super.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.surfaceContainerHighest,
     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.pngPer utilizzare il provider, crea un'istanza e passala all'oggetto tema con ambito in MaterialApp, che si trova in lib/src/shared/app.dart. Verrà ereditato da tutti gli oggetti Theme nidificati:

lib/src/shared/app.dart

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

Ora che il tema è configurato, scegli i colori per l'applicazione.

Scegliere il set di colori giusto non è sempre facile. Potresti avere un'idea del colore principale, ma è probabile che tu voglia che l'app abbia più di un colore. Di che colore deve essere il testo? Titolo? Contenuti? Link? E il colore dello sfondo? Material Theme Builder è uno strumento basato sul web (introdotto in Material 3) che ti aiuta a selezionare un insieme di colori complementari per la tua app.

a3c16fc17be25f6c.pngPer scegliere un colore di origine per l'applicazione, apri lo Strumento per la creazione di temi Material ed esplora i diversi colori dell'interfaccia utente. È importante scegliere un colore adatto all'estetica del brand e/o alle tue preferenze personali.

Dopo aver creato un tema, fai clic con il tasto destro del mouse sul fumetto del colore Principale: si apre una finestra di dialogo contenente il valore esadecimale del colore principale. Copia questo valore. Puoi anche impostare il colore utilizzando questa finestra di dialogo.

a3c16fc17be25f6c.pngTrasmetti il valore esadecimale del colore principale al fornitore del tema. Ad esempio, il colore esadecimale #00cbe6 è specificato come Color(0xff00cbe6). ThemeProvider genera un valore ThemeData contenente l'insieme di colori complementari che hai visualizzato in anteprima nel generatore di temi Materiali:

final settings = ValueNotifier(ThemeSettings(
   sourceColor:  Color(0xff00cbe6), // Replace this color
   themeMode: ThemeMode.system,
 ));

Riavvia l'app a caldo. Una volta impostato il colore principale, l'app inizia a essere più espressiva. Accedi a tutti i nuovi colori facendo riferimento al tema nel contesto e scegliendo ColorScheme:

final colors = Theme.of(context).colorScheme;

a3c16fc17be25f6c.pngPer usare un determinato colore, accedi a un ruolo Colore su colorScheme. Vai a lib/src/shared/views/outlined_card.dart e assegna un bordo a OutlinedCard:

lib/src/shared/views/outlined_card.dart

class _OutlinedCardState extends State<OutlinedCard> {
  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      cursor: widget.clickable
          ? SystemMouseCursors.click
          : SystemMouseCursors.basic,
      child: Container(
        child: widget.child,
        // Add from here...
        decoration: BoxDecoration(
          border: Border.all(
            color: Theme.of(context).colorScheme.outline,
            width: 1,
          ),
        ),
        // ... To here.
      ),
    );
  }
}

Material 3 introduce ruoli per i colori sfumati che si completano a vicenda e possono essere utilizzati nell'interfaccia utente per aggiungere nuovi livelli di espressione. Questi nuovi ruoli relativi ai colori includono:

  • Primary, OnPrimary, PrimaryContainer e OnPrimaryContainer
  • Secondary, OnSecondary, SecondaryContainer e OnSecondaryContainer
  • Tertiary, OnTertiary, TertiaryContainer e OnTertiaryContainer
  • Error, OnError, ErrorContainer e OnErrorContainer
  • Background, OnBackground
  • Surface, OnSurface, SurfaceVariant e OnSurfaceVariant
  • Shadow, Outline e InversePrimary

Inoltre, i nuovi token di design supportano sia il tema chiaro che quello scuro:

7b51703ed96196a4.png

Questi ruoli colore possono essere utilizzati per assegnare un significato ed enfasi a diverse parti dell'interfaccia utente. Anche se un componente non è in evidenza, può sfruttare il colore dinamico.

a3c16fc17be25f6c.png L'utente può impostare la luminosità dell'app nelle impostazioni di sistema del dispositivo. In lib/src/shared/app.dart, quando il dispositivo è impostato sulla modalità Buio, restituisci le modalità tema e tema scuro a MaterialApp.

lib/src/shared/app.dart

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

Fai clic sull'icona della luna nell'angolo in alto a destra per attivare la modalità Buio.

Problemi?

Se la tua app non viene eseguita correttamente, utilizza il codice al link riportato di seguito per tornare in pista.

6. Aggiungi il design adattivo

Con Flutter puoi creare app eseguibili praticamente ovunque, ma questo non significa che tutte le app dovrebbero comportarsi allo stesso modo ovunque. Gli utenti si aspettano comportamenti e funzionalità diversi da piattaforme diverse.

Material offre pacchetti per semplificare l'utilizzo dei layout adattivi: puoi trovare questi pacchetti Flutter su GitHub.

Quando crei un'applicazione adattiva multipiattaforma, tieni presente le seguenti differenze:

  • Metodo di inserimento: mouse, touch o gamepad
  • Dimensioni del carattere, orientamento del dispositivo e distanza di visualizzazione
  • Dimensioni schermo e fattore di forma: smartphone, tablet, pieghevoli, desktop, web

a3c16fc17be25f6c.png Il file lib/src/shared/views/adaptive_navigation.dart contiene una classe di navigazione in cui puoi fornire un elenco di destinazioni e contenuti per il rendering del corpo. Dato che usi questo layout su più schermi, esiste un layout di base condiviso da trasmettere a ogni account secondario. I binari di navigazione sono ideali per schermi di computer e grandi, ma ottimizza il layout per i dispositivi mobili mostrando invece una barra di navigazione inferiore sui dispositivi mobili.

lib/src/shared/views/adaptive_navigation.dart

import 'package:flutter/material.dart';

class AdaptiveNavigation extends StatelessWidget {
  const AdaptiveNavigation({
    super.key,
    required this.destinations,
    required this.selectedIndex,
    required this.onDestinationSelected,
    required super.child,
  });

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

a8487a3c4d7890c9.png

Non tutti gli schermi sono delle stesse dimensioni. Se provassi a visualizzare sullo smartphone la versione desktop della tua app, dovresti fare una combinazione di strizzare gli occhi e aumentare lo zoom per vedere tutto. Vuoi che l'app modifichi l'aspetto in base alla schermata in cui viene visualizzata. Grazie al responsive design, ti assicuri che l'app venga visualizzata al meglio su schermi di tutte le dimensioni.

Per rendere reattiva la tua app, introduci alcuni punti di interruzione adattivi (da non confondere con i punti di interruzione di debug). Questi punti di interruzione specificano le dimensioni dello schermo su cui l'app deve modificare il layout.

Gli schermi più piccoli non possono essere visualizzati tanto quanto quelli più grandi senza che i contenuti vengano ridotti. Per evitare che l'app abbia l'aspetto di un'app desktop ridotta, crea un layout separato per i dispositivi mobili che utilizzi le schede per suddividere i contenuti. Questo conferisce all'app un aspetto più nativo sui dispositivi mobili.

I seguenti metodi di estensione (definiti nel progetto MyArtist in lib/src/shared/extensions.dart) sono un buon punto di partenza quando progetti layout ottimizzati per target diversi.

lib/src/shared/extensions.dart

extension BreakpointUtils on BoxConstraints {
  bool get isTablet => maxWidth > 730;
  bool get isDesktop => maxWidth > 1200;
  bool get isMobile => !isTablet && !isDesktop;
}

Uno schermo di dimensioni superiori a 730 pixel (nella direzione più lunga), ma inferiore a 1200 pixel, è considerato un tablet. Gli annunci più grandi di 1200 pixel sono considerati desktop. Se un dispositivo non è né un tablet né un computer, viene considerato mobile. Puoi scoprire di più sui punti di interruzione adattivi su material.io. Potresti prendere in considerazione l'utilizzo del pacchetto adaptive_breakpoints.

Il layout reattivo della schermata Home utilizza i valori AdaptiveContainer e AdaptiveColumn basati su una griglia a 12 colonne con i pacchetti adaptive_components e adaptive_breakpoints per implementare un layout a griglia adattabile in 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.pngUn layout adattivo ha bisogno di due layout: uno per i dispositivi mobili e uno per schermi più grandi. Al momento LayoutBuilder restituisce solo un layout desktop. In lib/src/features/home/view/home_screen.dart crea il layout per dispositivi mobili come TabBar e TabBarView con 4 schede.

lib/src/features/home/view/home_screen.dart

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({super.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,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
     },
   );
 }
}

377cfdda63a9de54.png

Problemi?

Se la tua app non viene eseguita correttamente, utilizza il codice al link riportato di seguito per tornare in pista.

Usa uno spazio vuoto

Lo spazio vuoto è un importante strumento visivo per la tua app, che crea un'interruzione organizzativa tra le sezioni.

È meglio avere troppo spazio vuoto che non a sufficienza. È preferibile aggiungere più spazio vuoto, anziché ridurre le dimensioni del carattere o degli elementi visivi per adattarli maggiormente allo spazio.

La mancanza di spazio vuoto può rappresentare un ostacolo per chi ha problemi di vista. Troppo spazio vuoto può causare mancanza di coesione e rendere la tua UI poco organizzata. Ad esempio, guarda i seguenti screenshot:

7f5e3514a7ee1750.png

d5144a50f5b4142c.png

Successivamente, aggiungerai spazio vuoto alla schermata Home per concedergli più spazio. Successivamente modificherai ulteriormente il layout per regolare con maggiore precisione la spaziatura.

a3c16fc17be25f6c.png Aggrega un widget con un oggetto Padding per aggiungere uno spazio vuoto attorno al widget. Aumenta tutti i valori di spaziatura interna attualmente in lib/src/features/home/view/home_screen.dart a 35:

lib/src/features/home/view/home_screen.dart

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 Ricarica a caldo l'app. Dovrebbe essere lo stesso di prima, ma con più spazio vuoto tra i widget. La spaziatura interna aggiuntiva ha un aspetto migliore, ma il banner di evidenziazione nella parte superiore è ancora troppo vicino ai bordi.

a3c16fc17be25f6c.png In lib/src/features/home/view/home_highlight.dart, modifica la spaziatura interna nel banner impostandola su 35:

lib/src/features/home/view/home_highlight.dart

class HomeHighlight extends StatelessWidget {
  const HomeHighlight({super.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 Ricarica a caldo l'app. Le due playlist in basso non hanno spazi vuoti, quindi sembrano appartenere alla stessa tabella. Non è così e lo correggerai successivamente.

df1d9af97d039cc8.png

a3c16fc17be25f6c.png Aggiungi uno spazio vuoto tra le playlist inserendo un widget di dimensione nel file Row che le contiene. In lib/src/features/home/view/home_screen.dart, aggiungi un elemento SizedBox con una larghezza di 35:

lib/src/features/home/view/home_screen.dart

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 Ricarica a caldo l'app. L'app dovrebbe avere il seguente aspetto:

d8b2a3d47736dbab.png

Ora c'è molto spazio per i contenuti della schermata Home, ma tutto sembra troppo separato e non c'è coesione tra le sezioni.

a3c16fc17be25f6c.png Finora, hai impostato su 35 la spaziatura interna (orizzontale e verticale) per i widget sulla schermata Home con EdgeInsets.all(35), ma puoi anche impostare la spaziatura interna per ciascuno dei bordi in modo indipendente. Personalizza la spaziatura interna per adattarla meglio allo spazio.

  • EdgeInsets.LTRB() imposta sinistra, alto, destra e in basso singolarmente
  • EdgeInsets.symmetric() imposta la spaziatura interna per verticale (superiore e inferiore) e orizzontale (sinistra e destra) come equivalente
  • EdgeInsets.only() imposta solo i bordi specificati.
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 In lib/src/features/home/view/home_highlight.dart, imposta la spaziatura interna destra e sinistra sul banner su 35 e la spaziatura interna superiore e inferiore su 5:

lib/src/features/home/view/home_highlight.dart

class HomeHighlight extends StatelessWidget {
  const HomeHighlight({super.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 Ricarica a caldo l'app. Il layout e la spaziatura hanno un aspetto molto migliore. Per il tocco finale, aggiungi un po' di movimento e animazione.

7f5e3514a7ee1750.png

Problemi?

Se la tua app non viene eseguita correttamente, utilizza il codice al link riportato di seguito per tornare in pista.

7. Aggiungi movimento e animazione

Movimento e animazione sono ottimi modi per introdurre movimento ed energia e per fornire feedback quando l'utente interagisce con l'app.

Crea animazioni tra le schermate

L'ThemeProvider definisce un PageTransitionsTheme con animazioni di transizione dello schermo per le piattaforme mobile (iOS, Android). Gli utenti di computer desktop ricevono già un feedback dal clic del mouse o del trackpad, quindi non è necessaria un'animazione di transizione di pagina.

Flutter fornisce le animazioni di transizione dello schermo che puoi configurare per la tua app in base alla piattaforma di destinazione, come illustrato in lib/src/shared/providers/theme.dart:

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 Passa il PageTransitionsTheme al tema chiaro e al tema scuro in lib/src/shared/providers/theme.dart

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

Senza animazione su iOS

Con animazione su iOS

Problemi?

Se la tua app non viene eseguita correttamente, utilizza il codice al link riportato di seguito per tornare in pista.

Aggiungi stati al passaggio del mouse

Un modo per aggiungere movimento a un'app desktop è gli stati al passaggio del mouse, in cui un widget cambia il suo stato (ad esempio colore, forma o contenuti) quando l'utente passa il cursore del mouse su di esso.

Per impostazione predefinita, la classe _OutlinedCardState (utilizzata per i riquadri della playlist "Riproduzione di recente") restituisce un MouseRegion, che trasforma la freccia del cursore in un puntatore al passaggio del mouse, ma puoi aggiungere ulteriore feedback visivo.

a3c16fc17be25f6c.png Apri lib/src/shared/views/outlined_card.dart e sostituisci i relativi contenuti con la seguente implementazione per introdurre uno stato _hovered.

lib/src/shared/views/outlined_card.dart

import 'package:flutter/material.dart';

class OutlinedCard extends StatefulWidget {
  const OutlinedCard({
    super.key,
    required this.child,
    this.clickable = true,
  });
  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 Ricarica a caldo l'app e poi passa il mouse sopra uno dei riquadri delle playlist riprodotti di recente.

L'icona OutlinedCard cambia opacità e arrotonda gli angoli.

a3c16fc17be25f6c.png Infine, anima il numero del brano di una playlist in un pulsante di riproduzione utilizzando il widget HoverableSongPlayButton definito in lib/src/shared/views/hoverable_song_play_button.dart. In lib/src/features/playlists/view/playlist_songs.dart, racchiudi il widget Center (che contiene il numero del brano) con un HoverableSongPlayButton:

lib/src/features/playlists/view/playlist_songs.dart

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.pngRicarica l'app, quindi passa il cursore sopra il numero del brano nella playlist Brani più ascoltati di oggi o Nuove uscite.

Il numero si anima in un pulsante riproduci che riproduce il brano quando fai clic su quest'ultimo.

Visualizza il codice finale del progetto su GitHub.

8. Complimenti!

Hai completato il codelab. Hai imparato che ci sono molte piccole modifiche che puoi integrare in un'app per renderla più accattivante, e anche più accessibile, più localizzabile e più adatta a più piattaforme. Queste tecniche includono, a titolo esemplificativo:

  • Tipografia: il testo è molto più di uno strumento di comunicazione. Usa il modo in cui il testo viene visualizzato per produrre un effetto positivo sugli utenti l'esperienza e la percezione che hai avuto della tua app.
  • Temi: stabilisci un sistema di progettazione che puoi utilizzare in modo affidabile senza dover prendere decisioni di progettazione per ogni widget.
  • Adattabilità: considera il dispositivo, la piattaforma su cui l'utente esegue la tua app e le sue funzionalità. Tieni conto delle dimensioni dello schermo e di come viene visualizzata l'app.
  • Movimento e animazione: l'aggiunta di movimento all'app aggiunge energia all'esperienza utente e, in pratica, fornisce feedback agli utenti.

Con qualche piccolo ritocco, la tua app può diventare bellissima:

Prima

1e67c60667821082.png

Dopo

Passaggi successivi

Ci auguriamo che tu abbia scoperto di più sulla creazione di fantastiche app in Flutter.

Se applichi i suggerimenti o i trucchi menzionati qui (o hai un suggerimento da condividere), ci piacerebbe conoscere la tua opinione. Contattaci su Twitter agli indirizzi @rodydavis e @khanhnwin.

Potrebbero esserti utili anche le risorse che seguono.

Applicazione tema

Risorse adattive e adattabili:

Risorse di progettazione generali:

Inoltre, mettiti in contatto con la community di Flutter.

Vai avanti e rendi più bello il mondo delle app.