Membuat tampilan aplikasi Flutter menjadi lebih menarik

1. Pengantar

Flutter adalah toolkit UI Google untuk membuat aplikasi yang menarik dan dikompilasi secara native dari satu codebase untuk seluler, web, dan desktop. Flutter berfungsi dengan kode yang sudah ada, digunakan oleh developer dan organisasi di seluruh dunia, serta gratis dan bersifat open source.

Dalam codelab ini, Anda akan membuat tampilan aplikasi musik Flutter menjadi lebih menarik. Untuk melakukannya, codelab ini menggunakan alat dan API yang diperkenalkan di Material 3.

Yang akan Anda pelajari

  • Cara menulis aplikasi Flutter yang dapat digunakan dan terlihat menarik di semua platform.
  • Cara mendesain teks di aplikasi untuk memberikan nilai tambah pada pengalaman pengguna.
  • Cara memilih warna yang tepat, menyesuaikan widget, mem-build tema sendiri, serta menerapkan mode gelap dengan cepat dan mudah.
  • Cara mem-build aplikasi adaptif lintas platform.
  • Cara mem-build aplikasi yang terlihat bagus di semua layar.
  • Cara menambahkan gerakan ke aplikasi Flutter Anda agar terlihat bagus.

Prasyarat:

Codelab ini mengasumsikan bahwa Anda memiliki pengalaman menggunakan Flutter. Jika tidak, sebaiknya pelajari dasar-dasarnya terlebih dahulu. Link berikut akan membantu:

Yang akan Anda build

Codelab ini memandu Anda dalam mem-build layar utama untuk aplikasi bernama MyArtist, sebuah aplikasi pemutar musik tempat penggemar dapat terus mengikuti kabar terbaru dari artis favoritnya. Bagian ini membahas cara memodifikasi desain aplikasi Anda agar terlihat bagus di berbagai platform.

GIF animasi berikut menunjukkan cara kerja aplikasi pada akhir codelab ini:

4a0f6509a18aaf30.gif 1557a5d9dab19d75.gif

Apa yang ingin Anda pelajari dari codelab ini?

Saya baru mengenal topik ini, jadi saya ingin melihat ringkasan yang bagus. Saya sedikit paham soal topik ini, tetapi saya perlu mengingat kembali. Saya sedang mencari kode contoh untuk digunakan dalam project saya. Saya sedang mencari penjelasan tentang hal spesifik.

2. Menyiapkan lingkungan Flutter Anda

Anda memerlukan dua software untuk menyelesaikan lab ini—Flutter SDK dan editor.

Anda dapat menjalankan codelab menggunakan salah satu perangkat berikut:

  • Perangkat Android atau iOS fisik yang terhubung ke komputer dan disetel ke mode Developer.
  • Simulator iOS (perlu menginstal alat Xcode).
  • Android Emulator (memerlukan penyiapan di Android Studio).
  • Browser (Chrome diperlukan untuk proses debug).
  • Aplikasi desktop Windows, Linux, atau macOS. Anda harus melakukan pengembangan di platform tempat Anda berencana men-deploy aplikasi. Jadi, jika ingin mengembangkan aplikasi desktop Windows, Anda harus mengembangkannya di Windows untuk mengakses rantai build yang sesuai. Ada persyaratan khusus sistem operasi yang dibahas secara mendetail di flutter.dev/desktop.

3 Mendapatkan aplikasi awal codelab

Meng-clone dari GitHub

Untuk meng-clone codelab ini dari GitHub, jalankan perintah berikut:

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

Untuk memastikan semuanya berfungsi, jalankan aplikasi Flutter sebagai aplikasi desktop seperti yang ditunjukkan di bawah. Atau, buka project ini di IDE Anda, lalu gunakan alatnya untuk menjalankan aplikasi.

a3c16fc17be25f6c.png Menjalankan aplikasi.

Berhasil! Kode awal untuk layar utama MyArtist akan berjalan. Anda akan melihat layar utama MyArtist. Terlihat bagus di desktop, tetapi untuk perangkat seluler... Tidak bagus. Karena alasan tertentu, tampilannya tampak tidak pas dengan notch-nya. Jangan khawatir, Anda dapat memperbaikinya.

9ebe486bc7dfa36b.png 1b30e16df3cde215.png

Menerapkan kode

Selanjutnya, lakukan penerapan kode.

Buka lib/src/features/home/view/home_screen.dart, yang berisi hal berikut:

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({Key? key}) : super(key: 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,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

File ini mengimpor material.dart dan menerapkan widget stateful menggunakan dua class:

  • Pernyataan import akan menyediakan Komponen Material.
  • Class HomeScreen merepresentasikan seluruh halaman yang ditampilkan.
  • Metode build() class _HomeScreenState akan membuat root hierarki widget, yang memengaruhi cara pembuatan semua widget di UI.

4. Memanfaatkan tipografi

Teks digunakan di berbagai tempat. Teks sangat berguna sebagai alat komunikasi kepada pengguna. Apakah aplikasi Anda ditujukan untuk memberikan kesan ramah dan menyenangkan, atau mungkin tepercaya dan profesional? Ada alasan mengapa aplikasi perbankan favorit Anda tidak menggunakan font Comic Sans. Tampilan teks membentuk kesan pertama pengguna tentang aplikasi Anda. Berikut beberapa cara untuk menggunakan teks dengan lebih cermat.

Tunjukkan, jangan katakan

Jika memungkinkan, prioritaskan "tunjukkan" daripada "katakan". Misalnya, NavigationRail di aplikasi awal memiliki tab untuk setiap rute utama, tetapi ikon utamanya identik:

86c5f73b3aa5fd35.png

Hal ini tidak membantu karena pengguna masih harus membaca teks di setiap tab. Mulai dengan menambahkan isyarat visual sehingga pengguna dapat melihat sekilas ikon utama dengan cepat untuk menemukan tab yang diinginkan. Hal ini juga akan membantu dalam hal pelokalan dan aksesibilitas.

a3c16fc17be25f6c.png Di lib/src/shared/router.dart, tambahkan ikon utama yang berbeda untuk setiap tujuan navigasi (beranda, playlist, dan orang):

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

Terjadi masalah?

Jika aplikasi tidak berjalan dengan semestinya, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

Memilih font dengan cermat

Font menetapkan kepribadian aplikasi Anda, jadi pastikan untuk memilih font yang tepat. Saat memilih font, berikut beberapa hal yang perlu dipertimbangkan:

  • Sans-serif atau serif: Font serif memiliki garis-garis dekoratif atau "ekor" di akhir huruf dan memiliki kesan yang lebih formal. Font Sans-serif tidak memiliki goresan dekoratif dan cenderung memiliki kesan yang lebih informal. 34bf54e4cad90101.png Huruf besar T sans serif dan huruf besar T serif
  • Penggunaan font huruf besar semua: Penggunaan huruf besar semua di teks yang singkat sesuai untuk menarik perhatian (misalnya judul). Namun, jika digunakan secara berlebihan, hal ini dapat memberikan kesan kasar sehingga pengguna akan mengabaikannya.
  • Kapitalisasi judul atau kapitalisasi kalimat: Saat menambahkan judul atau label, pertimbangkan cara Anda menggunakan huruf besar: kapitalisasi judul, yang menggunakan huruf besar di awal setiap kata ("Ini adalah Judul Kasus Judul"), bersifat lebih formal. Kapitalisasi kalimat, yang hanya menggunakan huruf besar untuk kata benda khusus dan kata pertama dalam teks ("Ini adalah judul kapitalisasi kalimat"), bersifat komunikatif dan informal.
  • Kerning (spasi di antara setiap huruf), panjang baris (lebar teks lengkap di layar), dan tinggi baris (ukuran tinggi setiap baris teks): Penerapannya yang tidak seimbang akan membuat aplikasi Anda kurang mudah dibaca. Misalnya, Anda mudah kehilangan posisi bacaan saat membaca blok teks yang besar dan tidak terputus.

Dengan mempertimbangkan hal ini, buka Google Fonts dan pilih font sans-serif, seperti Montserrat, karena aplikasi musik ditujukan untuk memberikan kesan informal dan menyenangkan.

a3c16fc17be25f6c.png Dari command line, tarik paket google_fonts. Tindakan ini juga memperbarui file pubspec untuk menambahkan font sebagai dependensi aplikasi.

$ flutter pub add google_fonts
<?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> //For macOS only
        <true/>
// .. To here
        <key>com.apple.security.network.server</key>
        <true/>
</dict>
</plist>

a3c16fc17be25f6c.png Di lib/src/shared/extensions.dart, impor paket baru:

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

a3c16fc17be25f6c.png Tetapkan font Montserrat TextTheme:

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

a3c16fc17be25f6c.png Lakukan hot reload pada 7f9a9e103c7b5e5.png untuk mengaktifkan perubahan. (Gunakan tombol di IDE Anda atau, dari command line, masukkan r untuk melakukan hot reload):

ff6f09f4cc39c21e.png

Anda akan melihat ikon NavigationRail baru beserta teks yang ditampilkan dalam font Montserrat.

Terjadi masalah?

Jika aplikasi tidak berjalan dengan semestinya, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

5. Menetapkan tema

Tema membantu menghadirkan desain dan keseragaman terstruktur ke aplikasi dengan menentukan sistem kumpulan warna dan gaya teks. Dengan tema, Anda dapat menerapkan UI dengan cepat tanpa harus pusing memikirkan detail kecil seperti menentukan warna yang tepat untuk setiap widget.

Developer Flutter biasanya membuat komponen bertema kustom dengan salah satu dari dua cara berikut:

  • Membuat masing-masing widget kustom dengan temanya sendiri.
  • Membuat tema cakupan untuk widget default.

Contoh ini menggunakan penyedia tema yang terletak di lib/src/shared/providers/theme.dart untuk membuat widget dan warna bertema konsisten di seluruh aplikasi:

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(
     {Key? key,
     required this.settings,
     required this.lightDynamic,
     required this.darkDynamic,
     required Widget child})
     : super(key: key, child: 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.surfaceVariant,
     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.pngUntuk menggunakan penyedia tema, buat instance dan teruskan ke objek tema cakupan di MaterialApp, yang terletak di lib/src/shared/app.dart. Tema akan diwarisi oleh objek Theme bertingkat:

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({Key? key}) : super(key: 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,
                 );
               },
             ),
           )),
     ),
   );
 }
}

Setelah tema disiapkan, pilih warna untuk aplikasi.

Memilih kumpulan warna yang tepat tidak selalu mudah. Anda mungkin telah memikirkan warna primer, tetapi kemungkinan besar Anda ingin menggunakan lebih dari satu warna pada aplikasi Anda. Apa warna teksnya? Judul? Konten? Link? Bagaimana dengan warna latar belakang? Builder Tema Material adalah alat berbasis web (diperkenalkan di Material 3) yang membantu memilih kumpulan warna pelengkap untuk aplikasi Anda.

a3c16fc17be25f6c.pngUntuk memilih warna sumber aplikasi, buka Material Theme Builder dan coba berbagai warna untuk UI aplikasi Anda. Pastikan untuk memilih warna yang sesuai dengan estetika merek dan/atau preferensi pribadi Anda.

Setelah membuat tema, klik kanan balon warna Primer. Tindakan ini akan membuka dialog yang berisi nilai heksadesimal warna primer. Salin nilai ini. (Anda juga dapat menetapkan warna menggunakan dialog ini.)

a6201933c4be275c.gif

a3c16fc17be25f6c.pngTeruskan nilai hex warna primer ke penyedia tema. Misalnya, warna heksadesimal #00cbe6 ditentukan sebagai Color(0xff00cbe6). ThemeProvider akan menghasilkan ThemeData yang berisi kumpulan warna pelengkap yang Anda lihat di Builder Tema Material:

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

Lakukan hot restart pada aplikasi. Aplikasi akan mulai terlihat lebih ekspresif dengan warna primer. Akses semua warna baru dengan mereferensikan tema dalam konteks dan mengambil ColorScheme:

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

a3c16fc17be25f6c.pngUntuk menggunakan warna tertentu, akses peran warna di colorScheme. Buka lib/src/shared/views/outlined_card.dart dan beri OutlinedCard batas:

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 memperkenalkan berbagai peran warna yang saling melengkapi dan dapat digunakan di seluruh UI untuk menambahkan lapisan ekspresi baru. Peran warna baru tersebut meliputi:

  • 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

Selain itu, token desain yang baru mendukung tema terang dan gelap:

7b51703ed96196a4.png

Peran warna ini dapat digunakan untuk menetapkan makna dan penekanan pada berbagai bagian UI. Meskipun tidak cukup terlihat jelas, komponen masih dapat memanfaatkan warna dinamis.

a3c16fc17be25f6c.png Pengguna dapat menyesuaikan kecerahan aplikasi di setelan sistem perangkat. Di lib/src/shared/app.dart, jika perangkat disetel ke mode gelap, tampilkan tema gelap dan mode tema ke MaterialApp.

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

Klik ikon bulan di pojok kanan atas untuk mengaktifkan mode gelap.

60ad6e64df0c5957.gif

Terjadi masalah?

Jika aplikasi tidak berjalan dengan semestinya, gunakan kode di link berikut untuk membuatnya kembali normal.

6. Menambahkan desain adaptif

Dengan Flutter, Anda dapat mem-build aplikasi yang berjalan hampir di mana saja. Namun, hal ini bukan berarti bahwa setiap aplikasi dapat berperilaku sama di semua layar perangkat. Pengguna mengharapkan perilaku dan fitur yang berbeda dari berbagai platform.

Material menawarkan paket untuk mempermudah pekerjaan terkait tata letak adaptif. Anda dapat menemukan paket Flutter ini di GitHub.

Perhatikan perbedaan platform berikut saat mem-build aplikasi adaptif lintas platform:

  • Metode input: mouse, sentuh, atau gamepad
  • Ukuran font, orientasi perangkat, dan jarak pandang
  • Ukuran layar dan faktor bentuk: ponsel, tablet, perangkat foldable, desktop, web

a3c16fc17be25f6c.png File lib/src/shared/views/adaptive_navigation.dart berisi class navigasi tempat Anda dapat memberikan daftar tujuan dan konten untuk merender isi. Karena Anda menggunakan tata letak ini di beberapa layar, ada tata letak dasar bersama untuk diteruskan ke setiap turunan. Kolom samping navigasi cocok untuk desktop dan layar besar, tetapi jadikan tata letak tersebut mobile-friendly dengan menampilkan menu navigasi bawah di perangkat seluler.

import 'package:flutter/material.dart';

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

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

d1825d6f0d23314.png

Tidak semua layar memiliki ukuran yang sama. Jika Anda mencoba menampilkan versi desktop aplikasi di ponsel, Anda perlu melihat cermat dan melakukan zoom untuk melihat semuanya dengan jelas. Anda ingin aplikasi mengubah tampilannya berdasarkan layar tempat aplikasi ditampilkan. Dengan desain yang responsif, Anda memastikan bahwa aplikasi terlihat bagus di semua ukuran layar.

Agar aplikasi Anda responsif, perkenalkan beberapa titik henti sementara adaptif (jangan samakan dengan titik henti sementara proses debug). Titik henti sementara ini menentukan ukuran layar tempat aplikasi akan mengubah tata letak.

Layar yang lebih kecil tidak dapat menampilkan layar yang lebih besar tanpa mengecilkan konten. Agar aplikasi tidak terlihat seperti aplikasi desktop yang dikecilkan, buat tata letak terpisah untuk perangkat seluler yang menggunakan tab untuk membagi konten. Hal ini membuat aplikasi bersifat lebih native di perangkat seluler.

Metode ekstensi berikut (ditentukan dalam project MyArtist di lib/src/shared/extensions.dart), merupakan tempat yang tepat untuk memulai saat mendesain tata letak yang dioptimalkan untuk berbagai perangkat target.

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

Layar yang lebih besar dari 730 piksel (dalam sisi terpanjang), tetapi lebih kecil dari 1200 piksel, dianggap sebagai tablet. Layar pun yang berukuran lebih besar dari 1.200 piksel akan dianggap sebagai desktop. Jika bukan tablet atau desktop, perangkat akan dianggap sebagai perangkat seluler. Anda dapat mempelajari lebih lanjut titik henti sementara adaptif di material.io. Anda dapat mempertimbangkan untuk menggunakan paket adaptive_breakpoints.

Tata letak responsif layar utama menggunakan AdaptiveContainer dan AdaptiveColumn berdasarkan petak 12 kolom menggunakan paket adaptive_components dan adaptive_breakpoints untuk menerapkan tata letak petak responsif di Desain Material.

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.pngTata letak adaptif memerlukan dua tata letak: satu untuk perangkat seluler dan tata letak responsif untuk layar yang lebih besar. LayoutBuilder saat ini hanya menampilkan tata letak desktop. Di lib/src/features/home/view/home_screen.dart, buat tata letak seluler sebagai TabBar dan TabBarView dengan 4 tab.

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({Key? key}) : super(key: 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,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
     },
   );
 }
}

2e4115a01d76e7ae.png

Terjadi masalah?

Jika aplikasi tidak berjalan dengan semestinya, gunakan kode di link berikut untuk membuatnya kembali normal.

Menggunakan spasi kosong

Spasi kosong adalah alat visual penting untuk aplikasi Anda dan menciptakan jeda terstruktur di antar bagian.

Memiliki banyak spasi kosong lebih baik daripada spasi yang terlalu sedikit. Sebaiknya tambahkan lebih banyak spasi kosong daripada mengurangi ukuran font atau elemen visual agar lebih sesuai dengan ruang yang ada.

Kurangnya spasi kosong dapat menyebabkan masalah bagi pengguna yang memiliki masalah penglihatan. Terlalu banyak spasi kosong dapat mengurangi kepaduan dan membuat UI Anda terlihat buruk. Misalnya, lihat screenshot berikut:

f50d2fe899e57e42.png

cdf5a34a7658a15e.png

Berikutnya, Anda akan menambahkan spasi kosong ke layar utama untuk memberikan lebih banyak ruang. Anda kemudian akan menyesuaikan tata letak untuk mengatur jaraknya.

a3c16fc17be25f6c.png Gabungkan widget dengan objek Padding untuk menambahkan spasi kosong di sekitar widget tersebut. Tingkatkan semua nilai padding yang saat ini menggunakan lib/src/features/home/view/home_screen.dart ke 35:

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 Lakukan hot reload pada aplikasi. Tampilannya akan terlihat sama seperti sebelumnya, tetapi dengan lebih banyak spasi kosong di antara widget. Padding tambahan terlihat lebih baik, tetapi banner sorotan di bagian atas masih terlalu dekat dengan tepi.

a3c16fc17be25f6c.png Di lib/src/features/home/view/home_highlight.dart, ubah padding pada banner menjadi 35:

class HomeHighlight extends StatelessWidget {
  const HomeHighlight({Key? key}) : super(key: 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 Lakukan hot reload pada aplikasi. Kedua playlist di bagian bawah tidak memiliki spasi kosong di antaranya, sehingga terlihat seperti tabel yang sama. Bukan itu yang dimaksud dan Anda akan memperbaikinya nanti.

df1d9af97d039cc8.png

a3c16fc17be25f6c.png Tambahkan spasi kosong di antara playlist dengan memasukkan widget ukuran ke Row yang memuatnya. Di lib/src/features/home/view/home_screen.dart, tambahkan SizedBox dengan lebar 35:

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 Lakukan hot reload pada aplikasi. Aplikasi akan terlihat seperti berikut:

89411cc17daf641b.png

Sekarang ada banyak ruang untuk konten layar utama, tetapi tampilannya terlihat terlalu terpisah dan tidak ada kohesi di antara bagian.

a3c16fc17be25f6c.png Sejauh ini, Anda telah menyetel semua padding (baik horizontal maupun vertikal) untuk widget di layar utama ke 35 dengan EdgeInsets.all(35), tetapi Anda juga dapat menyetel padding untuk setiap tepi secara terpisah. Sesuaikan padding agar jaraknya lebih sesuai.

  • EdgeInsets.LTRB() menetapkan setiap batas bagian kiri, atas, kanan, dan bawah
  • EdgeInsets.symmetric() menetapkan padding agar jarak vertikal (atas dan bawah) menjadi setara dan jarak horizontal (kiri dan kanan) menjadi setara
  • EdgeInsets.only() hanya menyetel tepi yang ditentukan.
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 Di lib/src/features/home/view/home_highlight.dart, tetapkan padding kiri dan kanan di banner ke 35, dan padding atas dan bawah ke 5:

class HomeHighlight extends StatelessWidget {
  const HomeHighlight({Key? key}) : super(key: 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 Lakukan hot reload pada aplikasi. Tata letak dan spasinya akan terlihat jauh lebih baik. Untuk sentuhan akhir, tambahkan gerakan dan animasi.

2776abfa6ca738af.png

Terjadi masalah?

Jika aplikasi tidak berjalan dengan semestinya, gunakan kode di link berikut untuk membuatnya kembali normal.

7. Menambahkan gerakan dan animasi

Gerakan dan animasi adalah cara yang bagus untuk memberikan kesan aplikasi yang dinamis dan penuh semangat, serta untuk memberikan masukan saat pengguna berinteraksi dengan aplikasi.

Menganimasikan transisi antar-layar

ThemeProvider menentukan PageTransitionsTheme dengan animasi transisi layar untuk platform seluler (iOS, Android). Pengguna desktop sudah mendapatkan masukan dari klik mouse atau trackpad, sehingga animasi transisi halaman tidak diperlukan.

Flutter menyediakan animasi transisi layar yang dapat dikonfigurasikan untuk aplikasi Anda berdasarkan platform target seperti yang terlihat di 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 Teruskan PageTransitionsTheme ke tema terang dan gelap pada 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,
  );
}

Tanpa animasi di iOS

Dengan animasi di iOS

Terjadi masalah?

Jika aplikasi tidak berjalan dengan semestinya, gunakan kode di link berikut untuk membuatnya kembali normal.

Menambahkan status pengarahan kursor

Salah satu cara untuk menambahkan gerakan ke aplikasi desktop adalah dengan status pengarahan kursor, dengan widget yang mengubah statusnya (seperti warna, bentuk, atau konten) saat pengguna mengarahkan kursor ke widget.

Secara default, class _OutlinedCardState (digunakan untuk kartu playlist "baru diputar") menampilkan MouseRegion, yang mengubah panah kursor menjadi pointer saat kursor diarahkan. Namun, Anda dapat menambahkan lebih banyak masukan visual.

a3c16fc17be25f6c.png Buka lib/src/shared/views/outlined_card.dart dan ganti kontennya dengan penerapan berikut untuk memperkenalkan status _hovered.

import 'package:flutter/material.dart';

class OutlinedCard extends StatefulWidget {
  const OutlinedCard({
    Key? key,
    required this.child,
    this.clickable = true,
  }) : super(key: key);
  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 Lakukan hot reload pada aplikasi, lalu arahkan kursor ke salah satu kartu playlist baru diputar.

61c08e46a5926e10.gif

OutlinedCard mengubah opasitas dan membulatkan sudut.

a3c16fc17be25f6c.png Terakhir, animasikan nomor lagu pada playlist menjadi tombol putar menggunakan widget HoverableSongPlayButton yang ditentukan dalam lib/src/shared/views/hoverable_song_play_button.dart. Di lib/src/features/playlists/view/playlist_songs.dart, gabungkan widget Center (yang berisi nomor lagu) dengan HoverableSongPlayButton:

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.png Lakukan hot reload pada aplikasi, lalu arahkan kursor ke nomor lagu di playlist Lagu Teratas Hari Ini atau Rilis Baru.

Angka akan berubah menjadi tombol putar yang memutar lagu saat Anda mengkliknya.

82587ceb5452eedf.gif

Lihat kode project final di GitHub.

8 Selamat!

Anda telah menyelesaikan codelab ini. Anda telah mempelajari bahwa ada banyak perubahan kecil yang dapat diintegrasikan ke dalam aplikasi untuk membuat tampilannya lebih menarik, juga lebih mudah diakses, lebih mudah dilokalkan, dan lebih sesuai untuk beberapa platform. Teknik tersebut mencakup, tetapi tidak terbatas pada:

  • Tipografi: Teks lebih dari sekadar alat komunikasi. Menggunakan tampilan teks untuk menghasilkan efek positif pada pengalaman dan persepsi pengguna terhadap aplikasi Anda.
  • Tema: Membuat sistem desain yang dapat digunakan dengan andal tanpa harus membuat keputusan desain untuk setiap widget.
  • Adaptasi: Mempertimbangkan perangkat dan platform tempat pengguna menjalankan aplikasi Anda beserta kemampuannya. Pertimbangkan ukuran layar dan cara aplikasi ditampilkan.
  • Gerakan dan animasi: Menambahkan gerakan ke aplikasi Anda akan menambah energi pada pengalaman pengguna dan, lebih praktisnya, memberikan masukan untuk pengguna.

Dengan sedikit penyesuaian kecil, tampilan aplikasi bisa menjadi lebih menarik:

Sebelum

Sesudah

Langkah berikutnya

Semoga Anda telah belajar banyak hal terkait cara mem-build aplikasi yang terlihat menarik di Flutter.

Jika Anda menerapkan tips atau trik yang disebutkan di sini (atau memiliki tips untuk Anda sendiri), kami ingin mendengar pendapat Anda. Hubungi kami melalui Twitter di @rodydavis dan @khanhnwin.

Referensi berikut mungkin juga berguna bagi Anda.

Tema

Referensi terkait desain adaptif dan responsif:

Referensi desain umum:

Selain itu, ikuti komunitas Flutter.

Teruslah berkreasi dan jadikan dunia aplikasi semakin menarik!