Zmień wygląd aplikacji Flutter z nudnej na piękną

Jak sprawić, aby aplikacja Flutter była ładna, a nie nudna

Informacje o tym ćwiczeniu (w Codelabs)

subjectOstatnia aktualizacja: cze 24, 2025
account_circleAutorzy: The Flutter Team

1. Wprowadzenie

Flutter to pakiet narzędzi Google do tworzenia interfejsu użytkownika, który umożliwia tworzenie atrakcyjnych aplikacji kompilowanych natywnie na urządzenia mobilne, komputery i internet na podstawie jednej bazy kodu. Flutter współpracuje z dotychczasowym kodem, jest używany przez deweloperów i organizacje na całym świecie, a do tego jest bezpłatny i oparty na otwartym kodzie źródłowym.

W tym laboratorium programistycznym ulepszysz aplikację muzyczną Flutter, zmieniając ją z nudnej na piękną. W tym celu to ćwiczenie wykorzystuje narzędzia i interfejsy API omówione w Materiale 3.

Czego się nauczysz

  • Jak napisać aplikację Flutter, która jest użyteczna i ładna na różnych platformach.
  • Jak zaprojektować tekst w aplikacji, aby zapewnić lepsze wrażenia użytkownikom.
  • Jak wybierać odpowiednie kolory, dostosowywać widżety, tworzyć własne motywy i szybko wdrażać tryb ciemny.
  • Jak tworzyć aplikacje adaptacyjne na wiele platform.
  • Jak tworzyć aplikacje, które dobrze wyglądają na każdym ekranie
  • Jak dodać do aplikacji Flutter animacje, aby wyróżnić ją na tle innych.

Wymagania wstępne

To ćwiczenie z programowania zakłada, że masz już pewne doświadczenie w używaniu Fluttera. Jeśli nie, warto najpierw zapoznać się z podstawami. Te linki mogą być przydatne:

Co utworzysz

To ćwiczenie z programowania poprowadzi Cię przez proces tworzenia ekranu głównego aplikacji MyArtist, czyli odtwarzacza muzyki, w której fani mogą śledzić ulubionych wykonawców. W tym artykule znajdziesz informacje o tym, jak zmodyfikować projekt aplikacji, aby wyglądała ona estetycznie na wszystkich platformach.

Te filmy pokazują, jak działa aplikacja po zakończeniu tego ćwiczenia:

Czego chcesz się nauczyć z tego Codelab?

2. Konfigurowanie środowiska programistycznego Flutter

Do wykonania tego laboratorium potrzebne są 2 programy: Flutter SDKedytor.

Możesz uruchomić laboratorium programistyczne na dowolnym z tych urządzeń:

  • fizyczne urządzenie Android lub iOS połączone z komputerem i ustawione w trybie programisty.
  • Symulator iOS (wymaga zainstalowania narzędzi Xcode).
  • Emulator Androida (wymaga skonfigurowania w Android Studio).
  • przeglądarka (do debugowania wymagana jest przeglądarka Chrome);
  • jako aplikacja na komputer z systemem Windows, Linux lub macOS; Musisz tworzyć aplikację na platformie, na której planujesz ją wdrożyć. Jeśli więc chcesz tworzyć aplikacje na komputery z systemem Windows, musisz korzystać z Windowsa, aby uzyskać dostęp do odpowiedniego łańcucha kompilacji. Istnieją wymagania dotyczące poszczególnych systemów operacyjnych, które omówiono szczegółowo na stronie docs.flutter.dev/desktop.

3. Pobierz aplikację startową z ćwiczeniami z programowania

Sklonuj je z GitHuba.

Aby skopiować ten projekt z GitHuba, uruchom te polecenia:

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

Aby mieć pewność, że wszystko działa prawidłowo, uruchom aplikację Flutter jako aplikację komputerową, jak pokazano poniżej. Możesz też otworzyć ten projekt w swojej IDE i uruchomić aplikację za pomocą narzędzi dostępnych w tym środowisku.

flutter run

Gotowe! Kod inicjujący ekranu głównego MyArtist powinien być uruchomiony. Powinien pojawić się ekran główny MyArtist. Na komputerze wygląda dobrze, ale na urządzeniu mobilnym... Niezbyt dobrze. Po pierwsze, nie uwzględnia wycięcia. Nie martw się, to się da naprawić.

1e67c60667821082.pngd1139cde225de452.png

Omówienie kodu

Następnie zapoznaj się z kodem.

Otwórz plik lib/src/features/home/view/home_screen.dart, który zawiera:

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

Ten plik importuje material.dart i wdraża widżet stanowy za pomocą 2 klas:

  • Instrukcja import udostępnia komponenty Material Design.
  • Klasa HomeScreen reprezentuje całą wyświetlaną stronę.
  • Metoda build() klasy _HomeScreenState tworzy element główny drzewa widżetów, który wpływa na sposób tworzenia wszystkich widżetów w interfejsie użytkownika.

4. Korzystanie z typografii

Tekst jest wszędzie. Tekst to przydatny sposób na komunikowanie się z użytkownikiem. Czy Twoja aplikacja ma być przyjazna i zabawna, czy może wiarygodna i profesjonalna? Jest powód, dla którego Twoja ulubiona aplikacja bankowa nie używa czcionki Comic Sans. Sposób prezentacji tekstu wpływa na pierwsze wrażenie użytkownika na temat aplikacji. Oto kilka sposobów na bardziej przemyślane używanie tekstu.

Pokazuj zamiast opisywać

W miarę możliwości używaj „pokaż”, a nie „powiedz”. Na przykład NavigationRail w aplikacji startowej ma karty dla każdej głównej trasy, ale ikony na kartach są identyczne:

86c5f73b3aa5fd35.png

To nie jest pomocne, ponieważ użytkownik nadal musi przeczytać tekst na każdej karcie. Zacznij od dodania wskazówek wizualnych, aby użytkownik mógł szybko znaleźć odpowiednią ikonę. Pomaga to też w lokalizacji i ułatwieniu dostępu.

lib/src/shared/router.dart dodaj charakterystyczne ikony dla każdego miejsca docelowego nawigacji (Ekran główny, Playlista i Osoby):

lib/src/shared/router.dart

const List<NavigationDestination> destinations = [
  NavigationDestination(
    label: 'Home',
    icon: Icon(Icons.home),                                      // Modify this line
    route: '/',
  ),
  NavigationDestination(
    label: 'Playlists',
    icon: Icon(Icons.playlist_add_check),                        // Modify this line
    route: '/playlists',
  ),
  NavigationDestination(
    label: 'Artists',
    icon: Icon(Icons.people),                                    // Modify this line
    route: '/artists',
  ),
];

23278e4f4610fbf4.png

Masz problemy?

Jeśli aplikacja nie działa prawidłowo, sprawdź, czy nie ma w niej literówek. W razie potrzeby użyj kodu z tych linków, aby wrócić na właściwą drogę.

Rozważnie dobieraj czcionki

Czcionki nadają aplikacji charakter, dlatego wybór odpowiedniej czcionki jest kluczowy. Wybierając czcionkę, weź pod uwagę te kwestie:

  • Czcionki szeryfowe lub bezszeryfowe: czcionki szeryfowe mają ozdobne kreski lub „ogonki” na końcu liter i są postrzegane jako bardziej formalne. Czcionki bezszeryfowe nie mają dekoracyjnych kresek i są postrzegane jako bardziej nieformalne. Wielka litera T w czcionce bezszeryfowej i szeryfowej
  • Czcionki tylko z wielkich liter: używanie czcionek tylko z wielkich liter jest odpowiednie w przypadku chęci zwrócenia uwagi na niewielkie ilości tekstu (np. nagłówki), ale nadmierne ich stosowanie może być odbierane jako krzyczenie, przez co użytkownik może je całkowicie zignorować.
  • Jak nazwy własne lub jak w zdaniu: podczas dodawania tytułów lub etykiet zastanów się nad tym, jak używasz wielkich liter: jak nazwy własne, gdzie pierwsza litera każdego słowa jest wielką literą („To jest tytuł własny”) jest bardziej formalna. Jak w zdaniu, czyli tylko nazwy własne i pierwsze słowo w tekście są pisane wielką literą („To jest tytuł pisany jak w zdaniu”), jest bardziej potoczny i nieformalny.
  • Kerning (odstęp między literami), długość linii (szerokość całego tekstu na ekranie) i wysokość linii (wysokość każdej linii tekstu): zbyt duże lub zbyt małe wartości tych parametrów powodują, że aplikacja jest mniej czytelna. Podczas czytania dużego, nieprzerwanego bloku tekstu trudno jest na przykład zachować miejsce, w którym się zatrzymaliśmy.

Pamiętając o tym, wejdź na stronę Czcionki Google i wybierz czcionkę bezszeryfową, taką jak Montserrat, ponieważ aplikacja muzyczna ma być zabawna i ciekawa.

W wierszu poleceń pobierz pakiet google_fonts. Spowoduje to też zaktualizowanie pliku pubspec.yaml, aby dodać czcionki jako zależność aplikacji.

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>

W lib/src/shared/extensions.dart zaimportuj nowy pakiet:

lib/src/shared/extensions.dart

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

Ustaw Montserrat TextTheme:

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

Aby zastosować zmiany, wykonaj 7f9a9e103c7b5e5.png. (użyj przycisku w IDE lub wpisz r w wierszu poleceń, aby wykonać szybkie przeładowanie):

1e67c60667821082.png

Powinny się wyświetlić nowe ikony NavigationRail oraz tekst w czcionce Montserrat.

Masz problemy?

Jeśli aplikacja nie działa prawidłowo, sprawdź, czy nie ma w niej literówek. W razie potrzeby użyj kodu z tych linków, aby wrócić na właściwą drogę.

5. Ustawianie motywu

Motywy pomagają w zapewnianiu uporządkowanego projektu i jednolitości aplikacji przez określenie zestawu kolorów i stylów tekstu. Dzięki temu możesz szybko wdrożyć interfejs użytkownika bez konieczności zwracania uwagi na drobne szczegóły, takie jak dokładny kolor każdego widżetu.

Deweloperzy Fluttera zwykle tworzą komponenty w niestandardowym stylu na jeden z 2 sposobów:

  • Twórz poszczególne niestandardowe widżety z własnym motywem.
  • Tworzenie ograniczonych motywów dla domyślnych widżetów.

W tym przykładzie dostawca motywów znajdujący się w lib/src/shared/providers/theme.dart służy do tworzenia spójnych motywów widżetów i kolorów w całej aplikacji:

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

Aby użyć dostawcy, utwórz jego instancję i przenieś ją do obiektu tematu ograniczonego w funkcji MaterialApp, która znajduje się w pliku lib/src/shared/app.dart. Będzie ona dziedziczona przez wszystkie zagnieżdżone obiekty Theme:

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

Gdy motyw jest już skonfigurowany, wybierz kolory aplikacji.

Wybór odpowiedniego zestawu kolorów może być trudny. Możesz mieć pomysł na kolor podstawowy, ale prawdopodobnie chcesz, aby Twoja aplikacja miała więcej niż jeden kolor. Jakiego koloru ma być tekst? Tytuł? Treść? Linki? A co z kolorem tła? Kreator motywu Material to narzędzie internetowe (wprowadzone w Material 3), które pomaga wybrać zestaw uzupełniających się kolorów dla aplikacji.

Aby wybrać kolor źródłowy aplikacji, otwórz Kreator motywu Material i wypróbuj różne kolory interfejsu. Wybierz kolor, który pasuje do estetyki marki lub Twoich osobistych preferencji.

Po utworzeniu motywu kliknij prawym przyciskiem myszy okrąg koloru Główny. Otworzy się okno zawierające szesnastkowy kod koloru podstawowego. Skopiuj tę wartość. (możesz też ustawić kolor w tym oknie).

Przekaż wartość szesnastkową głównego koloru dostawcy motywu. Na przykład szesnastkowy kod koloru #00cbe6 jest określony jako Color(0xff00cbe6). ThemeProvider generuje ThemeData, który zawiera zestaw kolorów uzupełniających, które zostały wyświetlone w Material Theme Builder:

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

Uruchom aplikację ponownie. Gdy kolor podstawowy jest już ustawiony, aplikacja staje się bardziej wyrazista. Aby uzyskać dostęp do wszystkich nowych kolorów, odwołuj się do motywu w kontekście i pobierz ColorScheme:

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

Aby użyć określonego koloru, uzyskaj dostęp do rolę kolorucolorScheme. Otwórz lib/src/shared/views/outlined_card.dart i nadaj elementowi OutlinedCard obramowanie:

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 wprowadza subtelne role kolorów, które się wzajemnie uzupełniają i można ich używać w całym interfejsie, aby dodać nowe warstwy wyrazu. Te nowe role kolorów obejmują:

  • 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

Dodatkowo nowe tokeny projektów obsługują zarówno motyw jasny, jak i ciemny:

7b51703ed96196a4.png

Te role kolorów można wykorzystać do nadawania znaczenia i podkreślania różnych części interfejsu. Nawet jeśli komponent nie jest widoczny, może korzystać z dynamicznych kolorów.

Użytkownik może ustawić jasność aplikacji w ustawieniach systemu urządzenia. W lib/src/shared/app.dart, gdy urządzenie jest w trybie ciemnym, przywróć ciemny motyw i tryb 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,
);

Aby włączyć tryb ciemny, kliknij ikonę księżyca w prawym górnym rogu.

Masz problemy?

Jeśli aplikacja nie działa prawidłowo, skorzystaj z kodu pod tym linkiem, aby ją naprawić.

6. Dodawanie projektu adaptacyjnego

Dzięki Flutter możesz tworzyć aplikacje, które działają prawie wszędzie, ale nie oznacza to, że każda aplikacja działa wszędzie tak samo. Użytkownicy oczekują różnych zachowań i funkcji na różnych platformach.

Material Design udostępnia pakiety, które ułatwiają pracę z dopasowywanymi układami. Te pakiety Fluttera znajdziesz na GitHubie.

Podczas tworzenia aplikacji dostosowywalnej na wiele platform pamiętaj o tych różnicach:

  • Metoda wprowadzania: mysz, dotyk lub gamepad.
  • Rozmiar czcionki, orientacja urządzenia i odległość od ekranu
  • Rozmiar i format ekranu: telefon, tablet, urządzenie składane, komputer, przeglądarka

Plik lib/src/shared/views/adaptive_navigation.dart zawiera klasę nawigacji, w której możesz podać listę miejsc docelowych i treści do wyrenderowania w tekstowej części. Ponieważ używasz tego układu na wielu ekranach, do każdego ekranu podrzędnego należy przekazać wspólny podstawowy układ. Paski nawigacyjne sprawdzają się na komputerach i dużych ekranach, ale na urządzeniach mobilnych lepiej jest wyświetlać pasek nawigacyjny u dołu ekranu.

lib/src/shared/views/adaptive_navigation.dart

import 'package:flutter/material.dart';

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

 
final List<NavigationDestination> destinations;
 
final int selectedIndex;
 
final void Function(int index) onDestinationSelected;
 
final Widget child;

 
@override
 
Widget build(BuildContext context) {
   
return LayoutBuilder(
     
builder: (context, dimens) {
       
if (dimens.maxWidth >= 600) {                               // Add this line
         
return Scaffold(
           
body: Row(
             
children: [
               
NavigationRail(
                 
extended: dimens.maxWidth >= 800,
                 
minExtendedWidth: 180,
                 
destinations: destinations
                     
.map(
                       
(e) => NavigationRailDestination(
                         
icon: e.icon,
                         
label: Text(e.label),
                       
),
                     
)
                     
.toList(),
                 
selectedIndex: selectedIndex,
                 
onDestinationSelected: onDestinationSelected,
               
),
               
Expanded(child: child),
             
],
           
),
         
);
       
}                                                           // Add this line
       
// Mobile Layout
       
// Add from here...
       
return Scaffold(
         
body: child,
         
bottomNavigationBar: NavigationBar(
           
destinations: destinations,
           
selectedIndex: selectedIndex,
           
onDestinationSelected: onDestinationSelected,
         
),
       
);
       
// ... To here.
     
},
   
);
 
}
}

a8487a3c4d7890c9.png

Nie wszystkie ekrany mają ten sam rozmiar. Jeśli spróbujesz wyświetlić wersję aplikacji na komputer na telefonie, będziesz musiał mrużyć oczy i powiększać ekran, aby zobaczyć wszystko. Chcesz, aby wygląd aplikacji zmieniał się w zależności od ekranu, na którym jest wyświetlana. Dzięki elastycznemu projektowaniu masz pewność, że Twoja aplikacja będzie dobrze wyglądać na ekranach o różnych rozmiarach.

Aby aplikacja była responsywna, wprowadź kilka punktów przełamania dostosowujących się do zmian (nie myl ich z punktami przełamania do debugowania). Te punkty graniczne określają rozmiary ekranu, przy których aplikacja powinna zmienić układ.

Mniejsze ekrany nie mogą wyświetlać tak dużo treści jak większe ekrany bez ich zmniejszania. Aby aplikacja nie wyglądała jak aplikacja na komputer, która została pomniejszona, utwórz osobny układ na urządzenia mobilne, który wykorzystuje karty do podziału treści. Dzięki temu aplikacja będzie bardziej natywnych na urządzeniach mobilnych.

Podane niżej metody rozszerzania (zdefiniowane w projekcie MyArtist w pliku lib/src/shared/extensions.dart) stanowią dobry punkt wyjścia przy projektowaniu optymalnych układów pod kątem różnych grup docelowych.

lib/src/shared/extensions.dart

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

Ekran o rozmiary większych niż 730 pikseli (w najdłuższym kierunku), ale mniejszych niż 1200 pikseli jest uznawany za tablet. Wszystko, co przekracza 1200 pikseli, jest uważane za komputer. Jeśli urządzenie nie jest ani tabletem, ani komputerem stacjonarnym, jest uznawane za urządzenie mobilne. Więcej informacji o punktach kontrolnych w materiałach na stronie material.io.

Responsywny układ ekranu głównego używa AdaptiveContainerAdaptiveColumn na podstawie siatki 12 kolumn.

Układ adaptacyjny wymaga 2 układów: jednego dla urządzeń mobilnych i elastycznego dla większych ekranów. W tym momencie LayoutBuilder zwraca układ na komputery. W lib/src/features/home/view/home_screen.dart utwórz układ mobilny jako TabBarTabBarView z 4 kartami.

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

import 'package:flutter/material.dart';

import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../../utils/adaptive_components.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';

class HomeScreen extends StatefulWidget {
 
const HomeScreen({super.key});

 
@override
 
State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
 
@override
 
Widget build(BuildContext context) {
   
final PlaylistsProvider playlistProvider = PlaylistsProvider();
   
final List<Playlist> playlists = playlistProvider.playlists;
   
final Playlist topSongs = playlistProvider.topSongs;
   
final Playlist newReleases = playlistProvider.newReleases;
   
final ArtistsProvider artistsProvider = ArtistsProvider();
   
final List<Artist> artists = artistsProvider.artists;
   
return LayoutBuilder(
     
builder: (context, constraints) {
       
// Add from here...
       
if (constraints.isMobile) {
         
return DefaultTabController(
           
length: 4,
           
child: Scaffold(
             
appBar: AppBar(
               
centerTitle: false,
               
title: const Text('Good morning'),
               
actions: const [BrightnessToggle()],
               
bottom: const TabBar(
                 
isScrollable: true,
                 
tabs: [
                   
Tab(text: 'Home'),
                   
Tab(text: 'Recently Played'),
                   
Tab(text: 'New Releases'),
                   
Tab(text: 'Top Songs'),
                 
],
               
),
             
),
             
body: LayoutBuilder(
               
builder: (context, constraints) => TabBarView(
                 
children: [
                   
SingleChildScrollView(
                     
child: Column(
                       
children: [
                         
const HomeHighlight(),
                         
HomeArtists(
                           
artists: artists,
                           
constraints: constraints,
                         
),
                       
],
                     
),
                   
),
                   
HomeRecent(playlists: playlists, axis: Axis.vertical),
                   
PlaylistSongs(playlist: topSongs, constraints: constraints),
                   
PlaylistSongs(
                     
playlist: newReleases,
                     
constraints: constraints,
                   
),
                 
],
               
),
             
),
           
),
         
);
       
}
       
// ... To here.

       
return Scaffold(
         
body: SingleChildScrollView(
           
child: AdaptiveColumn(
             
children: [
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
child: Row(
                     
mainAxisAlignment: MainAxisAlignment.spaceBetween,
                     
children: [
                       
Expanded(
                         
child: Text(
                           
'Good morning',
                           
style: context.displaySmall,
                         
),
                       
),
                       
const SizedBox(width: 20),
                       
const BrightnessToggle(),
                     
],
                   
),
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Column(
                   
children: [
                     
const HomeHighlight(),
                     
LayoutBuilder(
                       
builder: (context, constraints) => HomeArtists(
                         
artists: artists,
                         
constraints: constraints,
                       
),
                     
),
                   
],
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Column(
                   
crossAxisAlignment: CrossAxisAlignment.start,
                   
children: [
                     
Padding(
                       
padding: const EdgeInsets.all(2),
                       
child: Text(
                         
'Recently played',
                         
style: context.headlineSmall,
                       
),
                     
),
                     
HomeRecent(playlists: playlists),
                   
],
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
child: Row(
                     
crossAxisAlignment: CrossAxisAlignment.start,
                     
children: [
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'Top Songs Today',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: topSongs,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'New Releases',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: newReleases,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                     
],
                   
),
                 
),
               
),
             
],
           
),
         
),
       
);
     
},
   
);
 
}
}

377cfdda63a9de54.png

Masz problemy?

Jeśli aplikacja nie działa prawidłowo, skorzystaj z kodu pod tym linkiem, aby ją naprawić.

7. Używanie spacji

Przestrzeń jest ważnym narzędziem wizualnym w aplikacji, ponieważ pozwala na uporządkowanie treści w poszczególnych sekcjach.

Lepiej jest mieć za dużo pustej przestrzeni niż za mało. Lepiej jest dodać więcej pustej przestrzeni niż zmniejszać rozmiar czcionki lub elementów wizualnych, aby zmieścić więcej treści.

Brak odstępów może być utrudnieniem dla osób mających problemy ze wzrokiem. Zbyt dużo pustego miejsca może powodować brak spójności i sprawić, że interfejs będzie wyglądał niechlujnie. Przykłady takich zrzutów ekranu:

7f5e3514a7ee1750.png

d5144a50f5b4142c.png

Następnie dodaj puste miejsce na ekranie głównym, aby uzyskać więcej miejsca. Następnie możesz dostosować układ, aby doprecyzować odstępy.

Owiń widżet obiektem Padding, aby dodać wokół niego białą przestrzeń. Zwiększ wszystkie wartości wypełniania w pliku lib/src/features/home/view/home_screen.dart do 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,
                            ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    ),
  ),
);

Ponownie załaduj aplikację. Powinna wyglądać tak samo jak wcześniej, ale z większymi odstępami między widżetami. Dodatkowe wypełnienie wygląda lepiej, ale banner z wyróżnieniem u góry jest nadal zbyt blisko krawędzi.

W pliku lib/src/features/home/view/home_highlight.dart zmień wypełnienie banera na 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')),
            ),
          ),
        ),
      ],
    );
  }
}

Ponownie wczytaj aplikację. Dwie playlisty u dołu nie mają między sobą żadnych pustych miejsc, więc wyglądają tak, jakby należały do tej samej tabeli. Nie jest to jednak Twój przypadek, więc w następnym kroku zajmiesz się tym problemem.

df1d9af97d039cc8.png

Dodaj pustą przestrzeń między playlistami, wstawiając widżet rozmiaru do Row, który je zawiera. W lib/src/features/home/view/home_screen.dart dodaj SizedBox o szerokości 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,
                    ),
              ),
            ],
          ),
        ),
      ],
    ),
  ),
),

Ponownie wczytaj aplikację. Aplikacja powinna wyglądać tak:

d8b2a3d47736dbab.png

Teraz jest wystarczająco dużo miejsca na treści na ekranie głównym, ale wszystko wygląda zbyt od siebie oddzielone i nie ma spójności między sekcjami.

Jak dotąd ustawiliśmy wszystkie odstępy (zarówno poziome, jak i pionowy) dla widżetów na ekranie głównym na 35 z użyciem EdgeInsets.all(35), ale możesz też ustawić odstępy dla poszczególnych krawędzi niezależnie od siebie. Dostosuj odstęp, aby lepiej dopasować obraz do miejsca.

  • EdgeInsets.LTRB() ustawia lewą, górną, prawą i dolną krawędź osobno
  • EdgeInsets.symmetric() ustawia dopełnienie w orientacji pionowej (góra i dół) na równe, a w orientacji poziomej (lewo i prawo) na równe.
  • EdgeInsets.only() ustawia tylko określone krawędzie.

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

lib/src/features/home/view/home_highlight.dart ustaw dopełnienie po lewej i po prawej stronie banera na 35, a po górze i na dole na 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')),
            ),
          ),
        ),
      ],
    );
  }
}

Załaduj ponownie aplikację. Układ i odstępy będą wyglądać znacznie lepiej. Na koniec dodaj ruch i animację.

7f5e3514a7ee1750.png

Masz problemy?

Jeśli aplikacja nie działa prawidłowo, skorzystaj z kodu pod tym linkiem, aby ją naprawić.

8. Dodawanie ruchu i animacji

Ruch i animacja to świetne sposoby na wprowadzenie ruchu i energii oraz zapewnienie informacji zwrotnych, gdy użytkownik wchodzi w interakcję z aplikacją.

Animacja między ekranami

Element ThemeProvider definiuje PageTransitionsTheme z animacjami przejścia między ekranami na platformach mobilnych (iOS, Android). Użytkownicy komputerów już otrzymują informacje zwrotne po kliknięciu myszką lub trackpadem, więc animacja przejścia między stronami nie jest potrzebna.

Flutter udostępnia animacje przejścia między ekranami, które możesz skonfigurować w aplikacji na podstawie platformy docelowej, jak widać na 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(),
  },
);

Przekaż PageTransitionsTheme do motywu jasnego i ciemnego w 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,
  );
}

Bez animacji na iOS

Z animacją na iOS

Masz problemy?

Jeśli aplikacja nie działa prawidłowo, skorzystaj z kodu pod tym linkiem, aby ją naprawić.

9. Dodawanie stanów najechania

Jednym ze sposobów na dodanie ruchu do aplikacji na komputery jest stan najechania kursorem, w którym widżet zmienia stan (np. kolor, kształt lub zawartość), gdy użytkownik najedzie na niego kursorem.

Domyślnie klasa _OutlinedCardState (używana w przypadku kafelków playlisty „Ostatnio odtwarzane”) zwraca wartość MouseRegion, która zmienia strzałkę kursora na wskaźnik po najechaniu kursorem na element, ale możesz dodać więcej informacji wizualnych.

Otwórz plik lib/src/shared/views/outlined_card.dart i zastąp jego zawartość poniższą implementacją, aby wprowadzić stan _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,
       
),
     
),
   
);
 
}
}

Ponownie załaduj aplikację, a następnie najedź kursorem na jeden z kafelków ostatnio odtwarzanych playlist.

OutlinedCard zmienia przezroczystość i zaokrągla rogi.

Na koniec użyj animacji, aby zmienić numer utworu na playliście w przycisk odtwarzania za pomocą widżetu HoverableSongPlayButton zdefiniowanego w lib/src/shared/views/hoverable_song_play_button.dart. W lib/src/features/playlists/view/playlist_songs.dart nawiń widżet Center (zawierający numer utworu) za pomocą elementu 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())),
  ],
),

Wczytaj ponownie aplikację, a następnie najedź kursorem na numer utworu na liście Najpopularniejsze dziś utwory lub na liście Nowości.

Liczba zmienia się w przycisk odtwarzania, który po kliknięciu odtwarza utwór.

Zobacz kod końcowy projektu na GitHubie.

10. Gratulacje!

To ćwiczenie zostało ukończone. Wiesz już, że w aplikacji można wprowadzić wiele drobnych zmian, które sprawią, że będzie ona ładniejsza, bardziej dostępna, łatwiejsza do zlokalizowania i bardziej odpowiednia dla wielu platform. Oto niektóre z tych technik:

  • Typografia: tekst to coś więcej niż tylko narzędzie do komunikacji. Zadbaj o sposób wyświetlania tekstu, aby pozytywnie wpłynąć na wrażenia użytkowników i ich postrzeganie aplikacji.
  • Motyw: utwórz system projektowania, którego możesz używać bez konieczności podejmowania decyzji dotyczących projektu każdego widżetu.
  • Adaptacyjność: weź pod uwagę urządzenie i platformę, na której użytkownik uruchamia aplikację, oraz ich możliwości. Weź pod uwagę rozmiar ekranu i sposób wyświetlania aplikacji.
  • Ruch i animacja: dodanie ruchu do aplikacji zwiększa jej dynamikę i wrażenia użytkownika, a co ważniejsze, zapewnia mu informacje zwrotne.

Kilka drobnych poprawek może sprawić, że Twoja aplikacja stanie się piękna:

Przed

1e67c60667821082.png

Po

Dalsze kroki

Mamy nadzieję, że udało Ci się dowiedzieć więcej o tworzeniu atrakcyjnych aplikacji w Flutterze.

Jeśli zastosujesz któryś z podanych tu wskazówek lub porad (lub masz własne, którymi chcesz się podzielić), daj nam znać. Skontaktuj się z nami na Twitterze: @rodydavis@khanhnwin.

Mogą Ci się też przydać te materiały.

Motywy

Zasoby adaptacyjne i responsywne:

Ogólne zasoby projektowe:

Nawiązuj kontakt ze społecznością Fluttera.

Zacznij tworzyć piękne aplikacje.