현황
- 팀 소개 앱 만들기 ( 헤이 모두들 안녕 내가 누군지 아니 )
- 멤버 페이지 레이아웃을 맡으신 팀장님이 멤버 페이지를 완성시켜주셔서 이를 받아서 각자 페이지를 꾸미는 것을 목표
개인 페이지 만들기!
- member_page_hsj.dart
import 'package:flutter/material.dart';
import 'package:flutter_test_api/musicmodel.dart';
import 'package:flutter_test_api/requsetmodel.dart';
import 'package:get/get.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
class MemberPageHsj extends StatefulWidget {
MemberPageHsj({super.key});
@override
State<MemberPageHsj> createState() => _MemberPageHsjState();
}
class _MemberPageHsjState extends State<MemberPageHsj> {
final List<MusicModel> _playlist = [
MusicModel(
title: '붉은 장미',
singer: '우예린',
image: AssetImage('assets/images/music1.png')),
MusicModel(
title: '신호등',
singer: '이무진',
image: AssetImage('assets/images/music2.png')),
MusicModel(
title: 'Lemon',
singer: '요네즈 켄시',
image: AssetImage('assets/images/music3.png')),
MusicModel(
title: '언제나',
singer: '허각',
image: AssetImage('assets/images/music4.png')),
MusicModel(
title: '네모의 꿈',
singer: '유영석',
image: AssetImage('assets/images/music5.png')),
MusicModel(
title: '여행',
singer: '볼빨간 사춘기',
image: AssetImage('assets/images/music6.png')),
MusicModel(
title: 'Brave Heart',
singer: '전영호',
image: AssetImage('assets/images/music7.png')),
MusicModel(
title: '정말로 사랑한다면',
singer: '버스커 버스커',
image: AssetImage('assets/images/music8.png')),
MusicModel(
title: 'Never Ending Story',
singer: '부활',
image: AssetImage('assets/images/music9.png')),
];
@override
Widget build(BuildContext context) {
List<Widget> pagelist = [
FirstScreen(),
SecondScreen(),
ThirdScreen(),
//FourthScreen(),
];
// check:: 여기에 보여줄 페이지들 목록 집어넣기.
return _infoPageWidget(pagelist);
}
final PageController _pageController = PageController();
Widget _infoPageWidget(List<Widget> myInfoPages) {
return LayoutBuilder(
builder: (context, constraints) {
return Scaffold(
// check:: 배경화면 컬러 지정
backgroundColor: Colors.black,
body: SafeArea(
child: Stack(
children: [
GestureDetector(
onTap: () {
int nextPage = _pageController.page!.toInt() + 1;
if (nextPage < myInfoPages.length) {
_pageController.animateToPage(
nextPage,
duration: const Duration(milliseconds: 100),
curve: Curves.easeInOut,
);
} else {
nextPage = 0;
_pageController.animateToPage(
nextPage,
duration: const Duration(milliseconds: 100),
curve: Curves.easeInOut,
);
}
},
child: Column(
children: [
Container(
padding: const EdgeInsets.all(20),
// height: 750,
height: constraints.maxHeight - 70,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
// pageSnapping: false,
scrollDirection: Axis.vertical,
children: myInfoPages,
),
),
),
SmoothPageIndicator(
controller: _pageController, // PageController
count: myInfoPages.length,
effect: const ExpandingDotsEffect(
dotWidth: 10,
dotHeight: 10,
activeDotColor: Colors.grey,
), // your preferred effect
),
const SizedBox(
height: 20,
),
const Text(
// Todo:: 멤버 인덱스 받아 이름 자동화 하기
"황상진",
style: TextStyle(color: Colors.white, fontSize: 24),
)
],
)),
Positioned(
top: 30,
left: 35,
child: GestureDetector(
onTap: () => Get.back(),
child: const Text('X',
style: TextStyle(color: Colors.white, fontSize: 24)),
)),
],
)),
);
},
);
}
// check:: 여기 아래로 Widget 만들어 페이지 생성.
Widget FirstScreen() {
return SizedBox.expand(
child: Container(
decoration: BoxDecoration(
color: const Color.fromARGB(255, 243, 100, 33),
image: DecorationImage(
image: AssetImage('assets/images/bluebird.png'),
fit: BoxFit.cover,
alignment: Alignment(-0.16, 0),
),
),
),
);
}
Widget SecondScreen() {
return SizedBox.expand(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 15),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color.fromARGB(255, 107, 64, 83),
const Color.fromARGB(255, 15, 15, 15)
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Stack(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
height: 50,
),
Text(
'아무곡 모음집',
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold),
),
SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
CircleAvatar(
radius: 12,
backgroundImage: AssetImage(
'assets/images/hsj1.png',
),
),
SizedBox(
width: 10,
),
Text(
'황상진',
style: TextStyle(
color: Colors.white,
fontSize: 16,
),
),
],
),
SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(
Icons.expand_circle_down_outlined,
color: Color(0xff999999),
size: 40,
),
SizedBox(
width: 16,
),
Icon(
Icons.person_add_alt_1_outlined,
color: Color(0xff999999),
size: 40,
),
SizedBox(
width: 16,
),
Icon(
Icons.menu,
color: Color(0xff999999),
size: 40,
)
],
),
SizedBox(
height: 10,
),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
...List.generate(
_playlist.length,
(index) {
return playlist_container(index);
},
)
],
),
)),
],
),
Positioned(
right: 0,
top: 150,
child: FloatingActionButton(
onPressed: () {},
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
child: Center(
child: Icon(
Icons.play_arrow,
color: Colors.white,
size: 32,
)),
),
),
],
),
),
);
}
Container playlist_container(int index) {
return Container(
padding: EdgeInsets.symmetric(vertical: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Image(
image: _playlist[index].image,
width: 50,
height: 50,
),
SizedBox(
width: 10,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_playlist[index].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 16,
),
),
Text(
_playlist[index].singer,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
)
],
),
),
CircleAvatar(
radius: 12,
backgroundImage: AssetImage(
'assets/images/hsj1.png',
),
),
SizedBox(
width: 16,
),
Icon(
Icons.more_horiz,
color: Colors.white,
size: 24,
)
],
),
);
}
final List<RequestModel> _requsetlist = [
RequestModel(question: '혈액형은?', anser: 'B형', index: 0, clickflag: true),
RequestModel(question: '봄 vs 여름 vs 가을 vs 겨울', anser: '가을', index: 1),
RequestModel(question: '좋아하는 숫자는?', anser: '37', index: 2),
RequestModel(question: '좋아하는 색깔은?', anser: '연하늘색', index: 3),
RequestModel(question: '좋아하는 스포츠는?', anser: '야구', index: 4),
RequestModel(question: '숲 vs 바다', anser: '집', index: 5),
RequestModel(question: '좋아하는 음식은?', anser: '닭갈비,뼈해장국,스시,비빔밥,스파게티', index: 6),
RequestModel(question: '좋아하는 게임장르는?', anser: '시뮬레이션게임,TCG게임', index: 7),
RequestModel(question: '요새 취미는?', anser: '웹소설읽기,모바일게임', index: 8),
RequestModel(question: '자주쓰는닉네임은?', anser: 'Hamiric', index: 9),
RequestModel(question: '좋아하는 과일은?', anser: '과즙이 풍부한 과일류 모두', index: 10),
RequestModel(question: '좋아하는 라면은?', anser: '농심 신라면', index: 11),
RequestModel(question: '탕수육 부먹 vs 찍먹', anser: '찍먹', index: 12),
RequestModel(question: '에어컨 없는 여름 vs 난방 없는 겨울', anser: '에어컨 없는건 버텨도, 난방 없는건 못 버팀', index: 13),
RequestModel(question: '좋아하는 음악은?', anser: '흥겨운 음악', index: 14),
RequestModel(question: '언젠가 한번 해보고싶은것은?', anser: '게임만들기,소설쓰기', index: 15),
RequestModel(question: '가장좋아하는(했던) TV프로그램은?', anser: '런닝맨', index: 16),
RequestModel(question: '기억에 남는 명언?', anser: '언제나 현재에 집중할수 있다면 행복할것이다. -파울로 코엘료', index: 17),
RequestModel(question: '쉴때 하는 일은?', anser: '잠자기,유튜브보기', index: 18),
RequestModel(question: '앞으로 한마디', anser: '끝까지 힘내자!', index: 19),
];
String question = '혈액형은?';
String anser = 'B형';
int flagnum = 0;
Widget ThirdScreen() {
return SizedBox.expand(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 24),
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 60,
),
Text(
'20문 20답',
style: TextStyle(fontSize: 48, color: Colors.black),
),
SizedBox(
height: 100,
),
SizedBox(
height: 100,
child: Text(
textAlign: TextAlign.center,
question,
style: TextStyle(fontSize: 32),
),
),
SizedBox(
height: 60,
),
SizedBox(
height: 80,
child: Text(
anser,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 24),
),
),
SizedBox(
height: 100,
),
GestureDetector(
onTap: () {},
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(
color: Colors.black,
width: 2.0,
)),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
...List.generate(_requsetlist.length, (index) {
return questionNumber(index);
})
],
),
),
),
)
],
)),
);
}
Container questionNumber(int index) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 8),
child: GestureDetector(
onTap: () {
setState(() {
if (flagnum != index) {
_requsetlist[flagnum].clickflag = false;
_requsetlist[index].clickflag = true;
flagnum = index;
question = _requsetlist[index].question;
anser = _requsetlist[index].anser;
}
});
},
child: Container(
width: 55,
height: 55,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color:
_requsetlist[index].clickflag ? Colors.green : Colors.grey),
child: Center(
child: Text(
(_requsetlist[index].index + 1).toString(),
style: TextStyle(fontSize: 32, color: Colors.black),
),
),
),
),
);
}
Widget FourthScreen() {
return SizedBox.expand(
child: Container(
color: const Color.fromARGB(255, 92, 156, 49),
child: const Center(
child: Text(
'Page index : 3',
style: TextStyle(fontSize: 20),
),
),
),
);
}
}
- musicmodel.dart
import 'package:flutter/material.dart';
class MusicModel {
final String title;
final String singer;
final AssetImage image;
MusicModel({
required this.title,
required this.singer,
required this.image,
});
}
- requestmodel.dart
class RequestModel {
String question;
String anser;
late int index;
bool clickflag;
RequestModel({
required this.question,
required this.anser,
required this.index,
this.clickflag = false,
});
}
알게된 점
1. 각각의 변수를 Model로 묶어서 사용하는 방법을 보다 잘 사용할 수 있게 되었다. (아직 생성자 부분만 이지만..)
2. FloatingActionButton은 Stack 위젯 내부에서만 사용이 가능하더라..
3. Text 위젯의 maxLines와 overflow 옵션을 알게 되었다.
4. GestureDetector 위젯의 사용자 상호작용 옵션들은 하위 위젯에서도 작동한다. 만약, 하위 위젯의 일부분에서 GestureDetector의 상호작용 이벤트가 발생하지 않게 하려면, 하위 위젯에도 GestureDetector을 생성하여 오버라이딩 해주는 방법으로 발생하지 않도록 만들 수 있다. 그 반대의 경우는, IngnorePointer, AbosrobPointer을 사용한다는데, 이건 다른이야기
5. git에 push 하기 전 주의사항에 대해 알게 되었다. [ fetch - 변경확인 후 수정 - pull - add - commit - push ]
6. 사용한 구글 아이콘들은 아래 링크에서 비스무리하게 알려주더라.. ( 100% 똑같은건 아님 )
Material Symbols and Icons - Google Fonts
Material Symbols are our newest icons consolidating over 2,500 glyphs in a single font file with a wide range of design variants.
fonts.google.com
개선할점
1. SetState 안쓰고 GetxController를 통해 상태관리 해보려 했는데, Get.find(..controller())를 어디에 넣어야 할지를 잘 모르겠어서 일단 SetState를 사용해서 구현했다. 이 부분 개선할 수있으면 보다 최적화가 될거 같다.
2. 리스트 기능을 SingleChildScrollView + Colum 으로 구현했는데, 최적화 부분에 있어서는 ListView 를 사용하는게 더 좋다고 한다.
- ListView:
- 항목 단위 업데이트: 각 항목이 개별적으로 업데이트되므로, 리스트의 특정 부분만 리빌드하여 성능을 향상시킬 수 있습니다.
- 성능 최적화: UI 성능을 유지하면서도 변경 사항에 대한 반응성을 높일 수 있습니다.
- SingleChildScrollView + Column:
- 전체 재렌더링: Column 내의 모든 위젯이 업데이트될 때, 전체 Column이 재렌더링될 수 있습니다. 이는 비효율적일 수 있으며, 성능에 영향을 미칠 수 있습니다.
결론적으로 ListView는 대규모 데이터 리스트를 효율적으로 처리하기 위해 설계되었으며, 성능과 메모리 관리에 있어 더 나은 선택입니다. 긴 리스트를 보여줄 때 ListView.builder를 사용하는 것이 이상적이고, SingleChildScrollView + Column은 상대적으로 적은 수의 위젯을 사용할 때 더 유연한 레이아웃을 제공하며, UI가 복잡한 경우에 적합하다. 그러나 긴 리스트를 처리할 때는 성능 문제가 발생할 수 있다.
'TIL' 카테고리의 다른 글
TIL - Flutter / Mac OS 작업환경 설정 (0) | 2024.10.30 |
---|---|
TIL - 콘솔 쇼핑몰 (24.10.29) (0) | 2024.10.29 |
TIL (24.10.25) (0) | 2024.10.25 |
온보딩 프로젝트(24.10.24) (0) | 2024.10.24 |
온보딩 프로젝트 (24.10.21) (0) | 2024.10.21 |