Rendi la tua app Flutter più noiosa a bella

Trasforma la tua app Flutter da noiosa a bella

Informazioni su questo codelab

subjectUltimo aggiornamento: giu 24, 2025
account_circleScritto da: The Flutter Team

1. Introduzione

Flutter è il toolkit UI di Google per la creazione di bellissime applicazioni compilate in modo nativo per mobile, web e desktop da un unico codebase. Flutter è compatibile con il codice esistente, viene utilizzato da sviluppatori e organizzazioni di tutto il mondo ed è senza costi e open source.

In questo codelab, migliorerai un'applicazione musicale Flutter, trasformandola da noiosa a bella. Per farlo, questo codelab utilizza gli strumenti e le API introdotti in Material 3.

Obiettivi didattici

  • Come scrivere un'app Flutter utilizzabile e bella su più piattaforme.
  • Come progettare il testo nella tua app per assicurarti che contribuisca all'esperienza utente.
  • Come scegliere i colori giusti, personalizzare i widget, creare il tuo tema e implementare rapidamente la modalità Buio.
  • Come creare app adattive multipiattaforma.
  • Come creare app che appaiano bene su qualsiasi schermo.
  • Come aggiungere movimento alla tua app Flutter per renderla davvero accattivante.

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 di un'applicazione chiamata MyArtist, un'app di lettore musicale in cui i fan possono rimanere al passo con i loro artisti preferiti. Descrive come modificare il design dell'app in modo che sia bello su tutte le piattaforme.

I seguenti video mostrano il funzionamento dell'app al termine di questo codelab:

Cosa vuoi imparare da questo codelab?

2. Configura l'ambiente di sviluppo Flutter

Per completare questo laboratorio, hai bisogno di due software: l'SDK Flutter e un editor.

Puoi eseguire il codelab utilizzando uno di questi dispositivi:

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

3. Ottenere l'app iniziale del codelab

Clonalo da GitHub

Per clonare questo codelab da GitHub, esegui i seguenti comandi:

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

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

flutter run

Operazione riuscita. Il codice iniziale per la schermata Home di MyArtist dovrebbe essere in esecuzione. Dovresti vedere la schermata Home di MyArtist. Sembra a posto su computer, ma su dispositivo mobile… Non male. Ad esempio, non supporta il notch. Non preoccuparti, ce la farai.

1e67c60667821082.pngd1139cde225de452.png

Esplora il codice

A questo punto, dai un'occhiata al codice.

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

lib/src/features/home/view/home_screen.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 '../../../utils/adaptive_components.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) {
       
return Scaffold(
         
body: SingleChildScrollView(
           
child: AdaptiveColumn(
             
children: [
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
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),
                       
child: Text(
                         
'Recently played',
                         
style: context.headlineSmall,
                       
),
                     
),
                     
HomeRecent(playlists: playlists),
                   
],
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
child: Row(
                     
crossAxisAlignment: CrossAxisAlignment.start,
                     
children: [
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'Top Songs Today',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: topSongs,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
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 con stato utilizzando due classi:

  • L'istruzione 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 sulla creazione di tutti i widget nell'interfaccia utente.

4. Sfrutta la tipografia

Il testo è ovunque. Il testo è un modo utile per comunicare con l'utente. La tua app vuole essere amichevole e divertente o forse affidabile e professionale? C'è un motivo per cui la tua app bancaria preferita non utilizza Comic Sans. La modalità di presentazione del testo determina la prima impressione dell'utente sulla tua app. Ecco alcuni modi per utilizzare il testo in modo più ponderato.

Mostra, non raccontare

Se possibile, "mostra" invece di "di'". Ad esempio, il NavigationRail nell'app iniziale ha 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 indicatori visivi in modo che l'utente possa dare un'occhiata rapida alle icone principali per trovare la scheda che preferisce. Ciò aiuta anche con la localizzazione e l'accessibilità.

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 funziona correttamente, cerca errori ortografici. Se necessario, utilizza il codice riportato nei seguenti link per tornare in pista.

Scegli i caratteri con cura

I caratteri definiscono la personalità della tua applicazione, quindi scegliere il carattere giusto è fondamentale. Quando scegli un carattere, tieni presente quanto segue:

  • Sans-serif o serif: i caratteri serif hanno tratti decorativi o "code" alla fine delle lettere e sono percepiti come più formali. I caratteri senza grazie non hanno tratti decorativi e tendono a essere percepiti come più informali. Una T maiuscola senza grazie e una T maiuscola con grazie
  • Caratteri in maiuscolo: l'uso di caratteri in maiuscolo è appropriato per attirare l'attenzione su piccole quantità di testo (ad esempio i titoli), ma se usato eccessivamente può essere percepito come urlato e causare l'ignoranza da parte dell'utente.
  • Maiuscole nel titolo o Maiuscole nella frase: quando aggiungi titoli o etichette, valuta come utilizzare le lettere maiuscole: lo stile Tutti titoli maiuscoli, in cui la prima lettera di ogni parola è maiuscola ("Questo è un titolo con tutte le lettere maiuscole"), è più formale. L'uso della maiuscola a inizio frase, che mette in maiuscolo solo i nomi propri e la prima parola del testo ("Questo è un titolo con l'uso della maiuscola a inizio frase"), è più colloquiale e informale.
  • Kerning (spaziatura tra le lettere), lunghezza riga (larghezza del testo completo sullo schermo) e altezza riga (altezza di ogni riga di testo): se sono troppo grandi o troppo piccoli, rendono la tua app meno leggibile. Ad esempio, può essere difficile mantenere il punto di lettura quando si legge un blocco di testo grande e continuo.

Tenendo presente questo, vai su Google Fonts e scegli un carattere senza grazie, come Montserrat, poiché l'app di musica è pensata per essere giocosa e divertente.

Dalla riga di comando, importa il pacchetto google_fonts. Viene aggiornato anche il file pubspec.yaml per aggiungere i caratteri come dipendenza dell'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" "https://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/>
        <key>com.apple.security.network.server</key>
        <true/>
        <!-- Make sure the following two lines are present -->
        <key>com.apple.security.network.client</key>
        <true/>
</dict>
</plist>

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.

Imposta il TextTheme: di Montserrat

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

Ricarica dinamica 7f9a9e103c7b5e5.png per attivare le modifiche. Utilizza il pulsante nell'IDE o, dalla riga di comando, inserisci r per il ricaricamento dinamico.

1e67c60667821082.png

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

Problemi?

Se l'app non funziona correttamente, cerca errori ortografici. Se necessario, utilizza il codice riportato nei seguenti link per tornare in pista.

5. Impostare il tema

I temi contribuiscono a dare un design strutturato e uniformità a un'app specificando un sistema di colori e stili di testo predefiniti. I temi ti consentono di implementare rapidamente un'interfaccia utente senza doverti preoccupare di dettagli minori come la specifica del colore esatto per ogni singolo widget.

In genere, gli sviluppatori Flutter creano componenti con temi personalizzati in due modi:

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

Questo esempio utilizza un provider di temi in lib/src/shared/providers/theme.dart per creare widget e colori in tema 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.toARGB32(),
       
settings.value.sourceColor.toARGB32(),
     
),
   
);
 
}

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

 
CardThemeData cardTheme() {
   
return CardThemeData(
     
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,
   
);
 
}

 
TabBarThemeData tabBarTheme(ColorScheme colors) {
   
return TabBarThemeData(
     
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 colorScheme = colors(Brightness.light, targetColor);
   
return ThemeData.light().copyWith(
     
colorScheme: colorScheme,
     
appBarTheme: appBarTheme(colorScheme),
     
cardTheme: cardTheme(),
     
listTileTheme: listTileTheme(colorScheme),
     
bottomAppBarTheme: bottomAppBarTheme(colorScheme),
     
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
     
navigationRailTheme: navigationRailTheme(colorScheme),
     
tabBarTheme: tabBarTheme(colorScheme),
     
drawerTheme: drawerTheme(colorScheme),
     
scaffoldBackgroundColor: colorScheme.surface,
   
);
 
}

 
ThemeData dark([Color? targetColor]) {
   
final colorScheme = colors(Brightness.dark, targetColor);
   
return ThemeData.dark().copyWith(
     
colorScheme: colorScheme,
     
appBarTheme: appBarTheme(colorScheme),
     
cardTheme: cardTheme(),
     
listTileTheme: listTileTheme(colorScheme),
     
bottomAppBarTheme: bottomAppBarTheme(colorScheme),
     
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
     
navigationRailTheme: navigationRailTheme(colorScheme),
     
tabBarTheme: tabBarTheme(colorScheme),
     
drawerTheme: drawerTheme(colorScheme),
     
scaffoldBackgroundColor: colorScheme.surface,
   
);
 
}

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

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

Per utilizzare il provider, crea un'istanza e passala all'oggetto tema basato sugli ambiti 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 la giusta combinazione di colori può essere difficile. Potresti avere un'idea del colore principale, ma è probabile che tu voglia che la tua app abbia più di un colore. Di che colore deve essere il testo? Titolo? Contenuti? Link? E il colore di 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.

Per scegliere un colore di origine per l'applicazione, apri Material Theme Builder ed esplora diversi colori per l'interfaccia utente. È importante selezionare un colore che si adatti all'estetica del brand o alle tue preferenze personali.

Dopo aver creato un tema, fai clic con il tasto destro del mouse sulla bolla di colore Principale per aprire una finestra di dialogo contenente il valore esadecimale del colore principale. Copia questo valore. Puoi anche impostare il colore utilizzando questa finestra di dialogo.

Passa il valore esadecimale del colore principale al provider del tema. Ad esempio, il colore esadecimale #00cbe6 è specificato come Color(0xff00cbe6). ThemeProvider genera un ThemeData contenente l'insieme di colori complementari di cui hai visualizzato l'anteprima in Material Theme Builder:

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

Riavvia l'app. Con il colore principale in posizione, l'app inizia a sembrare più espressiva. Accedi a tutti i nuovi colori facendo riferimento al tema nel contesto e acquisendo il ColorScheme:

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

Per utilizzare un determinato colore, accedi a un ruolo di colore su colorScheme. Vai a lib/src/shared/views/outlined_card.dart e applica 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(
        // Add from here...
        decoration: BoxDecoration(
          border: Border.all(
            color: Theme.of(context).colorScheme.outline,
            width: 1,
          ),
        ),
        // ... To here.
        child: widget.child,
      ),
    );
  }
}

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

  • 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

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

7b51703ed96196a4.png

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

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, reimposta un tema scuro e una modalità a tema in 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 a forma di luna nell'angolo in alto a destra per attivare la modalità Buio.

Problemi?

Se la tua app non funziona correttamente, utilizza il codice al seguente link per tornare in pista.

6. Aggiungere un design adattivo

Con Flutter puoi creare app che funzionano quasi ovunque, ma ciò non significa che ogni app debba comportarsi allo stesso modo ovunque. Gli utenti si aspettano comportamenti e funzionalità diversi da piattaforme diverse.

Material offre pacchetti per semplificare il lavoro con i layout adattabili. Puoi trovare questi pacchetti Flutter su GitHub.

Tieni presente le seguenti differenze tra le piattaforme durante la creazione di un'applicazione adattabile multipiattaforma:

  • Metodo di immissione: mouse, tocco o gamepad
  • Dimensioni dei caratteri, orientamento del dispositivo e distanza di visualizzazione
  • Dimensioni dello schermo e fattore di forma: smartphone, tablet, pieghevole, computer, web

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. Poiché utilizzi questo layout su più schermate, esiste un layout di base condiviso da passare a ogni elemento secondario. I riquadri di navigazione sono adatti per computer e schermi di grandi dimensioni, ma rendono il layout ottimizzato per il mobile mostrando una barra di navigazione in basso 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 this.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) {
       
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 tutte le schermate sono uguali. Se provassi a visualizzare la versione desktop della tua app sullo smartphone, dovresti fare una combinazione di sforzo visivo e zoom per vedere tutto. Vuoi che l'aspetto della tua app cambi in base allo schermo su cui viene visualizzata. Con il responsive design, puoi assicurarti che la tua app abbia un aspetto ottimale su schermi di tutte le dimensioni.

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

Gli schermi più piccoli non possono mostrare lo stesso numero di contenuti di quelli più grandi senza rimpicciolire i contenuti. Per evitare che l'app assomigli a un'app desktop rimpicciolita, crea un layout separato per il mobile che utilizzi le schede per suddividere i contenuti. In questo modo, l'app avrà 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 per progettare 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 più grande di 730 pixel (nel senso più lungo), ma più piccolo di 1200 pixel, è considerato un tablet. Qualsiasi dimensione superiore a 1200 pixel è considerata un computer. Se un dispositivo non è né un tablet né un computer, è considerato mobile. Scopri di più sulle interruzioni adattive su material.io.

Il layout adattabile della schermata Home utilizza AdaptiveContainer e AdaptiveColumn in base alla griglia a 12 colonne.

Un layout adattabile richiede due layout: uno per il mobile e uno adattabile per gli schermi più grandi. A questo punto, LayoutBuilder restituisce un layout desktop. In lib/src/features/home/view/home_screen.dart, crea il layout mobile come TabBar e TabBarView con 4 schede.

lib/src/features/home/view/home_screen.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 '../../../utils/adaptive_components.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.all(2),
                   
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),
                       
child: Text(
                         
'Recently played',
                         
style: context.headlineSmall,
                       
),
                     
),
                     
HomeRecent(playlists: playlists),
                   
],
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
child: Row(
                     
crossAxisAlignment: CrossAxisAlignment.start,
                     
children: [
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'Top Songs Today',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: topSongs,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'New Releases',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: newReleases,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                     
],
                   
),
                 
),
               
),
             
],
           
),
         
),
       
);
     
},
   
);
 
}
}

377cfdda63a9de54.png

Problemi?

Se la tua app non funziona correttamente, utilizza il codice al seguente link per tornare in pista.

7. Utilizza gli spazi vuoti

Gli spazi bianchi sono uno strumento visivo importante per la tua app, in quanto creano una separazione organizzativa tra le sezioni.

È meglio avere troppo spazio vuoto che non abbastanza. È preferibile aggiungere spazi bianchi rispetto a ridurre le dimensioni dei caratteri o degli elementi visivi per adattarli allo spazio.

La mancanza di spazi vuoti può essere un problema per chi ha problemi di vista. Troppi spazi vuoti possono mancare di coesione e rendere l'interfaccia utente poco organizzata. Ad esempio, guarda gli screenshot seguenti:

7f5e3514a7ee1750.png

d5144a50f5b4142c.png

Aggiungi spazi vuoti alla schermata Home per creare più spazio. Poi perfezionerai ulteriormente il layout per ottimizzare la spaziatura.

Inserisci un a capo in un widget con un oggetto Padding per aggiungere spazi vuoti intorno al widget. Aumenta tutti i valori di spaziatura in lib/src/features/home/view/home_screen.dart a 35:

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

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

Esegui il ricaricamento dell'app. L'app dovrebbe avere lo stesso aspetto di prima, ma con più spazi tra i widget. Il padding aggiuntivo ha un aspetto migliore, ma il banner in evidenza in alto è ancora troppo vicino ai bordi.

In lib/src/features/home/view/home_highlight.dart, imposta il padding del banner su 15:

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(15),                   // 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: () => launchUrl(Uri.parse('https://docs.flutter.dev')),
            ),
          ),
        ),
      ],
    );
  }
}

Esegui il ricaricamento dell'app. Le due playlist in basso non sono separate da spazi vuoti, quindi sembrano appartenere alla stessa tabella. Non è così e lo correggerai nel passaggio successivo.

df1d9af97d039cc8.png

Aggiungi spazi tra le playlist inserendo un widget delle dimensioni nell'Row che le contiene. In lib/src/features/home/view/home_screen.dart, aggiungi un SizedBox con una larghezza di 35:

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

AdaptiveContainer(
  columnSpan: 12,
  child: 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,
                ),
              ),
              LayoutBuilder(
                builder: (context, constraints) =>
                    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,
                ),
              ),
              LayoutBuilder(
                builder: (context, constraints) =>
                    PlaylistSongs(
                      playlist: newReleases,
                      constraints: constraints,
                    ),
              ),
            ],
          ),
        ),
      ],
    ),
  ),
),

Esegui il ricaricamento dell'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.

Finora hai impostato tutti i margini (orizzontali e verticali) per i widget nella schermata Home su 35 con EdgeInsets.all(35), ma puoi impostare anche i margini per ciascun lato in modo indipendente. Personalizza il padding per adattarlo meglio allo spazio.

  • EdgeInsets.LTRB() imposta i valori sinistro, superiore, destro e inferiore singolarmente
  • EdgeInsets.symmetric() imposta la spaziatura interna per la parte verticale (sopra e sotto) e per quella orizzontale (a sinistra e a destra) in modo che siano equivalenti
  • EdgeInsets.only() imposta solo gli spigoli specificati.

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

return 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(             // Modify from here...
                  horizontal: 15,
                  vertical: 10,
                ),                                               // To here.
                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(          // Modify from here...
                          left: 8,
                          bottom: 8,
                        ),                                       // To here.
                        child: Text(
                          'Top Songs Today',
                          style: context.titleLarge,
                        ),
                      ),
                      LayoutBuilder(
                        builder: (context, constraints) =>
                            PlaylistSongs(
                              playlist: topSongs,
                              constraints: constraints,
                            ),
                      ),
                    ],
                  ),
                ),
                const SizedBox(width: 25),                       // Modify this line
                Flexible(
                  flex: 10,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Padding(
                        padding: const EdgeInsets.only(          // Modify from here...
                          left: 8,
                          bottom: 8,
                        ),                                       // To here.
                        child: Text(
                          'New Releases',
                          style: context.titleLarge,
                        ),
                      ),
                      LayoutBuilder(
                        builder: (context, constraints) =>
                            PlaylistSongs(
                              playlist: newReleases,
                              constraints: constraints,
                            ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    ),
  ),
);

In lib/src/features/home/view/home_highlight.dart, imposta la spaziatura interna a sinistra e a destra del banner su 35 e quella in alto e in basso 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 the following 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: () => launchUrl(Uri.parse('https://docs.flutter.dev')),
            ),
          ),
        ),
      ],
    );
  }
}

Ricarica l'app. Il layout e la spaziatura sono molto migliorati. Per il tocco finale, aggiungi un po' di movimento e animazione.

7f5e3514a7ee1750.png

Problemi?

Se la tua app non funziona correttamente, utilizza il codice al seguente link per tornare in pista.

8. Aggiungere movimento e animazione

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

Passare da una schermata all'altra con animazione

ThemeProvider definisce un PageTransitionsTheme con animazioni di transizione tra le schermate per le piattaforme mobile (iOS, Android). Gli utenti che utilizzano un computer ricevono già un feedback dal clic del mouse o del trackpad, pertanto non è necessaria un'animazione di transizione di pagina.

Flutter fornisce le animazioni di transizione tra le schermate che puoi configurare per la tua app in base alla piattaforma di destinazione, come mostrato 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(),
  },
);

Passa PageTransitionsTheme sia al tema chiaro sia a quello scuro in lib/src/shared/providers/theme.dart

lib/src/shared/providers/theme.dart

ThemeData light([Color? targetColor]) {
  final colorScheme = colors(Brightness.light, targetColor);
  return ThemeData.light().copyWith(
    pageTransitionsTheme: pageTransitionsTheme,                     // Add this line
    colorScheme: colorScheme,
    appBarTheme: appBarTheme(colorScheme),
    cardTheme: cardTheme(),
    listTileTheme: listTileTheme(colorScheme),
    bottomAppBarTheme: bottomAppBarTheme(colorScheme),
    bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
    navigationRailTheme: navigationRailTheme(colorScheme),
    tabBarTheme: tabBarTheme(colorScheme),
    drawerTheme: drawerTheme(colorScheme),
    scaffoldBackgroundColor: colorScheme.surface,
  );
}

ThemeData dark([Color? targetColor]) {
  final colorScheme = colors(Brightness.dark, targetColor);
  return ThemeData.dark().copyWith(
    pageTransitionsTheme: pageTransitionsTheme,                     // Add this line
    colorScheme: colorScheme,
    appBarTheme: appBarTheme(colorScheme),
    cardTheme: cardTheme(),
    listTileTheme: listTileTheme(colorScheme),
    bottomAppBarTheme: bottomAppBarTheme(colorScheme),
    bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
    navigationRailTheme: navigationRailTheme(colorScheme),
    tabBarTheme: tabBarTheme(colorScheme),
    drawerTheme: drawerTheme(colorScheme),
    scaffoldBackgroundColor: colorScheme.surface,
  );
}

Senza animazione su iOS

Con animazione su iOS

Problemi?

Se la tua app non funziona correttamente, utilizza il codice al seguente link per tornare in pista.

9. Aggiungere stati di passaggio del mouse

Un modo per aggiungere movimento a un'app desktop è utilizzare gli stati di mouseover, in cui un widget cambia stato (ad esempio colore, forma o contenuti) quando l'utente passa il cursore sopra.

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

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.withAlpha(_hovered ? 30 : 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,
       
),
     
),
   
);
 
}
}

Esegui il ricaricamento dell'app e passa il mouse sopra uno dei riquadri delle playlist riprodotte di recente.

OutlinedCard modifica l'opacità e arrotondare gli angoli.

Infine, anima il numero del brano in 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) tra un HoverableSongPlayButton:

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

rowBuilder: (context, index) => DataRow.byIndex(
  index: index,
  cells: [
    DataCell(
      HoverableSongPlayButton(                                      // Modify from here...
        hoverMode: HoverMode.overlay,
        song: playlist.songs[index],
        child: Center(
          child: Text(
            (index + 1).toString(),
            textAlign: TextAlign.center,
          ),
        ),
      ),                                                            // To here.
    ),
    DataCell(
      Row(
        children: [
          Padding(
            padding: const EdgeInsets.all(2),
            child: ClippedImage(playlist.songs[index].image.image),
          ),
          const SizedBox(width: 10),
          Expanded(child: Text(playlist.songs[index].title)),
        ],
      ),
    ),
    DataCell(Text(playlist.songs[index].length.toHumanizedString())),
  ],
),

Ricarica l'app e passa il cursore sopra il numero del brano nella playlist I brani più ascoltati oggi o Uscite recenti.

Il numero si anima in un pulsante di riproduzione che riproduce il brano quando fai clic.

Consulta il codice del progetto finale su GitHub.

10. Complimenti!

Hai completato questo codelab. Hai appreso che esistono molte piccole modifiche che puoi integrare in un'app per renderla più bella, nonché 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. Utilizza la modalità di visualizzazione del testo per produrre un effetto positivo sull'esperienza e sulla percezione della tua app da parte degli utenti.
  • Temi:stabilisci un sistema di design che puoi utilizzare in modo affidabile senza dover prendere decisioni di design per ogni widget.
  • Adattabilità:prendi in considerazione il dispositivo e la piattaforma su cui l'utente esegue la tua app e le relative funzionalità. Tieni in considerazione le dimensioni dello schermo e il modo in cui viene visualizzata la tua app.
  • Movimento e animazione:aggiungere movimento alla tua app aggiunge energia all'esperienza utente e, più concretamente, fornisce feedback agli utenti.

Con alcuni piccoli accorgimenti, la tua app può passare da noiosa a bella:

Prima

1e67c60667821082.png

Dopo

Passaggi successivi

Ci auguriamo che tu abbia imparato di più su come creare app bellissime in Flutter.

Se applichi uno dei suggerimenti o dei trucchi qui menzionati (o se hai un tuo suggerimento da condividere), ci farebbe piacere sapere la tua opinione. Contattaci su Twitter all'indirizzo @rodydavis e @khanhnwin.

Potrebbero esserti utili anche le seguenti risorse.

Applicazione tema

Risorse adattivi e reattivi:

Risorse di design generale:

Inoltre, entra in contatto con la community di Flutter.

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