[PPB] Tugas 13 - Membuat Aplikasi Flutter Music
Nama : Ichlasul Hasanat
NRP : 5025201091
Kelas : PPB - I
----
Pada pertemuan kali ini, kami diberikan tugas untuk membuat aplikasi Flutter Music menggunakan Flutter. Berikut hasil source code beserta output dari aplikasi tersebut:
Github: Repository
Source Code:
playlist_home_screen.dart
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}'),
);
},
),
),
],
),
);
},
);
}
}
playlist_screen.dart
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,
),
),
),
],
),
);
});
}
}
playlist_songs.dart
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(
HoverableSongPlayButton(
hoverMode: HoverMode.overlay,
song: playlist.songs[index],
child: Center(
child: Text(
(index + 1).toString(),
textAlign: TextAlign.center,
),
),
),
),
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()),
);
},
);
}
}
theme.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
class NoAnimationPageTransitionsBuilder extends PageTransitionsBuilder {
const NoAnimationPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return child;
}
}
class ThemeSettingChange extends Notification {
ThemeSettingChange({required this.settings});
final ThemeSettings settings;
}
class ThemeProvider extends InheritedWidget {
const ThemeProvider(
{super.key,
required this.settings,
required this.lightDynamic,
required this.darkDynamic,
required super.child});
final ValueNotifier<ThemeSettings> settings;
final ColorScheme? lightDynamic;
final ColorScheme? darkDynamic;
final pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: NoAnimationPageTransitionsBuilder(),
TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(),
TargetPlatform.windows: NoAnimationPageTransitionsBuilder(),
},
);
Color custom(CustomColor custom) {
if (custom.blend) {
return blend(custom.color);
} else {
return custom.color;
}
}
Color blend(Color targetColor) {
return Color(
Blend.harmonize(targetColor.value, settings.value.sourceColor.value));
}
Color source(Color? target) {
Color source = settings.value.sourceColor;
if (target != null) {
source = blend(target);
}
return source;
}
ColorScheme colors(Brightness brightness, Color? targetColor) {
final dynamicPrimary = brightness == Brightness.light
? lightDynamic?.primary
: darkDynamic?.primary;
return ColorScheme.fromSeed(
seedColor: dynamicPrimary ?? source(targetColor),
brightness: brightness,
);
}
ShapeBorder get shapeMedium => RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
);
CardTheme cardTheme() {
return CardTheme(
elevation: 0,
shape: shapeMedium,
clipBehavior: Clip.antiAlias,
);
}
ListTileThemeData listTileTheme(ColorScheme colors) {
return ListTileThemeData(
shape: shapeMedium,
selectedColor: colors.secondary,
);
}
AppBarTheme appBarTheme(ColorScheme colors) {
return AppBarTheme(
elevation: 0,
backgroundColor: colors.surface,
foregroundColor: colors.onSurface,
);
}
TabBarTheme tabBarTheme(ColorScheme colors) {
return TabBarTheme(
labelColor: colors.secondary,
unselectedLabelColor: colors.onSurfaceVariant,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
color: colors.secondary,
width: 2,
),
),
),
);
}
BottomAppBarTheme bottomAppBarTheme(ColorScheme colors) {
return BottomAppBarTheme(
color: colors.surface,
elevation: 0,
);
}
BottomNavigationBarThemeData bottomNavigationBarTheme(ColorScheme colors) {
return BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: colors.surfaceContainerHighest,
selectedItemColor: colors.onSurface,
unselectedItemColor: colors.onSurfaceVariant,
elevation: 0,
landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
);
}
NavigationRailThemeData navigationRailTheme(ColorScheme colors) {
return const NavigationRailThemeData();
}
DrawerThemeData drawerTheme(ColorScheme colors) {
return DrawerThemeData(
backgroundColor: colors.surface,
);
}
ThemeData light([Color? targetColor]) {
final 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);
}
}
Output Code:
Komentar
Posting Komentar