6일차와 마찬가지로 현재 서비스 중인 앱 중 하나의 UI를 구현해 보았다. 추가로 앱 화면 구현 이후에 TextField 위젯과 중요한 Dart에서의 이벤트와 함수에 대해 학습했다.
학습한 내용
- 유튜브뮤직 앱 화면 따라 만들기
- TextField Widget
- Dart Event
- Dart 함수
Flexible
과 Expanded
위젯은 크기를 확장 시키는 기능을하는 위젯들로 비슷하다. 하지만 차이점이 있는데 Flexible
위젯은 child의 크기가 부모보다 작은 경우에는 크기 변화를 하지 않는다는 점이다. 이를 정리하면 아래와 같다.
Flexible | Expanded | |
---|---|---|
child가 부모보다 큰 경우 | 최대 사이즈로 확장 | 최대 사이즈로 확장 |
child가 부모보다 작은 경우 | 변화 없음 | 최대 사이즈로 확장 |
즉, Flexible
은 Expanded
보다 많은 설정을 할 수 있고, Flexible
을 사용해 Expanded
와 같은 UI를 구성할 수 도 있다. 자세한 사용법은 아래의 링크를 참고
[Flutter] 플러터 Expanded? 익스펜디드 Flexible? 플렉서블
text의 overflow는 텍스트의 길이가 부모의 영역보다 길어서 발생한다. 이는 아래와 같의 Text
위젯의 overflow
속성을 사용해 해결할 수 있다.
Text(
"This is a long text",
overflow: TextOverflow.ellipsis,
),
Text(
"This is a long text",
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
),
Text(
"This is a long text",
overflow: TextOverflow.clip,
maxLines: 1,
softWrap: false,
),
Wrap
자식들을 줄이나 행으로 배치하고 공간이 부족해 지면 자동으로 줄이나 행을 바꿔준다.
Wrap
의 속성을 사용해 상하 좌우의 공간, 정렬, 방향 등을 설정할 수 있다.
자세한 사용법은 아래의 공식 문서를 통해 확인 가능하다.
Wrap class
Dart에서 발생하는 이벤트의 이름에는 주로 on이 앞에 붙는다. 예를 들어 onChanged
onPressed
등이 있다.
함수는 수행하는 기능을 알아볼 수 있도록 네이밍 한다. 이벤트를 핸들링하는 핸들러는 보통 이름 앞에 handle이 붙는데 _handleOnChanged()
와 같이 생성하는 것이 좋다.
- 유튜브 뮤직 앱 화면 제작
아래의 화면과 같은 유튜브 뮤직 앱 화면을 제작하고자 한다.
사용할 데이터와 요구사항은 다음과 같다.
- Data
Come with me - Surfaces 및 salem ilese
Good day - Surfaces
Honesty - Pink Sweat$
I Wish I Missed My Ex - 마할리아 버크마
Plastic Plants - 마할리아 버크마
Sucker for you - 맷 테리
Summer is for falling in love - Sarah Kang & Eye Love Brandon
These days(feat. Jess Glynne, Macklemore & Dan Caplen) - Rudimental
You Make Me - DAY6
Zombie Pop - DPR IAN
- Requirements
- 음악명은 최대 2줄까지만 가능하다.
- 가수명과 플레이시간은 최대 1줄까지만 가능하며 필요한 경우 가수명을 줄인다.
- 음악의 정보를 보여주는 위젯을 만들고, 이름은 MusicTile로 한다.
import 'package:flutter/material.dart';
class MusicTile extends StatelessWidget {
const MusicTile({
super.key,
required this.title,
required this.subtitle,
required this.imgUrl,
required this.playTime,
});
final String title; //음악 이름
final String subtitle; //가수
final String imgUrl; //이미지 URL
final String playTime; //재생 시간
Widget build(BuildContext context) {
return ListTile(
title: Text(
style: TextStyle(
color: Colors.white70,
fontWeight: FontWeight.bold,
),
maxLines: 2,
title,
),
subtitle: Row(
children: [
Icon(
color: Colors.white70,
size: 16,
Icons.check_circle,
),
SizedBox(width: 4),
Flexible(
child: Text(
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white38,
fontWeight: FontWeight.bold,
),
subtitle,
),
),
Text(
style: TextStyle(
color: Colors.white38,
fontWeight: FontWeight.bold,
),
' · $playTime',
),
],
),
leading: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
),
clipBehavior: Clip.antiAlias,
child: Image.asset(imgUrl),
),
trailing: Icon(color: Colors.white70, Icons.more_vert),
);
}
}
import 'package:first_app/MusicTile.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.black,
//앱바
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white70,
shape: Border(
bottom: BorderSide(
color: Colors.white38,
width: 0.5,
)),
title: Text('아워리스트'),
leading: Icon(Icons.arrow_back_ios),
actions: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(Icons.monitor),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(Icons.search),
),
],
),
// 내부 요소 리스트
body: ListView(
physics: BouncingScrollPhysics(),
children: [
MusicTile(
title: 'Come with me',
subtitle: 'Surfaces 및 salem ilese',
playTime: '3:00',
imgUrl: 'assets/images/music_come_with_me.png',
),
MusicTile(
title: 'Good day',
subtitle: 'Surfaces',
playTime: '3:00',
imgUrl: 'assets/images/music_good_day.png',
),
MusicTile(
title: 'Honesty',
subtitle: 'Pink Sweat\$',
playTime: '3:09',
imgUrl: 'assets/images/music_honesty.png',
),
MusicTile(
title: 'I Wish I Missed My Ex',
subtitle: '미할리아 버크마',
playTime: '3:24',
imgUrl: 'assets/images/music_i_wish_i_missed_my_ex.png',
),
MusicTile(
title: 'Plastic Plants',
subtitle: '미할리아 버크마',
playTime: '3:20',
imgUrl: 'assets/images/music_plastic_plants.png',
),
MusicTile(
title: 'Sucker for you',
subtitle: '맷 테리',
playTime: '3:24',
imgUrl: 'assets/images/music_sucker_for_you.png',
),
MusicTile(
title: 'Summer is for falling in love',
subtitle: 'Sarah Kang & Eye Love Brandon',
playTime: '3:00',
imgUrl: 'assets/images/music_summer_is_for_falling_in_love.png',
),
MusicTile(
title: 'These days(feat. Jess Glynne, Macklemore & Dan Caplen)',
subtitle: 'Rudimental',
playTime: '3:00',
imgUrl: 'assets/images/music_these_days.png',
),
MusicTile(
title: 'You Make Me',
subtitle: 'DAY6',
playTime: '3:39',
imgUrl: 'assets/images/music_you_make_me.png',
),
MusicTile(
title: 'Zombie Pop',
subtitle: 'DPR IAN',
playTime: '1:54',
imgUrl: 'assets/images/music_zombie_pop.png',
),
],
),
//하단 음액 재생 박스 UI
bottomSheet: Container(
color: Colors.black87,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(
style: TextStyle(color: Colors.white70),
'You Make Me',
),
subtitle: Text(
style: TextStyle(color: Colors.white38),
'Day6',
),
leading: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
),
clipBehavior: Clip.antiAlias,
child: Image.asset('assets/images/music_you_make_me.png'),
),
trailing: Wrap(
spacing: 8,
children: [
Icon(color: Colors.white70, Icons.play_arrow),
Icon(color: Colors.white70, Icons.skip_next),
],
),
),
//음악 재생 현황 선
Row(
children: [
Container(
height: 1,
width: 16,
color: Colors.white70,
),
Expanded(
child: Container(
height: 1,
color: Colors.white38,
),
),
],
)
],
),
),
// 하단 네비게이션 바
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed, //네비게이션 타입
backgroundColor: Colors.white12,
selectedItemColor: Colors.white70,
unselectedItemColor: Colors.white38,
elevation: 0,
currentIndex: 2,
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '홈'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: '둘러보기'),
BottomNavigationBarItem(
icon: Icon(Icons.my_library_music), label: '보관함'),
],
),
),
);
}
}
입력 받을 매개 변수는 title
subtitle
imgUrl
playTime
로 각각 음악 이름, 가수 이름, 앨범 이미지 url, 재생 시간을 의미한다.
ListTile
을 사용해 각 요소를 작성했다. 여기서 title
은 최대 2줄까지 허용하기 위해 maxLines: 2
를 설정했고, sustitle
은 길어질 경우 ...으로 표시하기 위해서 Row
안에 Flexible
로 감싼 Text
위젯에 overflow: TextOverFlow.ellipsis
를 설정한 뒤 가수 이름을 적어주었다. 이어서 같은 Row
에 재생 시간을 출력해주는 텍스트 위젯을 넣었다.
앨범 이미지와 더보기 아이콘은 leading
과 trailing
을 사용했다.
전체 화면을 검은색으로 나타내기 위해 Scaffold
의 배경색을 검은색으로 설정했다.
앱바는 배경색을 투명하게 하고, 그림자를 제거한 뒤 shape
속성에서 Border
를 사용해 아래에 구분선을 넣어 주었다. 각 앱바의 요소도 속성을 사용해 입력해 주었다.
내부 음악 리스트는 ListTile
을 사용했는데 내부 요소들을 앞에서 작성한 MusicTile
을 사용해 총 10개의 음악을 생성했다.
하단의 음악 재생현황 박스는 bottomSheet
속성을 사용했는데 기능이 아닌 UI만 구현하기 위해 Container
안에 Column
을 생성했다. Column
은 최대로 확장되지 않도록 mainAxisSize: MainAxisSize.min
을 설정했고 ListTile
에서 현재 음악 정보를 보여주었다. 이때 trailing
에 두 개의 아이콘을 띄워주어야 하는데 Wrap
위젯을 사용했다.
리스트타일 아래에 Row
위젯을 넣고 두 개의 Container
를 생성한 뒤 색을 다르게 설정했다. 그리고 오른쪽의 Container
는 Expanded
로 감싼 뒤 왼쪽의 Container
의 길이를 적당히 설정해 주면 현재 음악의 재생 현황을 보여주는 선을 표현할 수 있다.
마지막으로 BottomNavigationBar
를 사용해 총 3가지의 아이템을 넣어주었다.
앱의 UI를 만들고 코드를 모두 작성한 뒤 강의를 들어보니 아쉬운 점이 한가지 생겼다. 앱의 배경을 검은색으로 설정하고 하나하나 글씨나 아이콘 색을 흰색으로 바꿨는데, 이 방법보다 다크모드를 사용하면 전체적으로 어두운 테마가 적용되고, 글씨나 아이콘이 기본 흰색으로 설정된다.
다크모드는 MaterialApp
위젯에서 theme
속성으로 설정할 수 있는데 대표적으로 아래와 같은 두 가지 방법이 있다.
//첫 번째 방법
MaterialApp(
theme: ThemeData.dark()
);
//두 번째 방법
MaterialApp(
theme: ThemeData.from(
colorScheme: ColorScheme.dark()
)
);
오늘도 6일차와 마찬가지로 현재 서비스 중인 앱의 UI를 그려보았다. 유튜브 뮤직 앱의 보관함 부분이었는데 어제와 비슷한 내용이 많아 크게 어렵지는 않았다. (어제 BottomSheet을 사용해 봐서 ㅋㅋㅋ) 근데 BottomSheet에서 재생시간 현황을 보여주는 선을 그리는데 오래 걸렸다. 이것 저것 찾아봤지만 사용할 수 있을 법한 것을 찾지 못했다. ㅠㅠ 외부 패키지로 똑같은 하단 재생현황이 있었는데 직접 만들어야 될거 같아서 이건 사용 안했다. 결국 Row로 컨테이너 두 개를 만들어서 색을 다르게 하는 방식으로 표현해 봤는데 이렇게 해도 되는건가 모르겠다.ㅋㅋㅋㅋ 오늘도 블로그 내용이 짧긴 하지만 일단 여기까지!!