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:
- Ikuti Tur Framework Widget Flutter
- Coba ikuti codelab Menulis Aplikasi Flutter Pertama Anda, bagian 1
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:
Apa yang ingin Anda pelajari dari codelab ini?
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.
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.
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:
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.
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',
),
];
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. 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.
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>
Di lib/src/shared/extensions.dart
, impor paket baru:
import 'package:google_fonts/google_fonts.dart'; // Add this line.
Tetapkan font Montserrat TextTheme:
TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line
Lakukan hot reload pada untuk mengaktifkan perubahan. (Gunakan tombol di IDE Anda atau, dari command line, masukkan r
untuk melakukan hot reload):
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);
}
}
Untuk 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.
Untuk 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.)
Teruskan 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;
Untuk 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:
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.
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.
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
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.
},
);
}
}
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
Tata 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,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
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:
Berikutnya, Anda akan menambahkan spasi kosong ke layar utama untuk memberikan lebih banyak ruang. Anda kemudian akan menyesuaikan tata letak untuk mengatur jaraknya.
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
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.
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'),
),
),
),
],
);
}
}
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.
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,
),
],
),
),
],
),
),
Lakukan hot reload pada aplikasi. Aplikasi akan terlihat seperti berikut:
Sekarang ada banyak ruang untuk konten layar utama, tetapi tampilannya terlihat terlalu terpisah dan tidak ada kohesi di antara bagian.
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 bawahEdgeInsets.symmetric()
menetapkan padding agar jarak vertikal (atas dan bawah) menjadi setara dan jarak horizontal (kiri dan kanan) menjadi setaraEdgeInsets.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,
),
),
],
),
),
],
),
),
),
],
),
),
);
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'),
),
),
),
],
);
}
}
Lakukan hot reload pada aplikasi. Tata letak dan spasinya akan terlihat jauh lebih baik. Untuk sentuhan akhir, tambahkan gerakan dan animasi.
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(),
},
);
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.
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,
),
),
);
}
}
Lakukan hot reload pada aplikasi, lalu arahkan kursor ke salah satu kartu playlist baru diputar.
OutlinedCard
mengubah opasitas dan membulatkan sudut.
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
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.
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
- Material Theme Builder (alat)
Referensi terkait desain adaptif dan responsif:
- Mendekode Flutter dalam hal Adaptif vs Responsif (video)
- Adaptive layouts (video dari The Boring Flutter Development Show)
- Membuat aplikasi yang responsif dan adaptif (flutter.dev)
- Komponen Materi Adaptif untuk Flutter (library di GitHub)
- 5 things you can do to prepare your app for large screens (video dari Google I/O 2021)
Referensi desain umum:
- The little things: Becoming the mythical designer-developer (video dari Flutter Engage)
- Desain Material 3 untuk Perangkat Foldable (material.io)
Selain itu, ikuti komunitas Flutter.
Teruslah berkreasi dan jadikan dunia aplikasi semakin menarik!