Membuat Aplikasi Flutter Music
PPB I - Tugas 13
Membuat Aplikasi Flutter Music
Hai semuanya ! Pada kesempatan ini, kita akan mencoba membuat aplikasi pemutar musik yang memungkinkan penggemar untuk terus mengikuti kabar terbaru dari artis favorit mereka. Aplikasi ini akan menampilkan foto dan deskripsi artis, daftar album lagu, serta komentar dari penggemar. Pengguna dapat melihat profil artis, mendengarkan lagu-lagu terbaru, dan membaca atau memberikan komentar. Berikut adalah beberapa kode program penting untuk pengembangan aplikasi ini.
Kode implementasi yang digunakan untuk menampilkan halaman home.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2022 The Flutter Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style license that can be | |
// found in the LICENSE file. | |
import 'package:flutter/material.dart'; | |
import 'package:go_router/go_router.dart'; | |
import '../../../shared/classes/classes.dart'; | |
import '../../../shared/providers/providers.dart'; | |
import '../../../shared/views/views.dart'; | |
class PlaylistHomeScreen extends StatelessWidget { | |
const PlaylistHomeScreen({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
PlaylistsProvider playlistProvider = PlaylistsProvider(); | |
List<Playlist> playlists = playlistProvider.playlists; | |
return LayoutBuilder( | |
builder: (context, constraints) { | |
return Scaffold( | |
primary: false, | |
appBar: AppBar( | |
title: const Text('PLAYLISTS'), | |
toolbarHeight: kToolbarHeight * 2, | |
), | |
body: Column( | |
children: [ | |
Expanded( | |
child: GridView.builder( | |
padding: const EdgeInsets.all(15), | |
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( | |
crossAxisCount: (constraints.maxWidth ~/ 175).toInt(), | |
childAspectRatio: 0.70, | |
mainAxisSpacing: 10, | |
crossAxisSpacing: 10, | |
), | |
itemCount: playlists.length, | |
itemBuilder: (context, index) { | |
final playlist = playlists[index]; | |
return GestureDetector( | |
child: ImageTile( | |
image: playlist.cover.image, | |
title: playlist.title, | |
subtitle: playlist.description, | |
), | |
onTap: () => | |
GoRouter.of(context).go('/playlists/${playlist.id}'), | |
); | |
}, | |
), | |
), | |
], | |
), | |
); | |
}, | |
); | |
} | |
} |
Kode implementasi yang digunakan untuk menampilkan halaman playlist.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2022 The Flutter Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style license that can be | |
// found in the LICENSE file. | |
import 'dart:math'; | |
import 'package:flutter/material.dart'; | |
import 'package:go_router/go_router.dart'; | |
import '../../../shared/classes/classes.dart'; | |
import '../../../shared/extensions.dart'; | |
import '../../../shared/views/adaptive_image_card.dart'; | |
import '../../../shared/views/views.dart'; | |
import 'playlist_songs.dart'; | |
class PlaylistScreen extends StatelessWidget { | |
const PlaylistScreen({required this.playlist, super.key}); | |
final Playlist playlist; | |
@override | |
Widget build(BuildContext context) { | |
return LayoutBuilder(builder: (context, constraints) { | |
final colors = Theme.of(context).colorScheme; | |
final double headerHeight = constraints.isMobile | |
? max(constraints.biggest.height * 0.5, 450) | |
: max(constraints.biggest.height * 0.25, 250); | |
if (constraints.isMobile) { | |
return Scaffold( | |
appBar: AppBar( | |
leading: BackButton( | |
onPressed: () => GoRouter.of(context).go('/playlists'), | |
), | |
title: Text(playlist.title), | |
actions: [ | |
IconButton( | |
icon: const Icon(Icons.play_circle_fill), | |
onPressed: () {}, | |
), | |
IconButton( | |
onPressed: () {}, | |
icon: const Icon(Icons.shuffle), | |
), | |
], | |
), | |
body: ArticleContent( | |
child: PlaylistSongs( | |
playlist: playlist, | |
constraints: constraints, | |
), | |
), | |
); | |
} | |
return Scaffold( | |
body: CustomScrollView( | |
slivers: [ | |
SliverAppBar( | |
leading: BackButton( | |
onPressed: () => GoRouter.of(context).go('/playlists'), | |
), | |
expandedHeight: headerHeight, | |
pinned: false, | |
flexibleSpace: FlexibleSpaceBar( | |
background: AdaptiveImageCard( | |
axis: constraints.isMobile ? Axis.vertical : Axis.horizontal, | |
constraints: | |
constraints.copyWith(maxHeight: headerHeight).normalize(), | |
image: playlist.cover.image, | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.end, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Text( | |
'PLAYLIST', | |
style: context.titleSmall! | |
.copyWith(color: colors.onSurface), | |
), | |
Text( | |
playlist.title, | |
style: context.displaySmall! | |
.copyWith(color: colors.onSurface), | |
), | |
Text( | |
playlist.description, | |
style: context.bodyLarge!.copyWith( | |
color: colors.onSurface.withOpacity(0.8), | |
), | |
), | |
const SizedBox(height: 8), | |
Row( | |
children: [ | |
IconButton( | |
icon: Icon( | |
Icons.play_circle_fill, | |
color: colors.tertiary, | |
), | |
onPressed: () {}, | |
), | |
TextButton.icon( | |
onPressed: () {}, | |
icon: Icon( | |
Icons.shuffle, | |
color: colors.tertiary, | |
), | |
label: Text( | |
'Shuffle', | |
style: context.bodySmall!.copyWith( | |
color: colors.tertiary, | |
), | |
), | |
), | |
], | |
), | |
], | |
), | |
), | |
), | |
), | |
SliverToBoxAdapter( | |
child: ArticleContent( | |
child: PlaylistSongs( | |
playlist: playlist, | |
constraints: constraints, | |
), | |
), | |
), | |
], | |
), | |
); | |
}); | |
} | |
} |
Kode implementasi yang digunakan untuk menampilkan halaman artist.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2022 The Flutter Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style license that can be | |
// found in the LICENSE file. | |
import 'package:flutter/material.dart'; | |
import 'package:flutter_bloc/flutter_bloc.dart'; | |
import '../../../shared/classes/classes.dart'; | |
import '../../../shared/extensions.dart'; | |
import '../../../shared/playback/bloc/bloc.dart'; | |
import '../../../shared/views/image_clipper.dart'; | |
import '../../../shared/views/views.dart'; | |
class PlaylistSongs extends StatelessWidget { | |
const PlaylistSongs( | |
{super.key, required this.playlist, required this.constraints}); | |
final Playlist playlist; | |
final BoxConstraints constraints; | |
@override | |
Widget build(BuildContext context) { | |
return AdaptiveTable<Song>( | |
items: playlist.songs, | |
breakpoint: 450, | |
columns: const [ | |
DataColumn( | |
label: Padding( | |
padding: EdgeInsets.only(left: 20), | |
child: Text('#'), | |
), | |
), | |
DataColumn( | |
label: Text('Title'), | |
), | |
DataColumn( | |
label: Padding( | |
padding: EdgeInsets.only(right: 10), | |
child: Text('Length'), | |
), | |
), | |
], | |
rowBuilder: (context, index) => DataRow.byIndex( | |
index: index, | |
cells: [ | |
DataCell( | |
// Add 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 | |
), | |
DataCell( | |
Row(children: [ | |
Padding( | |
padding: const EdgeInsets.all(2), | |
child: ClippedImage(playlist.songs[index].image.image), | |
), | |
const SizedBox(width: 10), | |
Expanded(child: Text(playlist.songs[index].title)), | |
]), | |
), | |
DataCell( | |
Text(playlist.songs[index].length.toHumanizedString()), | |
), | |
], | |
), | |
itemBuilder: (song, index) { | |
return ListTile( | |
onTap: () => BlocProvider.of<PlaybackBloc>(context).add( | |
PlaybackEvent.changeSong(song), | |
), | |
leading: ClippedImage(song.image.image), | |
title: Text(song.title), | |
subtitle: Text(song.length.toHumanizedString()), | |
); | |
}, | |
); | |
} | |
} |
Selain itu, pada implementasi kali ini, aplikasi memiliki dua mode yaitu mode dark dan light. Adapun implementasinya dapat kita lihat melalui kode berikut.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2022 The Flutter Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style license that can be | |
// found in the LICENSE file. | |
import 'dart:math'; | |
import 'package:flutter/material.dart'; | |
import 'package:material_color_utilities/material_color_utilities.dart'; | |
class NoAnimationPageTransitionsBuilder extends PageTransitionsBuilder { | |
const NoAnimationPageTransitionsBuilder(); | |
@override | |
Widget buildTransitions<T>( | |
PageRoute<T> route, | |
BuildContext context, | |
Animation<double> animation, | |
Animation<double> secondaryAnimation, | |
Widget child, | |
) { | |
return child; | |
} | |
} | |
class ThemeSettingChange extends Notification { | |
ThemeSettingChange({required this.settings}); | |
final ThemeSettings settings; | |
} | |
class ThemeProvider extends InheritedWidget { | |
const ThemeProvider( | |
{super.key, | |
required this.settings, | |
required this.lightDynamic, | |
required this.darkDynamic, | |
required super.child}); | |
final ValueNotifier<ThemeSettings> settings; | |
final ColorScheme? lightDynamic; | |
final ColorScheme? darkDynamic; | |
final pageTransitionsTheme = const PageTransitionsTheme( | |
builders: <TargetPlatform, PageTransitionsBuilder>{ | |
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), | |
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), | |
TargetPlatform.linux: NoAnimationPageTransitionsBuilder(), | |
TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(), | |
TargetPlatform.windows: NoAnimationPageTransitionsBuilder(), | |
}, | |
); | |
Color custom(CustomColor custom) { | |
if (custom.blend) { | |
return blend(custom.color); | |
} else { | |
return custom.color; | |
} | |
} | |
Color blend(Color targetColor) { | |
return Color( | |
Blend.harmonize(targetColor.value, settings.value.sourceColor.value)); | |
} | |
Color source(Color? target) { | |
Color source = settings.value.sourceColor; | |
if (target != null) { | |
source = blend(target); | |
} | |
return source; | |
} | |
ColorScheme colors(Brightness brightness, Color? targetColor) { | |
final dynamicPrimary = brightness == Brightness.light | |
? lightDynamic?.primary | |
: darkDynamic?.primary; | |
return ColorScheme.fromSeed( | |
seedColor: dynamicPrimary ?? source(targetColor), | |
brightness: brightness, | |
); | |
} | |
ShapeBorder get shapeMedium => RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(8), | |
); | |
CardTheme cardTheme() { | |
return CardTheme( | |
elevation: 0, | |
shape: shapeMedium, | |
clipBehavior: Clip.antiAlias, | |
); | |
} | |
ListTileThemeData listTileTheme(ColorScheme colors) { | |
return ListTileThemeData( | |
shape: shapeMedium, | |
selectedColor: colors.secondary, | |
); | |
} | |
AppBarTheme appBarTheme(ColorScheme colors) { | |
return AppBarTheme( | |
elevation: 0, | |
backgroundColor: colors.surface, | |
foregroundColor: colors.onSurface, | |
); | |
} | |
TabBarTheme tabBarTheme(ColorScheme colors) { | |
return TabBarTheme( | |
labelColor: colors.secondary, | |
unselectedLabelColor: colors.onSurfaceVariant, | |
indicator: BoxDecoration( | |
border: Border( | |
bottom: BorderSide( | |
color: colors.secondary, | |
width: 2, | |
), | |
), | |
), | |
); | |
} | |
BottomAppBarTheme bottomAppBarTheme(ColorScheme colors) { | |
return BottomAppBarTheme( | |
color: colors.surface, | |
elevation: 0, | |
); | |
} | |
BottomNavigationBarThemeData bottomNavigationBarTheme(ColorScheme colors) { | |
return BottomNavigationBarThemeData( | |
type: BottomNavigationBarType.fixed, | |
backgroundColor: colors.surfaceContainerHighest, | |
selectedItemColor: colors.onSurface, | |
unselectedItemColor: colors.onSurfaceVariant, | |
elevation: 0, | |
landscapeLayout: BottomNavigationBarLandscapeLayout.centered, | |
); | |
} | |
NavigationRailThemeData navigationRailTheme(ColorScheme colors) { | |
return const NavigationRailThemeData(); | |
} | |
DrawerThemeData drawerTheme(ColorScheme colors) { | |
return DrawerThemeData( | |
backgroundColor: colors.surface, | |
); | |
} | |
ThemeData light([Color? targetColor]) { | |
final colorScheme = colors(Brightness.light, targetColor); | |
return ThemeData.light(useMaterial3: true).copyWith( | |
pageTransitionsTheme: pageTransitionsTheme, | |
colorScheme: colorScheme, | |
appBarTheme: appBarTheme(colorScheme), | |
cardTheme: cardTheme(), | |
listTileTheme: listTileTheme(colorScheme), | |
bottomAppBarTheme: bottomAppBarTheme(colorScheme), | |
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme), | |
navigationRailTheme: navigationRailTheme(colorScheme), | |
tabBarTheme: tabBarTheme(colorScheme), | |
drawerTheme: drawerTheme(colorScheme), | |
scaffoldBackgroundColor: colorScheme.surface, | |
); | |
} | |
ThemeData dark([Color? targetColor]) { | |
final colorScheme = colors(Brightness.dark, targetColor); | |
return ThemeData.dark(useMaterial3: true).copyWith( | |
pageTransitionsTheme: pageTransitionsTheme, | |
colorScheme: colorScheme, | |
appBarTheme: appBarTheme(colorScheme), | |
cardTheme: cardTheme(), | |
listTileTheme: listTileTheme(colorScheme), | |
bottomAppBarTheme: bottomAppBarTheme(colorScheme), | |
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme), | |
navigationRailTheme: navigationRailTheme(colorScheme), | |
tabBarTheme: tabBarTheme(colorScheme), | |
drawerTheme: drawerTheme(colorScheme), | |
scaffoldBackgroundColor: colorScheme.surface, | |
); | |
} | |
ThemeMode themeMode() { | |
return settings.value.themeMode; | |
} | |
ThemeData theme(BuildContext context, [Color? targetColor]) { | |
final brightness = MediaQuery.of(context).platformBrightness; | |
return brightness == Brightness.light | |
? light(targetColor) | |
: dark(targetColor); | |
} | |
static ThemeProvider of(BuildContext context) { | |
return context.dependOnInheritedWidgetOfExactType<ThemeProvider>()!; | |
} | |
@override | |
bool updateShouldNotify(covariant ThemeProvider oldWidget) { | |
return oldWidget.settings != settings; | |
} | |
} | |
class ThemeSettings { | |
ThemeSettings({ | |
required this.sourceColor, | |
required this.themeMode, | |
}); | |
final Color sourceColor; | |
final ThemeMode themeMode; | |
} | |
Color randomColor() { | |
return Color(Random().nextInt(0xFFFFFFFF)); | |
} | |
// 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); | |
} | |
} |
Sekian implementasi dari Flutter Music App kali ini. Hope you enjoy and always be happy guys!!
Komentar
Posting Komentar