1. Introduzione
Flutter è il toolkit dell'interfaccia utente di Google che consente di creare fantastiche applicazioni compilate in modo nativo per dispositivi mobili, web e computer a partire da un unico codebase. Flutter funziona con codice esistente, è utilizzato da sviluppatori e organizzazioni di tutto il mondo ed è senza costi e open source.
In questo codelab, migliorerai un'applicazione di musica Flutter, trasformandola da noiosa a meravigliosa. A questo scopo, questo codelab utilizza strumenti e API introdotti in Material 3.
Obiettivi didattici
- Come scrivere un'app Flutter che sia fruibile e accattivante su più piattaforme.
- Come progettare il testo nella tua app per assicurarti che migliori l'esperienza utente.
- Come scegliere i colori giusti, personalizzare i widget, creare il tuo tema e implementare la modalità Buio in modo facile e veloce.
- Come creare app adattive multipiattaforma.
- Come creare app che abbiano un bell'aspetto su qualsiasi schermo.
- Come aggiungere movimento all'app Flutter per renderla più straordinaria.
Prerequisiti:
Questo codelab presuppone che tu abbia una certa esperienza con Flutter. In caso contrario, ti consigliamo di apprendere prima le nozioni di base. I seguenti link sono utili:
- Fai un tour del framework del widget Flutter
- Prova il codelab Write Your First Flutter App, parte 1
Cosa creerai
Questo codelab ti guida nella creazione della schermata Home per un'applicazione chiamata MyArtist, un'app di music player in cui i fan possono tenersi aggiornati con i loro artisti preferiti. Descrive come modificare il design dell'app per renderla più accattivante su tutte le piattaforme.
I seguenti video mostrano come funziona l'app al completamento di questo codelab:
Cosa ti piacerebbe imparare da questo codelab?
2. Configura l'ambiente di sviluppo di Flutter
Per completare questo lab sono necessari due software: l'SDK Flutter e l'editor.
Puoi eseguire il codelab utilizzando uno di questi dispositivi:
- Un dispositivo fisico Android o iOS connesso al computer e impostato sulla modalità sviluppatore.
- Il simulatore iOS (richiede l'installazione degli strumenti Xcode).
- L'emulatore Android (richiede la configurazione in Android Studio).
- Un browser (per il debug è richiesto Chrome).
- Come applicazione desktop Windows, Linux o macOS. Devi svilupparle sulla piattaforma in cui prevedi di eseguire il deployment. Quindi, se vuoi sviluppare un'app desktop per Windows, devi sviluppare su Windows per accedere alla catena di build appropriata. Alcuni requisiti specifici del sistema operativo sono descritti in dettaglio all'indirizzo docs.flutter.dev/desktop.
3. Scarica l'app iniziale del codelab
Clona da GitHub
Per clonare questo codelab da GitHub, esegui questi comandi:
git clone https://github.com/flutter/codelabs.git cd codelabs/boring_to_beautiful/step_01/
Per assicurarti che tutto funzioni correttamente, esegui l'applicazione Flutter come applicazione desktop come mostrato di seguito. In alternativa, apri questo progetto nel tuo IDE e utilizza i relativi strumenti per eseguire l'applicazione.
Operazione riuscita. Deve essere in esecuzione il codice di avvio per la schermata Home di MyArtist. Dovresti visualizzare la schermata Home di MyArtist. Viene visualizzato bene sui computer, ma sui dispositivi mobili... Non ideale. Per una cosa, non rispetta il livello. Non preoccuparti, lo risolverai!
Esplora il codice
Quindi, fai un tour del codice.
Apri lib/src/features/home/view/home_screen.dart
, che contiene quanto segue:
lib/src/features/home/view/home_screen.dart
import 'package:adaptive_components/adaptive_components.dart';
import 'package:flutter/material.dart';
import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
final PlaylistsProvider playlistProvider = PlaylistsProvider();
final List<Playlist> playlists = playlistProvider.playlists;
final Playlist topSongs = playlistProvider.topSongs;
final Playlist newReleases = playlistProvider.newReleases;
final ArtistsProvider artistsProvider = ArtistsProvider();
final List<Artist> artists = artistsProvider.artists;
return LayoutBuilder(
builder: (context, constraints) {
// Add conditional mobile layout
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2), // Modify this line
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(2), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
// Add spacer between tables
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(2), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Questo file importa material.dart
e implementa un widget stateful utilizzando due classi:
- La dichiarazione
import
rende disponibili i componenti Material. - La classe
HomeScreen
rappresenta l'intera pagina visualizzata. - Il metodo
build()
della classe_HomeScreenState
crea la radice della struttura ad albero dei widget, che influisce sul modo in cui vengono creati tutti i widget nella UI.
4. Sfrutta la tipografia
Il testo è ovunque. Il testo è un modo utile per comunicare con l'utente. La tua app è pensata per essere amichevole e divertente oppure forse affidabile e professionale? Esiste un motivo per cui la tua app di online preferita non usa Comic Sans. Il modo in cui viene presentato il testo determina la prima impressione che l'utente ha della tua app. Ecco alcuni modi per utilizzare il testo in modo più ponderato.
Usa le immagini, non le parole
Dove possibile, "mostra" invece di "tell". Ad esempio, NavigationRail
nell'app iniziale presenta schede per ogni percorso principale, ma le icone iniziali sono identiche:
Non è utile perché l'utente deve comunque leggere il testo di ogni scheda. Inizia aggiungendo segnali visivi in modo che l'utente possa dare rapidamente un'occhiata alle icone iniziali per trovare la scheda desiderata. Questo favorisce anche la localizzazione e l'accessibilità.
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 viene eseguita correttamente, cerca gli errori di battitura. Se necessario, utilizza il codice ai seguenti link per tornare in pista.
Scegli i caratteri con cura
I caratteri determinano la personalità della tua applicazione, pertanto è fondamentale scegliere il carattere giusto. Quando selezioni un carattere, tieni in considerazione i seguenti aspetti:
- Sans Serif o Serif: i caratteri Serif hanno tratti o "code" decorativi. in fondo alle lettere e sono percepiti come più formali. I caratteri Sans Serif non presentano tratti decorativi e tendono a essere percepiti come più informali. : T maiuscola per Sans Serif e T maiuscola per Serif
- Caratteri Tutto maiuscolo: l'uso di tutte le lettere maiuscole è appropriato per richiamare l'attenzione su piccole quantità di testo (ad esempio per i titoli), ma se viene fatto un uso eccessivo, si può percepire come un grido che spinge l'utente a ignorarlo completamente.
- Iniziale maiuscola o maiuscola: quando aggiungi i titoli o le etichette, considera le modalità di utilizzo delle lettere maiuscole. Inizia con l'iniziale maiuscola, in cui la prima lettera di ogni parola è maiuscola ("This Is a Title Case Title"), è più formale. L'uso della maiuscola a inizio frase, che utilizza la maiuscola solo per i nomi propri e la prima parola del testo ("Questo è il titolo di una frase"), è più colloquiale e informale.
- Kerning (spaziatura tra ogni lettera), lunghezza della riga (larghezza del testo completo sullo schermo) e altezza della riga (l'altezza di ogni riga di testo): una quantità eccessiva o insufficiente di questi elementi rende la tua app meno leggibile. Ad esempio, è facile perdere il punto in cui si trova la lettura di un blocco di testo di grandi dimensioni e ininterrotto.
Tenendo conto di questo, vai su Google Fonts e scegli un font Sans- Serif, come Montserrat, poiché l'app Music è pensata per essere giocosa e divertente.
Dalla riga di comando, esegui il pull del pacchetto google_fonts
. Inoltre, il file pubspec viene aggiornato per aggiungere i caratteri come dipendenza dall'app.
$ flutter pub add google_fonts
macos/Runner/DebugProfile.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<!-- Make sure these lines are present from here... -->
<key>com.apple.security.network.client</key>
<true/>
<!-- To here. -->
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
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 Montserrat TextTheme:
TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line
Ricarica a caldo per attivare le modifiche. (Utilizza il pulsante nel tuo IDE o, dalla riga di comando, inserisci r
per eseguire il ricaricamento a caldo).
Dovresti vedere le nuove icone NavigationRail
insieme al testo visualizzato nel carattere Montserrat.
Problemi?
Se l'app non viene eseguita correttamente, cerca gli errori di battitura. Se necessario, utilizza il codice ai seguenti link per tornare in pista.
5. Impostare il tema
I temi aiutano a conferire un design strutturato e un'uniformità a un'app specificando un sistema prestabilito di colori e stili di testo. I temi consentono di implementare rapidamente un'interfaccia utente senza doversi preoccupare di dettagli minori come specificare il colore esatto per ogni singolo widget.
Gli sviluppatori di Flutter in genere creano componenti a tema personalizzato in due modi:
- Crea singoli widget personalizzati, ciascuno con il proprio tema.
- Crea temi con ambito per i widget predefiniti.
In questo esempio viene utilizzato un fornitore di temi che si trova in lib/src/shared/providers/theme.dart
per creare widget e colori coerenti in tutta l'app:
lib/src/shared/providers/theme.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
class NoAnimationPageTransitionsBuilder extends PageTransitionsBuilder {
const NoAnimationPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return child;
}
}
class ThemeSettingChange extends Notification {
ThemeSettingChange({required this.settings});
final ThemeSettings settings;
}
class ThemeProvider extends InheritedWidget {
const ThemeProvider(
{super.key,
required this.settings,
required this.lightDynamic,
required this.darkDynamic,
required super.child});
final ValueNotifier<ThemeSettings> settings;
final ColorScheme? lightDynamic;
final ColorScheme? darkDynamic;
final pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: NoAnimationPageTransitionsBuilder(),
TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(),
TargetPlatform.windows: NoAnimationPageTransitionsBuilder(),
},
);
Color custom(CustomColor custom) {
if (custom.blend) {
return blend(custom.color);
} else {
return custom.color;
}
}
Color blend(Color targetColor) {
return Color(
Blend.harmonize(targetColor.value, settings.value.sourceColor.value));
}
Color source(Color? target) {
Color source = settings.value.sourceColor;
if (target != null) {
source = blend(target);
}
return source;
}
ColorScheme colors(Brightness brightness, Color? targetColor) {
final dynamicPrimary = brightness == Brightness.light
? lightDynamic?.primary
: darkDynamic?.primary;
return ColorScheme.fromSeed(
seedColor: dynamicPrimary ?? source(targetColor),
brightness: brightness,
);
}
ShapeBorder get shapeMedium => RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
);
CardTheme cardTheme() {
return CardTheme(
elevation: 0,
shape: shapeMedium,
clipBehavior: Clip.antiAlias,
);
}
ListTileThemeData listTileTheme(ColorScheme colors) {
return ListTileThemeData(
shape: shapeMedium,
selectedColor: colors.secondary,
);
}
AppBarTheme appBarTheme(ColorScheme colors) {
return AppBarTheme(
elevation: 0,
backgroundColor: colors.surface,
foregroundColor: colors.onSurface,
);
}
TabBarTheme tabBarTheme(ColorScheme colors) {
return TabBarTheme(
labelColor: colors.secondary,
unselectedLabelColor: colors.onSurfaceVariant,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
color: colors.secondary,
width: 2,
),
),
),
);
}
BottomAppBarTheme bottomAppBarTheme(ColorScheme colors) {
return BottomAppBarTheme(
color: colors.surface,
elevation: 0,
);
}
BottomNavigationBarThemeData bottomNavigationBarTheme(ColorScheme colors) {
return BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: colors.surfaceContainerHighest,
selectedItemColor: colors.onSurface,
unselectedItemColor: colors.onSurfaceVariant,
elevation: 0,
landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
);
}
NavigationRailThemeData navigationRailTheme(ColorScheme colors) {
return const NavigationRailThemeData();
}
DrawerThemeData drawerTheme(ColorScheme colors) {
return DrawerThemeData(
backgroundColor: colors.surface,
);
}
ThemeData light([Color? targetColor]) {
final _colors = colors(Brightness.light, targetColor);
return ThemeData.light().copyWith(
pageTransitionsTheme: pageTransitionsTheme,
colorScheme: _colors,
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(_colors),
bottomAppBarTheme: bottomAppBarTheme(_colors),
bottomNavigationBarTheme: bottomNavigationBarTheme(_colors),
navigationRailTheme: navigationRailTheme(_colors),
tabBarTheme: tabBarTheme(_colors),
drawerTheme: drawerTheme(_colors),
scaffoldBackgroundColor: _colors.background,
useMaterial3: true,
);
}
ThemeData dark([Color? targetColor]) {
final _colors = colors(Brightness.dark, targetColor);
return ThemeData.dark().copyWith(
pageTransitionsTheme: pageTransitionsTheme,
colorScheme: _colors,
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(_colors),
bottomAppBarTheme: bottomAppBarTheme(_colors),
bottomNavigationBarTheme: bottomNavigationBarTheme(_colors),
navigationRailTheme: navigationRailTheme(_colors),
tabBarTheme: tabBarTheme(_colors),
drawerTheme: drawerTheme(_colors),
scaffoldBackgroundColor: _colors.background,
useMaterial3: true,
);
}
ThemeMode themeMode() {
return settings.value.themeMode;
}
ThemeData theme(BuildContext context, [Color? targetColor]) {
final brightness = MediaQuery.of(context).platformBrightness;
return brightness == Brightness.light
? light(targetColor)
: dark(targetColor);
}
static ThemeProvider of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ThemeProvider>()!;
}
@override
bool updateShouldNotify(covariant ThemeProvider oldWidget) {
return oldWidget.settings != settings;
}
}
class ThemeSettings {
ThemeSettings({
required this.sourceColor,
required this.themeMode,
});
final Color sourceColor;
final ThemeMode themeMode;
}
Color randomColor() {
return Color(Random().nextInt(0xFFFFFFFF));
}
// Custom Colors
const linkColor = CustomColor(
name: 'Link Color',
color: Color(0xFF00B0FF),
);
class CustomColor {
const CustomColor({
required this.name,
required this.color,
this.blend = true,
});
final String name;
final Color color;
final bool blend;
Color value(ThemeProvider provider) {
return provider.custom(this);
}
}
Per utilizzare il provider, crea un'istanza e passala all'oggetto tema con ambito in MaterialApp
, che si trova in lib/src/shared/app.dart
. Verrà ereditato da tutti gli oggetti Theme
nidificati:
lib/src/shared/app.dart
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'playback/bloc/bloc.dart';
import 'providers/theme.dart';
import 'router.dart';
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final settings = ValueNotifier(ThemeSettings(
sourceColor: Colors.pink,
themeMode: ThemeMode.system,
));
@override
Widget build(BuildContext context) {
return BlocProvider<PlaybackBloc>(
create: (context) => PlaybackBloc(),
child: DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) => ThemeProvider(
lightDynamic: lightDynamic,
darkDynamic: darkDynamic,
settings: settings,
child: NotificationListener<ThemeSettingChange>(
onNotification: (notification) {
settings.value = notification.settings;
return true;
},
child: ValueListenableBuilder<ThemeSettings>(
valueListenable: settings,
builder: (context, value, _) {
final theme = ThemeProvider.of(context); // Add this line
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: theme.light(settings.value.sourceColor), // Add this line
routeInformationParser: appRouter.routeInformationParser,
routerDelegate: appRouter.routerDelegate,
);
},
),
)),
),
);
}
}
Ora che il tema è configurato, scegli i colori per l'applicazione.
Scegliere il set di colori giusto non è sempre facile. Potresti avere un'idea del colore principale, ma è probabile che tu voglia che l'app abbia più di un colore. Di che colore deve essere il testo? Titolo? Contenuti? Link? E il colore dello sfondo? Material Theme Builder è uno strumento basato sul web (introdotto in Material 3) che ti aiuta a selezionare un insieme di colori complementari per la tua app.
Per scegliere un colore di origine per l'applicazione, apri lo Strumento per la creazione di temi Material ed esplora i diversi colori dell'interfaccia utente. È importante scegliere un colore adatto all'estetica del brand e/o alle tue preferenze personali.
Dopo aver creato un tema, fai clic con il tasto destro del mouse sul fumetto del colore Principale: si apre una finestra di dialogo contenente il valore esadecimale del colore principale. Copia questo valore. Puoi anche impostare il colore utilizzando questa finestra di dialogo.
Trasmetti il valore esadecimale del colore principale al fornitore del tema. Ad esempio, il colore esadecimale #00cbe6
è specificato come Color(0xff00cbe6)
. ThemeProvider
genera un valore ThemeData
contenente l'insieme di colori complementari che hai visualizzato in anteprima nel generatore di temi Materiali:
final settings = ValueNotifier(ThemeSettings(
sourceColor: Color(0xff00cbe6), // Replace this color
themeMode: ThemeMode.system,
));
Riavvia l'app a caldo. Una volta impostato il colore principale, l'app inizia a essere più espressiva. Accedi a tutti i nuovi colori facendo riferimento al tema nel contesto e scegliendo ColorScheme
:
final colors = Theme.of(context).colorScheme;
Per usare un determinato colore, accedi a un ruolo Colore su colorScheme
. Vai a lib/src/shared/views/outlined_card.dart
e assegna un bordo a OutlinedCard
:
lib/src/shared/views/outlined_card.dart
class _OutlinedCardState extends State<OutlinedCard> {
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: widget.clickable
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: Container(
child: widget.child,
// Add from here...
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
),
// ... To here.
),
);
}
}
Material 3 introduce ruoli per i colori sfumati che si completano a vicenda e possono essere utilizzati nell'interfaccia utente per aggiungere nuovi livelli di espressione. Questi nuovi ruoli relativi ai colori includono:
Primary
,OnPrimary
,PrimaryContainer
eOnPrimaryContainer
Secondary
,OnSecondary
,SecondaryContainer
eOnSecondaryContainer
Tertiary
,OnTertiary
,TertiaryContainer
eOnTertiaryContainer
Error
,OnError
,ErrorContainer
eOnErrorContainer
Background
,OnBackground
Surface
,OnSurface
,SurfaceVariant
eOnSurfaceVariant
Shadow
,Outline
eInversePrimary
Inoltre, i nuovi token di design supportano sia il tema chiaro che quello scuro:
Questi ruoli colore possono essere utilizzati per assegnare un significato ed enfasi a diverse parti dell'interfaccia utente. Anche se un componente non è in evidenza, può sfruttare il colore dinamico.
L'utente può impostare la luminosità dell'app nelle impostazioni di sistema del dispositivo. In lib/src/shared/app.dart
, quando il dispositivo è impostato sulla modalità Buio, restituisci le modalità tema e tema scuro a MaterialApp
.
lib/src/shared/app.dart
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: theme.light(settings.value.sourceColor),
darkTheme: theme.dark(settings.value.sourceColor), // Add this line
themeMode: theme.themeMode(), // Add this line
routeInformationParser: appRouter.routeInformationParser,
routerDelegate: appRouter.routerDelegate,
);
Fai clic sull'icona della luna nell'angolo in alto a destra per attivare la modalità Buio.
Problemi?
Se la tua app non viene eseguita correttamente, utilizza il codice al link riportato di seguito per tornare in pista.
6. Aggiungi il design adattivo
Con Flutter puoi creare app eseguibili praticamente ovunque, ma questo non significa che tutte le app dovrebbero comportarsi allo stesso modo ovunque. Gli utenti si aspettano comportamenti e funzionalità diversi da piattaforme diverse.
Material offre pacchetti per semplificare l'utilizzo dei layout adattivi: puoi trovare questi pacchetti Flutter su GitHub.
Quando crei un'applicazione adattiva multipiattaforma, tieni presente le seguenti differenze:
- Metodo di inserimento: mouse, touch o gamepad
- Dimensioni del carattere, orientamento del dispositivo e distanza di visualizzazione
- Dimensioni schermo e fattore di forma: smartphone, tablet, pieghevoli, desktop, web
Il file lib/src/shared/views/adaptive_navigation.dart
contiene una classe di navigazione in cui puoi fornire un elenco di destinazioni e contenuti per il rendering del corpo. Dato che usi questo layout su più schermi, esiste un layout di base condiviso da trasmettere a ogni account secondario. I binari di navigazione sono ideali per schermi di computer e grandi, ma ottimizza il layout per i dispositivi mobili mostrando invece una barra di navigazione inferiore sui dispositivi mobili.
lib/src/shared/views/adaptive_navigation.dart
import 'package:flutter/material.dart';
class AdaptiveNavigation extends StatelessWidget {
const AdaptiveNavigation({
super.key,
required this.destinations,
required this.selectedIndex,
required this.onDestinationSelected,
required super.child,
});
final List<NavigationDestination> destinations;
final int selectedIndex;
final void Function(int index) onDestinationSelected;
final Widget child;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, dimens) {
// Tablet Layout
if (dimens.maxWidth >= 600) { // Add this line
return Scaffold(
body: Row(
children: [
NavigationRail(
extended: dimens.maxWidth >= 800,
minExtendedWidth: 180,
destinations: destinations
.map((e) => NavigationRailDestination(
icon: e.icon,
label: Text(e.label),
))
.toList(),
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
),
Expanded(child: child),
],
),
);
} // Add this line
// Mobile Layout
// Add from here...
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
destinations: destinations,
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
),
);
// ... To here.
},
);
}
}
Non tutti gli schermi sono delle stesse dimensioni. Se provassi a visualizzare sullo smartphone la versione desktop della tua app, dovresti fare una combinazione di strizzare gli occhi e aumentare lo zoom per vedere tutto. Vuoi che l'app modifichi l'aspetto in base alla schermata in cui viene visualizzata. Grazie al responsive design, ti assicuri che l'app venga visualizzata al meglio su schermi di tutte le dimensioni.
Per rendere reattiva la tua app, introduci alcuni punti di interruzione adattivi (da non confondere con i punti di interruzione di debug). Questi punti di interruzione specificano le dimensioni dello schermo su cui l'app deve modificare il layout.
Gli schermi più piccoli non possono essere visualizzati tanto quanto quelli più grandi senza che i contenuti vengano ridotti. Per evitare che l'app abbia l'aspetto di un'app desktop ridotta, crea un layout separato per i dispositivi mobili che utilizzi le schede per suddividere i contenuti. Questo conferisce all'app un aspetto più nativo sui dispositivi mobili.
I seguenti metodi di estensione (definiti nel progetto MyArtist in lib/src/shared/extensions.dart
) sono un buon punto di partenza quando progetti layout ottimizzati per target diversi.
lib/src/shared/extensions.dart
extension BreakpointUtils on BoxConstraints {
bool get isTablet => maxWidth > 730;
bool get isDesktop => maxWidth > 1200;
bool get isMobile => !isTablet && !isDesktop;
}
Uno schermo di dimensioni superiori a 730 pixel (nella direzione più lunga), ma inferiore a 1200 pixel, è considerato un tablet. Gli annunci più grandi di 1200 pixel sono considerati desktop. Se un dispositivo non è né un tablet né un computer, viene considerato mobile. Puoi scoprire di più sui punti di interruzione adattivi su material.io. Potresti prendere in considerazione l'utilizzo del pacchetto adaptive_breakpoints.
Il layout reattivo della schermata Home utilizza i valori AdaptiveContainer
e AdaptiveColumn
basati su una griglia a 12 colonne con i pacchetti adaptive_components e adaptive_breakpoints per implementare un layout a griglia adattabile in Material Design.
return LayoutBuilder(
builder: (context, constraints) {
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 40,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 20,
),
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
Un layout adattivo ha bisogno di due layout: uno per i dispositivi mobili e uno per schermi più grandi. Al momento LayoutBuilder
restituisce solo un layout desktop. In lib/src/features/home/view/home_screen.dart
crea il layout per dispositivi mobili come TabBar
e TabBarView
con 4 schede.
lib/src/features/home/view/home_screen.dart
import 'package:adaptive_components/adaptive_components.dart';
import 'package:flutter/material.dart';
import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
final PlaylistsProvider playlistProvider = PlaylistsProvider();
final List<Playlist> playlists = playlistProvider.playlists;
final Playlist topSongs = playlistProvider.topSongs;
final Playlist newReleases = playlistProvider.newReleases;
final ArtistsProvider artistsProvider = ArtistsProvider();
final List<Artist> artists = artistsProvider.artists;
return LayoutBuilder(
builder: (context, constraints) {
// Add from here...
if (constraints.isMobile) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
centerTitle: false,
title: const Text('Good morning'),
actions: const [BrightnessToggle()],
bottom: const TabBar(
isScrollable: true,
tabs: [
Tab(text: 'Home'),
Tab(text: 'Recently Played'),
Tab(text: 'New Releases'),
Tab(text: 'Top Songs'),
],
),
),
body: LayoutBuilder(
builder: (context, constraints) => TabBarView(
children: [
SingleChildScrollView(
child: Column(
children: [
const HomeHighlight(),
HomeArtists(
artists: artists,
constraints: constraints,
),
],
),
),
HomeRecent(
playlists: playlists,
axis: Axis.vertical,
),
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
],
),
),
),
);
}
// ... To here.
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 40,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 20,
),
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Problemi?
Se la tua app non viene eseguita correttamente, utilizza il codice al link riportato di seguito per tornare in pista.
Usa uno spazio vuoto
Lo spazio vuoto è un importante strumento visivo per la tua app, che crea un'interruzione organizzativa tra le sezioni.
È meglio avere troppo spazio vuoto che non a sufficienza. È preferibile aggiungere più spazio vuoto, anziché ridurre le dimensioni del carattere o degli elementi visivi per adattarli maggiormente allo spazio.
La mancanza di spazio vuoto può rappresentare un ostacolo per chi ha problemi di vista. Troppo spazio vuoto può causare mancanza di coesione e rendere la tua UI poco organizzata. Ad esempio, guarda i seguenti screenshot:
Successivamente, aggiungerai spazio vuoto alla schermata Home per concedergli più spazio. Successivamente modificherai ulteriormente il layout per regolare con maggiore precisione la spaziatura.
Aggrega un widget con un oggetto Padding
per aggiungere uno spazio vuoto attorno al widget. Aumenta tutti i valori di spaziatura interna attualmente in lib/src/features/home/view/home_screen.dart
a 35:
lib/src/features/home/view/home_screen.dart
Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(35), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) => PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
// Add spacer between tables
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(35), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) => PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
Ricarica a caldo l'app. Dovrebbe essere lo stesso di prima, ma con più spazio vuoto tra i widget. La spaziatura interna aggiuntiva ha un aspetto migliore, ma il banner di evidenziazione nella parte superiore è ancora troppo vicino ai bordi.
In lib/src/features/home/view/home_highlight.dart
, modifica la spaziatura interna nel banner impostandola su 35:
lib/src/features/home/view/home_highlight.dart
class HomeHighlight extends StatelessWidget {
const HomeHighlight({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Clickable(
child: SizedBox(
height: 275,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
'assets/images/news/concert.jpeg',
fit: BoxFit.cover,
),
),
),
onTap: () => launch('https://docs.flutter.dev'),
),
),
),
],
);
}
}
Ricarica a caldo l'app. Le due playlist in basso non hanno spazi vuoti, quindi sembrano appartenere alla stessa tabella. Non è così e lo correggerai successivamente.
Aggiungi uno spazio vuoto tra le playlist inserendo un widget di dimensione nel file Row
che le contiene. In lib/src/features/home/view/home_screen.dart
, aggiungi un elemento SizedBox
con una larghezza di 35:
lib/src/features/home/view/home_screen.dart
Padding(
padding: const EdgeInsets.all(35),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
],
),
),
const SizedBox(width: 35), // Add this line
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
],
),
),
],
),
),
Ricarica a caldo l'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 su 35 la spaziatura interna (orizzontale e verticale) per i widget sulla schermata Home con EdgeInsets.all(35)
, ma puoi anche impostare la spaziatura interna per ciascuno dei bordi in modo indipendente. Personalizza la spaziatura interna per adattarla meglio allo spazio.
EdgeInsets.LTRB()
imposta sinistra, alto, destra e in basso singolarmenteEdgeInsets.symmetric()
imposta la spaziatura interna per verticale (superiore e inferiore) e orizzontale (sinistra e destra) come equivalenteEdgeInsets.only()
imposta solo i bordi specificati.
Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 25, 20, 10), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
), // Modify this line
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 8, bottom: 8), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 8, bottom: 8), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
In lib/src/features/home/view/home_highlight.dart
, imposta la spaziatura interna destra e sinistra sul banner su 35 e la spaziatura interna superiore e inferiore su 5:
lib/src/features/home/view/home_highlight.dart
class HomeHighlight extends StatelessWidget {
const HomeHighlight({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Padding(
// Modify this line
padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 5),
child: Clickable(
child: SizedBox(
height: 275,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
'assets/images/news/concert.jpeg',
fit: BoxFit.cover,
),
),
),
onTap: () => launch('https://docs.flutter.dev'),
),
),
),
],
);
}
}
Ricarica a caldo l'app. Il layout e la spaziatura hanno un aspetto molto migliore. Per il tocco finale, aggiungi un po' di movimento e animazione.
Problemi?
Se la tua app non viene eseguita correttamente, utilizza il codice al link riportato di seguito per tornare in pista.
7. Aggiungi movimento e animazione
Movimento e animazione sono ottimi modi per introdurre movimento ed energia e per fornire feedback quando l'utente interagisce con l'app.
Crea animazioni tra le schermate
L'ThemeProvider
definisce un PageTransitionsTheme
con animazioni di transizione dello schermo per le piattaforme mobile (iOS, Android). Gli utenti di computer desktop ricevono già un feedback dal clic del mouse o del trackpad, quindi non è necessaria un'animazione di transizione di pagina.
Flutter fornisce le animazioni di transizione dello schermo che puoi configurare per la tua app in base alla piattaforma di destinazione, come illustrato in lib/src/shared/providers/theme.dart
:
lib/src/shared/providers/theme.dart
final pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: NoAnimationPageTransitionsBuilder(),
TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(),
TargetPlatform.windows: NoAnimationPageTransitionsBuilder(),
},
);
Passa il PageTransitionsTheme
al tema chiaro e al tema scuro in lib/src/shared/providers/theme.dart
lib/src/shared/providers/theme.dart
ThemeData light([Color? targetColor]) {
final _colors = colors(Brightness.light, targetColor);
return ThemeData.light().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: ColorScheme.fromSeed(
seedColor: source(targetColor),
brightness: Brightness.light,
),
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(),
tabBarTheme: tabBarTheme(_colors),
scaffoldBackgroundColor: _colors.background,
);
}
ThemeData dark([Color? targetColor]) {
final _colors = colors(Brightness.dark, targetColor);
return ThemeData.dark().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: ColorScheme.fromSeed(
seedColor: source(targetColor),
brightness: Brightness.dark,
),
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(),
tabBarTheme: tabBarTheme(_colors),
scaffoldBackgroundColor: _colors.background,
);
}
Senza animazione su iOS
Con animazione su iOS
Problemi?
Se la tua app non viene eseguita correttamente, utilizza il codice al link riportato di seguito per tornare in pista.
Aggiungi stati al passaggio del mouse
Un modo per aggiungere movimento a un'app desktop è gli stati al passaggio del mouse, in cui un widget cambia il suo stato (ad esempio colore, forma o contenuti) quando l'utente passa il cursore del mouse su di esso.
Per impostazione predefinita, la classe _OutlinedCardState
(utilizzata per i riquadri della playlist "Riproduzione di recente") restituisce un MouseRegion
, che trasforma la freccia del cursore in un puntatore al passaggio del mouse, ma puoi aggiungere ulteriore feedback visivo.
Apri lib/src/shared/views/outlined_card.dart e sostituisci i relativi contenuti con la seguente implementazione per introdurre uno stato _hovered
.
lib/src/shared/views/outlined_card.dart
import 'package:flutter/material.dart';
class OutlinedCard extends StatefulWidget {
const OutlinedCard({
super.key,
required this.child,
this.clickable = true,
});
final Widget child;
final bool clickable;
@override
State<OutlinedCard> createState() => _OutlinedCardState();
}
class _OutlinedCardState extends State<OutlinedCard> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final borderRadius = BorderRadius.circular(_hovered ? 20 : 8);
const animationCurve = Curves.easeInOut;
return MouseRegion(
onEnter: (_) {
if (!widget.clickable) return;
setState(() {
_hovered = true;
});
},
onExit: (_) {
if (!widget.clickable) return;
setState(() {
_hovered = false;
});
},
cursor: widget.clickable ? SystemMouseCursors.click : SystemMouseCursors.basic,
child: AnimatedContainer(
duration: kThemeAnimationDuration,
curve: animationCurve,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
borderRadius: borderRadius,
),
foregroundDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurface.withOpacity(
_hovered ? 0.12 : 0,
),
borderRadius: borderRadius,
),
child: TweenAnimationBuilder<BorderRadius>(
duration: kThemeAnimationDuration,
curve: animationCurve,
tween: Tween(begin: BorderRadius.zero, end: borderRadius),
builder: (context, borderRadius, child) => ClipRRect(
clipBehavior: Clip.antiAlias,
borderRadius: borderRadius,
child: child,
),
child: widget.child,
),
),
);
}
}
Ricarica a caldo l'app e poi passa il mouse sopra uno dei riquadri delle playlist riprodotti di recente.
L'icona OutlinedCard
cambia opacità e arrotonda gli angoli.
Infine, anima il numero del brano di una playlist in un pulsante di riproduzione utilizzando il widget HoverableSongPlayButton
definito in lib/src/shared/views/hoverable_song_play_button.dart
. In lib/src/features/playlists/view/playlist_songs.dart
, racchiudi il widget Center
(che contiene il numero del brano) con un HoverableSongPlayButton
:
lib/src/features/playlists/view/playlist_songs.dart
HoverableSongPlayButton( // Add this line
hoverMode: HoverMode.overlay, // Add this line
song: playlist.songs[index], // Add this line
child: Center( // Modify this line
child: Text(
(index + 1).toString(),
textAlign: TextAlign.center,
),
),
), // Add this line
Ricarica l'app, quindi passa il cursore sopra il numero del brano nella playlist Brani più ascoltati di oggi o Nuove uscite.
Il numero si anima in un pulsante riproduci che riproduce il brano quando fai clic su quest'ultimo.
Visualizza il codice finale del progetto su GitHub.
8. Complimenti!
Hai completato il codelab. Hai imparato che ci sono molte piccole modifiche che puoi integrare in un'app per renderla più accattivante, e anche più accessibile, più localizzabile e più adatta a più piattaforme. Queste tecniche includono, a titolo esemplificativo:
- Tipografia: il testo è molto più di uno strumento di comunicazione. Usa il modo in cui il testo viene visualizzato per produrre un effetto positivo sugli utenti l'esperienza e la percezione che hai avuto della tua app.
- Temi: stabilisci un sistema di progettazione che puoi utilizzare in modo affidabile senza dover prendere decisioni di progettazione per ogni widget.
- Adattabilità: considera il dispositivo, la piattaforma su cui l'utente esegue la tua app e le sue funzionalità. Tieni conto delle dimensioni dello schermo e di come viene visualizzata l'app.
- Movimento e animazione: l'aggiunta di movimento all'app aggiunge energia all'esperienza utente e, in pratica, fornisce feedback agli utenti.
Con qualche piccolo ritocco, la tua app può diventare bellissima:
Prima
Dopo
Passaggi successivi
Ci auguriamo che tu abbia scoperto di più sulla creazione di fantastiche app in Flutter.
Se applichi i suggerimenti o i trucchi menzionati qui (o hai un suggerimento da condividere), ci piacerebbe conoscere la tua opinione. Contattaci su Twitter agli indirizzi @rodydavis e @khanhnwin.
Potrebbero esserti utili anche le risorse che seguono.
Applicazione tema
- Generatore di temi Material (strumento)
Risorse adattive e adattabili:
- Decodificare Flutter su annunci adattivi e adattabili (video)
- Adattivi layout (video di The Boring Flutter Development Show)
- Creazione di app reattive e adattive (flutter.dev)
- Componenti di Adaptive Material per Flutter (libreria su GitHub)
- 5 cose che puoi fare per preparare la tua app per gli schermi di grandi dimensioni (video di Google I/O 2021)
Risorse di progettazione generali:
- Le piccole cose: diventare il mitico designer-sviluppatore (video di Flutter Engage)
- Material Design 3 per dispositivi pieghevoli (material.io)
Inoltre, mettiti in contatto con la community di Flutter.
Vai avanti e rendi più bello il mondo delle app.