Rabu, 26 Juni 2024
Selasa, 25 Juni 2024
FlutterMusicApp
Kali ini saya akan membuat flutter music App.. Untuk memulai proyek ini pertama kita harus mendapatkan kode awal terlebih dahulu dengan clone dari github codelab berikut:
Pertama buka lib/src/features/home/view/home_screen.dart, yang berisi hal berikut:
git clone https://github.com/flutter/codelabs.git cd codelabs/boring_to_beautiful/step_01/
Setelah berhasi di clone dan di jalankan, anda akan melihat layar utama myArtist dalam tampilan desktop namu tidak rapi dalam tampilan android.
kita akan memperbaikinya.
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.
Selanjutnya kita akan menggunakan tipografi agar memberikan kesan ramah dan menyenangkan kepada user.
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',
),
];
Dengan kode di atas anda akan mendapatkan tampilan seperti berikut:
Langkah selanjutnya menggunakan font, pilih font yang menetukan karakteristik aplikasi yang akan di bagun. Font yang akan di guanakan adalah Montserrat dari google Fonts.
Untuk menambahkan font caranya sebagai berikut:
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
mendapatkan kode awal terlebih dahulu dengan clone dari github codelab berikut:Lakukan hot reload pada
untuk mengaktifkan perubahan. (Gunakan tombol di IDE Anda atau, dari command line, masukkan r untuk melakukan hot reload):

Selanjutnya kita akan mentapkan tema, fungsinya untuk membantu menghadirkan desain dan kesegaran terstruktur ke dalam aplikasi dengan menentukan sistem kumpulan warna dan teks.
Kita bisa membuat komponen bertema kustom dengan dua cara:
- Membuat masing-masing widget kustom dengan temanya sendiri.
- Membuat tema cakupan untuk widget default.
Kita akan menggunakan opsi kedua, kita menggunakan penyedia tema pada 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,
);
},
),
)),
),
);
}
}
mendapatkan kode awal terlebih dahulu dengan clone dari github codelab berikut:Lakukan hot reload
Setelah tema sudah siap, kita pilih warna untuk aplikasi, 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
Disisi lain, token desain yang baru mendukung tema terang dan gelap;
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.
Setelah itu, kita akan menambahkan desain yang adaptif.
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.
},
);
}
}
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.
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;
}
Dari kode di atas, 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,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Selanjutnya kita akan menggunakan spasi kosong untuk menciptakan jeda terstruktur di atara bagian.
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:
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
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.
utama ke 35 dengan EdgeInsets.all(35), tetapi Anda juga dapat menyetel padding untuk setiap tepi
Selanjutnya kita akan menambahkan animasi gerakan. Untuk meberikan kesan aplikasi yang dinamis dan penuh semangat.
Pertama kita akan membuat animasi 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,
);
}
Dengan Animasi di iOS
Tanpa animasi di iOS
Selanjutnya kita akan memberikan 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 reload pada aplikasi.
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,
),
),
),
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.
Kini aplikasi yang sudah di buat akan nampak seperti berikut:
Source code dapat di lihat pada: https://github.com/khalidjuna/flutter-music-app.git
Atau bisa langsung pada: https://github.com/flutter/codelabs.git
Langganan:
Postingan (Atom)
-
Pada kesempatan ini saya akan meampilkan ucapan happy birthday dengan menggunakan jetpack compose. Jetpack Compose adalah toolkit modern unt...