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

1. Wprowadzenie

Flutter to opracowany przez Google zestaw narzędzi interfejsu do tworzenia pięknych, natywnie skompilowanych aplikacji na urządzenia mobilne, komputery i komputery przy użyciu jednej bazy kodu. Flutter współpracuje z istniejącym kodem, jest używany przez deweloperów i organizacje na całym świecie oraz jest dostępny bezpłatnie i o otwartym kodzie źródłowym.

Dzięki temu ćwiczeniu w programowaniu ulepszysz aplikację muzyczną Flutter, zmieniając ją z nudnej na piękną. W tym celu wykorzystano narzędzia i interfejsy API wprowadzone w Material 3.

Czego się nauczysz

  • Jak napisać użyteczną i atrakcyjną aplikację Flutter na różnych platformach.
  • Jak zaprojektować tekst w aplikacji, aby spełniał oczekiwania użytkowników.
  • Jak wybrać odpowiednie kolory, dostosować widżety, utworzyć własny motyw oraz szybko i łatwo wdrożyć tryb ciemny.
  • Jak tworzyć aplikacje adaptacyjne działające na różnych platformach
  • Jak tworzyć aplikacje, które będą dobrze wyglądać na każdym ekranie
  • Jak dodać ruch do aplikacji Flutter, dzięki czemu będzie się wyróżniała.

Wymagania wstępne:

W tym ćwiczeniu w programowaniu zakładamy, że masz już doświadczenie w korzystaniu z platformy Flutter. Jeśli nie, warto najpierw poznać podstawy. Przydatne będą te linki:

Co utworzysz

Dzięki nim dowiesz się, jak utworzyć ekran główny aplikacji o nazwie MyArtist, czyli odtwarzacza muzyki, dzięki któremu fani mogą na bieżąco śledzić informacje o ulubionych wykonawcach. Omawiamy w nim, jak zmienić wygląd aplikacji, aby wyglądała atrakcyjnie na różnych platformach.

Po ukończeniu tego ćwiczenia z programowania zobaczysz, jak działa aplikacja:

Czego chcesz się dowiedzieć z tego ćwiczenia z programowania?

Jestem w tym nowym temacie i chcę uzyskać ogólne informacje na ten temat. Wiem coś na ten temat, ale chcę odświeżyć informacje. Szukam przykładowego kodu do użycia w moim projekcie. Potrzebuję wyjaśnienia czegoś konkretnego.

2. Konfigurowanie środowiska programistycznego Flutter

Aby ukończyć ten moduł, potrzebujesz 2 oprogramowania: pakietu SDK Flutter i edytora.

Ćwiczenie z programowania możesz uruchomić na dowolnym z tych urządzeń:

  • Fizyczne urządzenie z Androidem lub iOS podłączone do komputera i ustawione w trybie programisty.
  • Symulator iOS (wymaga zainstalowania narzędzi Xcode).
  • Emulator Androida (wymaga skonfigurowania Android Studio).
  • Przeglądarka (do debugowania wymagany jest Chrome).
  • Aplikacja komputerowa w systemie Windows, Linux lub macOS Programowanie należy tworzyć na platformie, na której zamierzasz wdrożyć usługę. Jeśli więc chcesz opracować aplikację komputerową dla systemu Windows, musisz to zrobić w tym systemie, aby uzyskać dostęp do odpowiedniego łańcucha kompilacji. Istnieją wymagania związane z konkretnymi systemami operacyjnymi, które zostały szczegółowo omówione na stronie docs.flutter.dev/desktop.

3. Pobierz aplikację startową w Codelabs

Sklonuj je z GitHuba

Aby skopiować to ćwiczenia z programowania z GitHuba, uruchom te polecenia:

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

Aby upewnić się, że wszystko działa, uruchom aplikację Flutter jako aplikację komputerową, jak pokazano poniżej. Możesz też otworzyć ten projekt w swoim IDE i użyć jego narzędzi do uruchomienia aplikacji.

a3c16fc17be25f6c.png Uruchom aplikację.

Gotowe! Kod startowy ekranu głównego kanału MyArtist powinien być uruchomiony. Powinien wyświetlić się ekran główny usługi MyArtist. Na komputerach wygląda dobrze, ale na komórce jest... Kiepska sprawa. Po pierwsze, nie uwzględnia czołówki. Nie martw się, rozwiążesz ten problem.

1e67c60667821082.png d1139cde225de452.png

Poznaj kod

Następnie zapoznaj się z kodem.

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

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

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

  • Instrukcja import udostępnia komponenty materiałowe.
  • Klasa HomeScreen reprezentuje całą wyświetlaną stronę.
  • Metoda build() klasy _HomeScreenState tworzy element główny drzewa widżetów, co ma wpływ na sposób tworzenia wszystkich widżetów w interfejsie.

4. Korzystaj z typografii

Tekst jest wszędzie. Tekst to przydatny sposób komunikacji z użytkownikiem. Czy Twoja aplikacja jest przyjazna i zabawna, czy może bezpieczna i profesjonalna? Twoja ulubiona aplikacja bankowa z jakiegoś powodu nie korzysta z Comic Sans. Sposób przedstawiania tekstu wpływa na pierwsze wrażenie użytkownika o aplikacji. Oto kilka sposobów na bardziej przemyślane korzystanie z tekstu.

Pokazuj zamiast opisywać

Gdy tylko jest to możliwe, należy pokazywać zamiast „powiedz”. Na przykład NavigationRail w aplikacji startowej zawiera karty każdej głównej trasy, ale ikony wiodące są identyczne:

86c5f73b3aa5fd35.png

Nie jest to pomocne, ponieważ użytkownik nadal musi przeczytać tekst na każdej karcie. Zacznij od dodania wskazówek wizualnych, aby użytkownik mógł szybko spojrzeć na główne ikony i znaleźć odpowiednią kartę. Pomaga to również w lokalizacji i ułatwieniach dostępu.

a3c16fc17be25f6c.png W lib/src/shared/router.dart dodaj różne wiodące ikony dla każdego miejsca docelowego nawigacji (domu, playlisty i osób):

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 poprawnie, poszukaj literówek. W razie potrzeby możesz ponownie skorzystać z kodu, do którego prowadzą podane niżej linki.

Przemyślany dobór czcionek

Czcionki nadają aplikacji charakter, dlatego dobór odpowiedniej czcionki ma kluczowe znaczenie. Oto kilka kwestii, które należy wziąć pod uwagę podczas wybierania czcionki:

  • Sans-szeryfowa lub szeryfowa: czcionki szeryfowe zawierają dekoracyjne kreski. na końcu listów i są postrzegane jako bardziej formalne. Czcionki bezszeryfowe nie mają dodatkowych kreski i są postrzegane jako bardziej nieformalne. 34bf54e4cad90101.png Wielka bezszeryfowa wielka litera T i wielka litera T
  • Czcionki z wielkością liter: pisanie pisanymi wielkimi literami jest odpowiednie, aby zwrócić uwagę na niewielką ilość tekstu (na przykład nagłówki), ale jeśli zostanie nadwyższone, może zostać postrzegany jako krzyki i skłaniające użytkownika do całkowitego ignorowania go.
  • Jak nazwy własne lub jak w zdaniu: dodając tytuły lub etykiety, staraj się używać wielkich liter. Użyj jakości tytułu, gdzie pierwsze litery każdego wyrazu będą pisane wielką literą („To jest tytuł pisany wielkimi literami”). Jak w zdaniu, w którym wielkie litery są używane tylko w rzeczownikach własnych i pierwszym słowie w tekście („Jak w zdaniu Jak w przypadku tytułów”), jest bardziej swobodny i swobodny.
  • Kering (odstępy między poszczególnymi literami), długość wiersza (szerokość całego tekstu na ekranie) i wysokość wiersza (wysokość każdego wiersza tekstu): zbyt duże lub małe odstępy między literami utrudniają czytanie aplikacji. Na przykład podczas czytania dużego, nierozdzielonego bloku tekstu łatwo zgubić się w miejscu.

Mając to na uwadze, przejdź do Google Fonts i wybierz czcionkę bezszeryfową, np. Montserrat, ponieważ aplikacja muzyczna ma być zabawna i zabawna.

a3c16fc17be25f6c.png Z wiersza poleceń pobierz pakiet google_fonts. Spowoduje to też zaktualizowanie pliku pubspec przez dodanie czcionek jako zależności 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" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <key>com.apple.security.cs.allow-jit</key>
        <true/>
        <!-- Make sure these lines are present from here... -->
        <key>com.apple.security.network.client</key>
        <true/>
        <!-- To here. -->
        <key>com.apple.security.network.server</key>
        <true/>
</dict>
</plist>

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

lib/src/shared/extensions.dart

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

a3c16fc17be25f6c.png Montserrat TextTheme:

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

a3c16fc17be25f6c.png Załaduj ponownie gorąco 7f9a9e103c7b5e5.png, aby aktywować zmiany. Aby załadować ponownie z góry, użyj przycisku w środowisku IDE lub w wierszu poleceń wpisz r.

1e67c60667821082.png

Nowe ikony NavigationRail powinny pojawić się razem z tekstem zapisanym czcionką Montserrat.

Masz problemy?

Jeśli aplikacja nie działa poprawnie, poszukaj literówek. W razie potrzeby możesz ponownie skorzystać z kodu, do którego prowadzą podane niżej linki.

5. Ustaw motyw

Motywy umożliwiają ustrukturyzowany projekt i jednolitość aplikacji przez określenie systemu kolorów i stylów tekstu. Motywy umożliwiają szybką implementację interfejsu użytkownika bez konieczności zajmowania się drobnymi szczegółami, takimi jak określenie dokładnego koloru każdego widżetu.

Programiści korzystający z platformy Flutter zazwyczaj tworzą komponenty o niestandardowej tematyce na 2 sposoby:

  • Twórz indywidualne widżety niestandardowe, każdy z własnym motywem.
  • Utwórz motywy o zakresie na potrzeby widżetów domyślnych.

Ten przykład korzysta z dostawcy motywów znajdującego się w lokalizacji lib/src/shared/providers/theme.dart, aby utworzyć widżety i kolory o spójnej tematyce 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.value, settings.value.sourceColor.value));
 }

 Color source(Color? target) {
   Color source = settings.value.sourceColor;
   if (target != null) {
     source = blend(target);
   }
   return source;
 }

 ColorScheme colors(Brightness brightness, Color? targetColor) {
   final dynamicPrimary = brightness == Brightness.light
       ? lightDynamic?.primary
       : darkDynamic?.primary;
   return ColorScheme.fromSeed(
     seedColor: dynamicPrimary ?? source(targetColor),
     brightness: brightness,
   );
 }

 ShapeBorder get shapeMedium => RoundedRectangleBorder(
       borderRadius: BorderRadius.circular(8),
     );

 CardTheme cardTheme() {
   return CardTheme(
     elevation: 0,
     shape: shapeMedium,
     clipBehavior: Clip.antiAlias,
   );
 }

 ListTileThemeData listTileTheme(ColorScheme colors) {
   return ListTileThemeData(
     shape: shapeMedium,
     selectedColor: colors.secondary,
   );
 }

 AppBarTheme appBarTheme(ColorScheme colors) {
   return AppBarTheme(
     elevation: 0,
     backgroundColor: colors.surface,
     foregroundColor: colors.onSurface,
   );
 }

 TabBarTheme tabBarTheme(ColorScheme colors) {
   return TabBarTheme(
     labelColor: colors.secondary,
     unselectedLabelColor: colors.onSurfaceVariant,
     indicator: BoxDecoration(
       border: Border(
         bottom: BorderSide(
           color: colors.secondary,
           width: 2,
         ),
       ),
     ),
   );
 }

 BottomAppBarTheme bottomAppBarTheme(ColorScheme colors) {
   return BottomAppBarTheme(
     color: colors.surface,
     elevation: 0,
   );
 }

 BottomNavigationBarThemeData bottomNavigationBarTheme(ColorScheme colors) {
   return BottomNavigationBarThemeData(
     type: BottomNavigationBarType.fixed,
     backgroundColor: colors.surfaceContainerHighest,
     selectedItemColor: colors.onSurface,
     unselectedItemColor: colors.onSurfaceVariant,
     elevation: 0,
     landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
   );
 }

 NavigationRailThemeData navigationRailTheme(ColorScheme colors) {
   return const NavigationRailThemeData();
 }

 DrawerThemeData drawerTheme(ColorScheme colors) {
   return DrawerThemeData(
     backgroundColor: colors.surface,
   );
 }

 ThemeData light([Color? targetColor]) {
   final _colors = colors(Brightness.light, targetColor);
   return ThemeData.light().copyWith(
     pageTransitionsTheme: pageTransitionsTheme,
     colorScheme: _colors,
     appBarTheme: appBarTheme(_colors),
     cardTheme: cardTheme(),
     listTileTheme: listTileTheme(_colors),
     bottomAppBarTheme: bottomAppBarTheme(_colors),
     bottomNavigationBarTheme: bottomNavigationBarTheme(_colors),
     navigationRailTheme: navigationRailTheme(_colors),
     tabBarTheme: tabBarTheme(_colors),
     drawerTheme: drawerTheme(_colors),
     scaffoldBackgroundColor: _colors.background,
     useMaterial3: true,
   );
 }

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

 ThemeMode themeMode() {
   return settings.value.themeMode;
 }

 ThemeData theme(BuildContext context, [Color? targetColor]) {
   final brightness = MediaQuery.of(context).platformBrightness;
   return brightness == Brightness.light
       ? light(targetColor)
       : dark(targetColor);
 }

 static ThemeProvider of(BuildContext context) {
   return context.dependOnInheritedWidgetOfExactType<ThemeProvider>()!;
 }

 @override
 bool updateShouldNotify(covariant ThemeProvider oldWidget) {
   return oldWidget.settings != settings;
 }
}

class ThemeSettings {
 ThemeSettings({
   required this.sourceColor,
   required this.themeMode,
 });

 final Color sourceColor;
 final ThemeMode themeMode;
}

Color randomColor() {
 return Color(Random().nextInt(0xFFFFFFFF));
}

// Custom Colors
const linkColor = CustomColor(
 name: 'Link Color',
 color: Color(0xFF00B0FF),
);

class CustomColor {
 const CustomColor({
   required this.name,
   required this.color,
   this.blend = true,
 });

 final String name;
 final Color color;
 final bool blend;

 Color value(ThemeProvider provider) {
   return provider.custom(this);
 }
}

a3c16fc17be25f6c.pngAby użyć dostawcy, utwórz instancję i przekaż ją do obiektu motywu o zakresie w MaterialApp znajdującym się w lokalizacji lib/src/shared/app.dart. Zostanie ona odziedziczona 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,
                 );
               },
             ),
           )),
     ),
   );
 }
}

Po skonfigurowaniu motywu wybierz kolory aplikacji.

Dobór odpowiedniego zestawu kolorów nie zawsze jest łatwy. Kolor podstawowy może Ci się przydać, ale prawdopodobnie chcesz, by aplikacja miała więcej niż 1 kolor. Jakiego koloru ma być tekst? Tytuł? Treść? Linki? A co z kolorem tła? Material Theme Builder to narzędzie internetowe (dostępne w Material 3), które pozwala wybrać zestaw uzupełniających się kolorów do aplikacji.

a3c16fc17be25f6c.pngAby wybrać kolor źródłowy aplikacji, otwórz Material Theme Builder i zapoznaj się z różnymi kolorami interfejsu użytkownika. Ważne jest, aby wybrać kolor, który pasuje do estetyki marki i Twoich osobistych preferencji.

Po utworzeniu motywu kliknij prawym przyciskiem myszy dymek Podstawowy – otworzy się okno z wartością szesnastkową koloru podstawowego. Skopiuj tę wartość. (W tym oknie możesz też ustawić kolor).

a3c16fc17be25f6c.pngPrzekaż wartość szesnastkową koloru podstawowego do dostawcy motywu. Na przykład szesnastkowy kod koloru #00cbe6 jest określony jako Color(0xff00cbe6). Element ThemeProvider generuje ThemeData zawierający zestaw uzupełniających się kolorów, które zostały wyświetlone na podglądzie w narzędziu Material Theme Builder:

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

Uruchom ponownie aplikację na gorąco. Po ustawieniu koloru głównego aplikacja staje się bardziej wyrazista. Wszystkie nowe kolory są dostępne dzięki odwołaniu się do motywu w kontekście i złapaniu ColorScheme:

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

a3c16fc17be25f6c.pngAby użyć konkretnego koloru, uzyskaj dostęp do roli dotyczącej koloru w colorScheme. Otwórz lib/src/shared/views/outlined_card.dart i dodaj ramkę 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 wprowadza zróżnicowane role kolorów, które się uzupełniają i można ich używać w interfejsie do dodawania nowych warstw ekspresji. Te nowe role dotyczące 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 projektowe obsługują zarówno jasny, jak i ciemny motyw:

7b51703ed96196a4.png

Tych ról kolorystycznych można użyć, aby nadać znaczenie różnym częściom interfejsu. Nawet jeśli komponent nie jest widoczny, nadal może korzystać z dynamicznego koloru.

a3c16fc17be25f6c.png Użytkownik może ustawić jasność aplikacji w ustawieniach systemowych urządzenia. Gdy w lib/src/shared/app.dart na urządzeniu jest ustawiony tryb ciemny, przywrócisz MaterialApp ciemny motyw i tryb motywu.

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 Twoja aplikacja nie działa prawidłowo, skorzystaj z poniższego linku, aby rozwiązać ten problem.

6. Dodaj projekt adaptacyjny

Flutter umożliwia tworzenie aplikacji, które działają niemal wszędzie, ale nie oznacza to, że wszystkie działają tak samo wszędzie. Użytkownicy oczekują różnych zachowań i funkcji na różnych platformach.

Material oferuje pakiety, które ułatwiają pracę z układami adaptacyjnymi. Te pakiety znajdziesz na GitHubie.

Podczas tworzenia wieloplatformowej aplikacji adaptacyjnej pamiętaj o tych różnicach między platformami:

  • Metoda wprowadzania tekstu: mysz, przycisk dotykowy lub pad do gier
  • rozmiar czcionki, orientacja urządzenia i odległość wyświetlania;
  • Rozmiar i format ekranu: telefon, tablet, urządzenie składane, komputer, internet

a3c16fc17be25f6c.png Plik lib/src/shared/views/adaptive_navigation.dart zawiera klasę nawigacji, w której możesz podać listę miejsc docelowych i treści do renderowania. Ponieważ używasz tego układu na wielu ekranach, każdy element podrzędny ma swój wspólny układ podstawowy. Szyny nawigacyjne sprawdzają się w przypadku komputerów i dużych ekranów, ale warto dostosować układ do urządzeń mobilnych, wyświetlając dolny pasek nawigacyjny na urządzeniach mobilnych.

lib/src/shared/views/adaptive_navigation.dart

import 'package:flutter/material.dart';

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

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

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

        // Mobile Layout
        // Add from here...
        return Scaffold(
          body: child,
          bottomNavigationBar: NavigationBar(
            destinations: destinations,
            selectedIndex: selectedIndex,
            onDestinationSelected: onDestinationSelected,
          ),
        );
        // ... To here.
      },
    );
  }
}

a8487a3c4d7890c9.png

Nie wszystkie ekrany mają ten sam rozmiar. Aby wyświetlić na telefonie wersję aplikacji na komputer, trzeba było użyć kombinacji mrugania i powiększania, żeby zobaczyć wszystko. Chcesz, aby wygląd aplikacji zmieniał się w zależności od ekranu, na którym się wyświetla. Dzięki projektowaniu responsywnemu Twoja aplikacja będzie dobrze wyglądać na ekranach o różnych rozmiarach.

Aby Twoja aplikacja była elastyczna, wprowadź kilka adaptacyjnych punktów przerwania (nie należy ich mylić z punktami przerwania debugowania). Te punkty przerwania określają rozmiary ekranu, na których aplikacja powinna zmieniać układ.

Na mniejszych ekranach nie można wyświetlić tak dużych ekranów, nawet pomniejszając zawartość. Aby aplikacja nie wyglądała jak aplikacja komputerowa, która została skrócona, utwórz osobny układ dla urządzeń mobilnych, który rozdziela zawartość za pomocą kart. Dzięki temu aplikacja będzie bardziej natywna na urządzeniach mobilnych.

Podczas projektowania zoptymalizowanych układów pod kątem różnych miejsc docelowych warto zacząć od poniższych metod rozszerzeń (zdefiniowanych w projekcie MyArtist w lib/src/shared/extensions.dart).

lib/src/shared/extensions.dart

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

Za tablet uznajemy ekran większy niż 730 pikseli (w najdłuższym kierunku), ale mniejszy niż 1200 pikseli. Wszystkie elementy większe niż 1200 pikseli są uznawane za komputery. Jeśli urządzenie nie jest tabletem ani komputerem, jest uznawane za urządzenie mobilne. Więcej informacji o adaptacyjnych punktach przerwania znajdziesz na stronie material.io. Rozważ użycie pakietu adaptive_breakpoints.

Elastyczny układ ekranu głównego wykorzystuje elementy AdaptiveContainer i AdaptiveColumn oparte na 12-kolumnowej siatce wykorzystującej pakiety adaptive_components i adaptive_breakpoints do wdrożenia elastycznego układu siatki w stylu Material Design.

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

a3c16fc17be25f6c.pngUkład adaptacyjny wymaga 2 układów: jednego dla urządzeń mobilnych i układu elastycznego dla większych ekranów. LayoutBuilder obecnie zwraca tylko układ na komputery. W lib/src/features/home/view/home_screen.dart utwórz układ mobilny jako TabBar i TabBarView z 4 kartami.

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

import 'package:adaptive_components/adaptive_components.dart';
import 'package:flutter/material.dart';

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

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

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

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

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

377cfdda63a9de54.png

Masz problemy?

Jeśli Twoja aplikacja nie działa prawidłowo, skorzystaj z poniższego linku, aby rozwiązać ten problem.

Użyj odstępu

Odstęp jest ważnym narzędziem wizualnym w przypadku aplikacji, który tworzy odstęp między sekcjami.

Lepiej mieć za dużo białego obszaru niż za mało. Zwiększając odstęp, lepiej jest zmniejszyć rozmiar czcionki lub elementów wizualnych, aby lepiej zmieścić się w przestrzeni.

Brak białego obszaru może być poważnym problemem dla osób z wadami wzroku. Zbyt duża ilość białego obszaru może brakować spójności i sprawić, że interfejs będzie wyglądać słabo. Oto przykładowe zrzuty ekranu:

7f5e3514a7ee1750.png

d5144a50f5b4142c.png

Dodaj do niego odstęp, aby zwolnić miejsce. Następnie musisz dokładniej dostosować układ, aby dostosować odstępy.

a3c16fc17be25f6c.png Otaczaj widżet obiektem Padding, aby dodać do niego odstęp. Zwiększ wszystkie wartości dopełnienia, które są obecnie w polu lib/src/features/home/view/home_screen.dart, do 35:

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

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

a3c16fc17be25f6c.png Ponownie załaduj aplikację na gorąco. Powinien wyglądać tak samo jak wcześniej, ale z większą liczbą odstępów między widżetami. Dodatkowe dopełnienie wygląda lepiej, ale baner z najciekawszymi momentami u góry nadal jest za blisko krawędzi.

a3c16fc17be25f6c.png W lib/src/features/home/view/home_highlight.dart zmień dopełnienie banera na 35:

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

class HomeHighlight extends StatelessWidget {
  const HomeHighlight({super.key});

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(35), // Modify this line
            child: Clickable(
              child: SizedBox(
                height: 275,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(10),
                  child: Image.asset(
                    'assets/images/news/concert.jpeg',
                    fit: BoxFit.cover,
                  ),
                ),
              ),
              onTap: () => launch('https://docs.flutter.dev'),
            ),
          ),
        ),
      ],
    );
  }
}

a3c16fc17be25f6c.png Ponownie załaduj aplikację na gorąco. Dwie playlisty na dole nie są rozdzielone, więc wyglądają, jakby należały do tej samej tabeli. To nie jest problem. Następnym razem rozwiążesz ten problem.

df1d9af97d039cc8.png

a3c16fc17be25f6c.png Dodaj odstępy między playlistami, wstawiając widżet rozmiaru do elementu 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

Padding(
  padding: const EdgeInsets.all(35),
  child: Row(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Flexible(
        flex: 10,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: const EdgeInsets.all(35),
              child: Text(
                'Top Songs Today',
                style: context.titleLarge,
              ),
            ),
            PlaylistSongs(
              playlist: topSongs,
              constraints: constraints,
            ),
          ],
        ),
      ),
      const SizedBox(width: 35), // Add this line
      Flexible(
        flex: 10,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: const EdgeInsets.all(35),
              child: Text(
                'New Releases',
                style: context.titleLarge,
              ),
            ),
            PlaylistSongs(
              playlist: newReleases,
              constraints: constraints,
            ),
          ],
        ),
      ),
    ],
  ),
),

a3c16fc17be25f6c.png Ponownie załaduj aplikację na gorąco. Aplikacja powinna wyglądać tak:

d8b2a3d47736dbab.png

Teraz masz sporo miejsca na zawartość ekranu głównego, ale wszystko wygląda zbyt osobno i nie ma spójności między sekcjami.

a3c16fc17be25f6c.png Dotychczas ustawiliśmy całe dopełnienie (poziome i pionowe) widżetów na ekranie głównym na 35 za pomocą parametru EdgeInsets.all(35), ale możesz też ustawić dopełnienie każdej krawędzi oddzielnie. Dostosuj dopełnienie, aby lepiej pasowało do miejsca.

  • EdgeInsets.LTRB() ustawia kolejno lewo, górną, prawą i dolną część
  • EdgeInsets.symmetric() ustawia dopełnienie w pionie (góra i dół) jako równoważne, a dopełnienie poziome (lewa i prawa) – równoważne.
  • EdgeInsets.only() ustawia tylko określone krawędzie.
Scaffold(
  body: SingleChildScrollView(
    child: AdaptiveColumn(
      children: [
        AdaptiveContainer(
           columnSpan: 12,
             child: Padding(
               padding: const EdgeInsets.fromLTRB(20, 25, 20, 10), // Modify this line
               child: Row(
                 mainAxisAlignment: MainAxisAlignment.spaceBetween,
                   children: [
                     Expanded(
                       child: Text(
                         'Good morning',
                          style: context.displaySmall,
                       ),
                     ),
                     const SizedBox(width: 20),
                     const BrightnessToggle(),
                   ],
                 ),
               ),
             ),
             AdaptiveContainer(
               columnSpan: 12,
               child: Column(
                 children: [
                   const HomeHighlight(),
                   LayoutBuilder(
                     builder: (context, constraints) => HomeArtists(
                       artists: artists,
                       constraints: constraints,
                     ),
                   ),
                 ],
               ),
             ),
             AdaptiveContainer(
               columnSpan: 12,
               child: Column(
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: [
                   Padding(
                     padding: const EdgeInsets.symmetric(
                       horizontal: 15,
                       vertical: 10,
                     ), // Modify this line
                     child: Text(
                       'Recently played',
                       style: context.headlineSmall,
                     ),
                   ),
                   HomeRecent(
                     playlists: playlists,
                   ),
                 ],
               ),
             ),
             AdaptiveContainer(
               columnSpan: 12,
               child: Padding(
                 padding: const EdgeInsets.all(15), // Modify this line
                 child: Row(
                   crossAxisAlignment: CrossAxisAlignment.start,
                   children: [
                     Flexible(
                       flex: 10,
                         child: Column(
                           mainAxisAlignment: MainAxisAlignment.start,
                           crossAxisAlignment: CrossAxisAlignment.start,
                           children: [
                             Padding(
                               padding: const EdgeInsets.only(left: 8, bottom: 8), // Modify this line
                               child: Text(
                                 'Top Songs Today',
                                 style: context.titleLarge,
                               ),
                             ),
                             LayoutBuilder(
                               builder: (context, constraints) =>
                                    PlaylistSongs(
                                  playlist: topSongs,
                                  constraints: constraints,
                                ),
                              ),
                            ],
                          ),
                        ),
                    const SizedBox(width: 25),
                    Flexible(
                      flex: 10,
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.start,
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Padding(
                            padding: const EdgeInsets.only(left: 8, bottom: 8), // Modify this line
                            child: Text(
                              'New Releases',
                               style: context.titleLarge,
                            ),
                          ),
                          LayoutBuilder(
                            builder: (context, constraints) =>
                                    PlaylistSongs(
                                  playlist: newReleases,
                                  constraints: constraints,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );

a3c16fc17be25f6c.png W usłudze lib/src/features/home/view/home_highlight.dart ustaw dopełnienie lewe i prawe banera na 35, a dopełnienie u góry i u dołu 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 this line
            padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 5),
            child: Clickable(
              child: SizedBox(
                height: 275,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(10),
                  child: Image.asset(
                    'assets/images/news/concert.jpeg',
                    fit: BoxFit.cover,
                  ),
                ),
              ),
              onTap: () => launch('https://docs.flutter.dev'),
            ),
          ),
        ),
      ],
    );
  }
}

a3c16fc17be25f6c.png Ponownie załaduj aplikację na gorąco. Układ i odstępy wyglądają znacznie lepiej. Na koniec dodaj trochę ruchu i animacji.

7f5e3514a7ee1750.png

Masz problemy?

Jeśli Twoja aplikacja nie działa prawidłowo, skorzystaj z poniższego linku, aby rozwiązać ten problem.

7. Dodaj ruch i animację

Ruch i animacja to świetne sposoby na przedstawienie ruchu i energii oraz przekazywania opinii, gdy użytkownik korzysta z aplikacji.

Przełączanie się między ekranami

ThemeProvider definiuje element PageTransitionsTheme z animacjami przejścia ekranu dla platform mobilnych (iOS, Android). Użytkownicy komputerów otrzymują informacje zwrotne po kliknięciu myszki lub trackpada, więc animacja przejścia między stronami nie jest potrzebna.

Flutter udostępnia animacje przejścia ekranu, które możesz skonfigurować w przypadku aplikacji na podstawie platformy docelowej (patrz lib/src/shared/providers/theme.dart):

lib/src/shared/providers/theme.dart

final pageTransitionsTheme = const PageTransitionsTheme(
  builders: <TargetPlatform, PageTransitionsBuilder>{
    TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
    TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
    TargetPlatform.linux: NoAnimationPageTransitionsBuilder(),
    TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(),
    TargetPlatform.windows: NoAnimationPageTransitionsBuilder(),
  },
);

a3c16fc17be25f6c.png Na pasku PageTransitionsTheme wybierz jasny i ciemny motyw w 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,
  );
}

Bez animacji na iOS

Z animacją w iOS

Masz problemy?

Jeśli Twoja aplikacja nie działa prawidłowo, skorzystaj z poniższego linku, aby rozwiązać ten problem.

Dodaj stany po najechaniu kursorem

Jednym ze sposobów na dodanie ruchu do aplikacji komputerowej jest stosowanie stanów po najechaniu, w którym widżet zmienia swój stan (np. kolor, kształt lub zawartość) po najechaniu na niego kursorem myszy.

Domyślnie klasa _OutlinedCardState (używana w przypadku kafelków playlisty „Ostatnio odtwarzane”) zwraca wartość MouseRegion – dzięki czemu strzałka kursora po najechaniu kursorem zmienia się w wskaźnik. Możesz jednak dodać więcej wizualnych wskazówek.

a3c16fc17be25f6c.png 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.withOpacity(
                _hovered ? 0.12 : 0,
              ),
          borderRadius: borderRadius,
        ),
        child: TweenAnimationBuilder<BorderRadius>(
          duration: kThemeAnimationDuration,
          curve: animationCurve,
          tween: Tween(begin: BorderRadius.zero, end: borderRadius),
          builder: (context, borderRadius, child) => ClipRRect(
            clipBehavior: Clip.antiAlias,
            borderRadius: borderRadius,
            child: child,
          ),
          child: widget.child,
        ),
      ),
    );
  }
}

a3c16fc17be25f6c.png Załaduj ponownie aplikację na gorąco, a następnie najedź kursorem na jeden z ostatnio odtwarzanych kafelków playlisty.

Element OutlinedCard zmienia przezroczystość i zaokrągla rogi.

a3c16fc17be25f6c.pngNa koniec dodaj do animacji 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 narzędziu lib/src/features/playlists/view/playlist_songs.dart umieść w widżecie Center (zawierającym numer utworu) etykietę HoverableSongPlayButton:

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

HoverableSongPlayButton(        // Add this line
  hoverMode: HoverMode.overlay, // Add this line
  song: playlist.songs[index],  // Add this line
  child: Center(                // Modify this line
    child: Text(
      (index + 1).toString(),
       textAlign: TextAlign.center,
       ),
    ),
  ),                            // Add this line

a3c16fc17be25f6c.pngPonownie załaduj aplikację i najedź kursorem na numer utworu na playliście Najpopularniejsze utwory lub Nowości.

Liczba zmienia się w przycisk odtwórz, którego kliknięcie powoduje odtworzenie utworu.

Ostateczny kod projektu znajdziesz na GitHub.

8. Gratulacje!

To już koniec ćwiczenia z programowania. Wiesz już, że istnieje wiele drobnych zmian, które można zintegrować z aplikacją, aby uczynić ją ładniejszą, bardziej dostępną, bardziej zlokalizowaną i dostosowaną do wielu platform. Te techniki to m.in.:

  • Typografia: tekst to nie tylko narzędzie do komunikacji. Wykorzystaj sposób prezentowania tekstu, aby uzyskać pozytywny wpływ na wrażeniach i postrzegania aplikacji.
  • Tematyka: stwórz system projektowania, z którego można korzystać niezawodnie bez konieczności podejmowania decyzji dotyczących projektu każdego widżetu.
  • Adaptalność: weź pod uwagę urządzenie i platformę, na których użytkownik korzysta z Twojej aplikacji, oraz jej możliwości. Weź pod uwagę rozmiar ekranu i sposób wyświetlania aplikacji.
  • Ruch i animacja: dodanie ruchu do aplikacji zwiększa satysfakcję użytkowników, a praktycznie pozwala im przekazać informacje zwrotne.

Wystarczy kilka drobnych poprawek, aby aplikacja stała się nudna lub atrakcyjna:

Przed

1e67c60667821082.png

Po

Dalsze kroki

Mamy nadzieję, że dowiedzieliście się więcej o tworzeniu pięknych aplikacji w ramach Flutter.

Jeśli zastosujesz którąś z wymienionych tutaj porad lub wskazówek (lub chcesz podzielić się własną wskazówką), chętnie poznamy Twoją opinię. Skontaktuj się z nami na Twitterze: @rodydavis i @khanhnwin.

Przydatne mogą okazać się również poniższe materiały.

Motywy

Zasoby adaptacyjne i elastyczne:

Ogólne zasoby dotyczące projektowania:

Nawiąż kontakt ze społecznością Flutter.

Twórz piękne rzeczy ze świata aplikacji.