Informazioni su questo codelab
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:
- Consulta Creare interfacce utente con Flutter
- Prova il codelab La tua prima app Flutter
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.
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:
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',
),
];
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.
- 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 per attivare le modifiche. Utilizza il pulsante nell'IDE o, dalla riga di comando, inserisci
r
per il ricaricamento dinamico.
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:
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.
},
);
}
}
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
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:
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.
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:
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 singolarmenteEdgeInsets.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 equivalentiEdgeInsets.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.
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
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
- Material Theme Builder (strumento)
Risorse adattivi e reattivi:
- Decodifica di Flutter su Adaptive e Responsive (video)
- Layout adattivi (video di The Boring Flutter Development Show)
- Creare app responsive e adattive (flutter.dev)
- Componenti Material adattabili per Flutter (libreria su GitHub)
- Cinque cose che puoi fare per preparare la tua app per gli schermi di grandi dimensioni (video di Google I/O 2021)
Risorse di design generale:
- Le piccole cose: diventare il mitico designer-sviluppatore (video di Flutter Engage)
- Material Design 3 per dispositivi pieghevoli (material.io)
Inoltre, entra in contatto con la community di Flutter.
Vai avanti e rendi il mondo delle app più bello.