Skip to content

Instantly share code, notes, and snippets.

@jinyongp
Created June 28, 2023 15:45
Show Gist options
  • Save jinyongp/252f87e790119799f8be13d1c7875a6d to your computer and use it in GitHub Desktop.
Save jinyongp/252f87e790119799f8be13d1c7875a6d to your computer and use it in GitHub Desktop.
Flutter Shazam App Clone
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Shazam',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return DefaultTabController(
initialIndex: 1,
length: 3,
child: Builder(builder: (context) {
DefaultTabController.of(context).addListener(() {
setState(() {});
});
return Scaffold(
body: Stack(
children: [
const TabBarView(
children: [
FirstTab(),
SecondTab(),
ThirdTab(),
],
),
tabIndicator(context),
],
),
);
}),
);
}
Widget tabIndicator(BuildContext context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
child: Column(
children: [
Container(
alignment: Alignment.topCenter,
child: TabPageSelector(
color: DefaultTabController.of(context).index == 1
? Colors.blue[300]
: Colors.grey[400],
selectedColor: DefaultTabController.of(context).index == 1
? Colors.white
: Colors.blue,
indicatorSize: 8,
),
),
],
),
),
);
}
}
class FirstTab extends StatelessWidget {
const FirstTab({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
const menus = [
{'icon': Icons.ac_unit, 'title': 'Shazam'},
{'icon': Icons.person, 'title': '아티스트'},
{'icon': Icons.music_note, 'title': '회원님을 위한 재생 목록'},
];
const songs = [
{
'imageUrl': 'https://i.ytimg.com/vi/jAO0KXRdz_4/hqdefault.jpg',
'title': '가을밤에 든 생각',
'artist': '잔나비',
},
{
'imageUrl': 'https://i.ytimg.com/vi/jAO0KXRdz_4/hqdefault.jpg',
'title': '가을밤에 든 생각',
'artist': '잔나비',
},
{
'imageUrl': 'https://i.ytimg.com/vi/jAO0KXRdz_4/hqdefault.jpg',
'title': '가을밤에 든 생각',
'artist': '잔나비',
},
{
'imageUrl': 'https://i.ytimg.com/vi/jAO0KXRdz_4/hqdefault.jpg',
'title': '가을밤에 든 생각',
'artist': '잔나비',
},
{
'imageUrl': 'https://i.ytimg.com/vi/jAO0KXRdz_4/hqdefault.jpg',
'title': '가을밤에 든 생각',
'artist': '잔나비',
},
{
'imageUrl': 'https://i.ytimg.com/vi/jAO0KXRdz_4/hqdefault.jpg',
'title': '가을밤에 든 생각',
'artist': '잔나비',
},
];
return SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () {},
icon: const Icon(
Icons.settings,
)),
),
const Text("라이브러리",
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 16)),
const SizedBox(width: 24),
],
),
const SizedBox(height: 20),
Column(
children: menus.map((menu) {
return Column(
children: [
Row(children: [
Icon(menu['icon'] as IconData),
const SizedBox(width: 8),
Text(menu['title'] as String,
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.w500))
]),
menus.last != menu
? const Divider(height: 20, thickness: 0.5)
: const SizedBox(),
],
);
}).toList(),
),
const SizedBox(height: 20),
const Text("최근 Shazam",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700)),
const SizedBox(height: 16),
Expanded(
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1 / 1.05,
),
itemCount: songs.length,
itemBuilder: (context, index) {
return songListItem(songs[index]);
}),
)
],
),
));
}
Widget songListItem(Map<String, String> song) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
song['imageUrl'] as String,
fit: BoxFit.cover,
),
Container(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(song['title'] as String,
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Text(song['artist'] as String,
style: const TextStyle(
fontSize: 12, fontWeight: FontWeight.w400)),
],
),
),
],
),
),
),
],
);
}
}
class SecondTab extends StatelessWidget {
const SecondTab({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.blue.shade200, Colors.blueAccent, Colors.blue.shade900],
)),
child: Stack(
children: [
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
child: Row(children: [
GestureDetector(
child: const Column(children: [
Icon(Icons.person, color: Colors.white, size: 28),
Text("라이브러리", style: TextStyle(color: Colors.white))
]),
onTap: () {
DefaultTabController.of(context).animateTo(0);
},
),
const Expanded(child: SizedBox()),
GestureDetector(
child: const Column(
children: [
Icon(Icons.show_chart_rounded,
color: Colors.white, size: 28),
Text("차트", style: TextStyle(color: Colors.white))
],
),
onTap: () {
DefaultTabController.of(context).animateTo(2);
},
),
]),
)),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Shazam하려면 탭하세요.",
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.w600)),
const SizedBox(height: 50),
Row(
children: [
Expanded(flex: 1, child: Container()),
Expanded(
flex: 2,
child: ClipRRect(
borderRadius: BorderRadius.circular(9999),
child: Image.network(
"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c0/Shazam_icon.svg/800px-Shazam_icon.svg.png",
fit: BoxFit.fitWidth,
color: Colors.blue.shade300,
colorBlendMode: BlendMode.lighten,
),
),
),
Expanded(flex: 1, child: Container()),
],
),
const SizedBox(height: 100),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue.shade300,
),
child:
const Icon(Icons.search, color: Colors.white, size: 28)),
],
)
],
),
);
}
}
class ThirdTab extends StatelessWidget {
const ThirdTab({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
const chartData = {
'korea': [
{
'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
'name': 'Dynamite',
'artist': 'BTS',
},
{
'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
'name': 'Dynamite',
'artist': 'BTS',
},
{
'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
'name': 'Dynamite',
'artist': 'BTS',
},
],
'global': [
{
'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
'name': 'Dynamite',
'artist': 'BTS',
},
{
'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
'name': 'Dynamite',
'artist': 'BTS',
},
{
'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
'name': 'Dynamite',
'artist': 'BTS',
},
],
'newyork': [
{
'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
'name': 'Dynamite',
'artist': 'BTS',
},
{
'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
'name': 'Dynamite',
'artist': 'BTS',
},
{
'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
'name': 'Dynamite',
'artist': 'BTS',
},
],
};
return SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Column(
children: [
const Center(
child: Text("차트",
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 16)),
),
const SizedBox(
height: 20,
),
Expanded(
child: ListView(
children: [
header(),
Divider(height: 10, thickness: 10, color: Colors.grey.shade300),
listItem(chartData['korea']!, "대한민국 차트"),
Divider(height: 10, thickness: 10, color: Colors.grey.shade300),
listItem(chartData['global']!, "글로벌 차트"),
Divider(height: 10, thickness: 10, color: Colors.grey.shade300),
listItem(chartData['newyork']!, "뉴욕 차트"),
],
),
)
],
),
));
}
Widget header() {
return Container(
color: Colors.deepPurple,
width: double.infinity,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 50.0, horizontal: 30),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Container(
color: Colors.white,
child: const ButtonBar(
alignment: MainAxisAlignment.center,
buttonPadding: EdgeInsets.all(10),
children: [
Text("국가 및 도시별 차트",
style: TextStyle(
color: Colors.deepPurple,
fontWeight: FontWeight.w600,
fontSize: 16)),
],
),
),
),
const SizedBox(height: 20),
const Text("전 세계",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 16)),
],
),
),
);
}
Widget listItem(List<Map<String, String>> data, String chartName) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(chartName, style: const TextStyle(fontSize: 16)),
const Text("모두 보기",
style: TextStyle(color: Colors.blue, fontSize: 14)),
],
),
),
const SizedBox(height: 8),
Row(
children: data.map((datum) {
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: chartItem(datum),
));
}).toList(),
)
],
),
);
}
Widget chartItem(Map<String, String> datum) {
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Image.network(
datum['imageUrl'] as String,
),
Text(datum['name'] as String,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Text(datum['artist'] as String),
]);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment