이 프로젝트는 개발하는남자님의 유튜브 영상을 참고하여 제작하였습니다. 허나, 원본 영상에서 제작하는 방법과는 다를 수 있습니다.
업로드 버튼을 클릭하면 해당 페이지로 라우팅되면서 현재 디바이스의 갤러리의 모든 미디어파일들을 불러올 수 있습니다. 그리고 이미지를 선택하면 해당 이미지를 자세하게 확인할 수 있고, 앨범의 종류를 선택할수도 있습니다. 하단은 완성하면 볼 수 있는 업로드 화면입니다.
![]() |
![]() |
그러면 바로 시작하겠습니다.
업로드 화면은 다른 바텀 네비게이션과는 다르게 새로운 화면으로 이동하는 애니메이션을 갖고 있습니다. 따라서, 해당 부분은 기존의 인덱스를 변경하는 방식보다는 네비게이션을 직접하는 방식이 좋을 것 같습니다. 실제로 인스타그램은 Dismissible한 네비게이션 효과가 나타나지만, 간단하게 기본 네비게이션으로 만들어주겠습니다.
바텀 네비게이션의 컨트롤러를 수정하면 되겠죠? bottom_nav_controller.dart파일을 엽니다.
...
void changeIndex(int value) {
var page = Page.values[value];
switch (page) {
case Page.HOME:
case Page.SEARCH:
case Page.REELS:
case Page.MYPAGE:
moveToPage(value);
case Page.UPLOAD:
//업로드 화면 선택시
moveToUpload();
}
}
...
//업로드 화면으로 이동
void moveToUpload() {
Get.to(() => const Upload());
}
이렇게 하면 업로드화면을 선택할 경우에만 기본 라우팅 방식을 사용할 수 있습니다.
업로드 화면은 앱바를 제외하고 크게 3가지 영역으로 나눌 수 있습니다. 선택한 사진을 볼 수 있는 preview 영역, 앨범과 여러가지 옵션을 선택하는 header()영역, 현재 앨범의 미디어파일을 볼 수 있는 images()영역으로 나눌 수 있어요. 하나하나 만들어주겠습니다.
앱바는 굉장히 간단하니까 코드보면 금방 이해될겁니다.
import ...
class Upload extends StatelessWidget {
const Upload({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
//앱바 타이틀
title: const Text('새 게시물'),
titleTextStyle: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
//뒤로 가기 버튼
leading: Padding(
padding: const EdgeInsets.all(15.0),
child: GestureDetector(
onTap: Get.back, child: ImageData(path: ImagePath.closeImage)),
),
//actions
actions: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ImageData(path: ImagePath.nextImage),
)
],
),
);
}
앱바는 특별히 설명할게 없습니다. 뒤로가기 버튼이 기존의 arrow_back과는 다르다는 차이가 있습니다. 이는 직접 leading에 원하는 아이콘 버튼과 이벤트를 설정해주면 됩니다.
preview 영역은 선택한 사진을 확인하는 영역입니다. 하지만, 아직 사진을 불러올 수 없기 때문에 간단하게 Container로 표현해주겠습니다.
class Upload extends GetView<UploadController> {
const Upload({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('새 게시물'),
titleTextStyle: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
leading: Padding(
padding: const EdgeInsets.all(15.0),
child: GestureDetector(
onTap: Get.back, child: ImageData(path: ImagePath.closeImage)),
),
actions: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ImageData(path: ImagePath.nextImage),
)
],
),
body: Column(
children: [
//preview 영역 추거
_preview(),
],
),
);
}
...
Widget _preview() {
return Container(
height: Get.size.width,
width: Get.size.width,
color: Colors.black,
);
}
이 영역은 이 후에 기능을 완성하면 추가로 완성시켜보겠습니다.
header 영역은 앨범을 선택할 수 있는 버튼 영역과 다양한 옵션 버튼들이 존재합니다. 가로를 기준으로 여러 위젯들끼리 뭉쳐져 있으니 Row위젯을 여러번 적용하면 손쉽게 만들 수 있습니다.
앨범을 표시하는 부분부터 만들겠습니다. 앨범은 아직 알 수 없으니, Recent라는 글자를 임의로 적겠습니다. 옆에 dropdown 메뉴는 이미 asset에 존재합니다. 따라서 그냥 asset을 사용해주면 됩니다.
...
body: Column(
children: [
_preview(),
// header 영역 추가
_header(),
],
),
);
}
...
Widget _header() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Recent',
style: const TextStyle(
fontSize: 18,
color: Colors.black,
fontWeight: FontWeight.bold),
),
),
//드롭다운 메뉴 이미지
ImageData(
path: ImagePath.arrowDownIcon,
width: 60,
)
],
),
],
);
}
...
옆 옵션영역도 마찬가지로 이미 asset에 해당 이미지가 존재합니다. 다만, 해당 이미지가 회색 동그라미 안에 적용되어 있죠. Container위젯을 이용해서 쉽게 표현해보겠습니다.
...
Widget _header() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
...
// 왼쪽 영역 제외
...
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
//동그라미 효과
Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(4.0),
decoration: const BoxDecoration(
color: Color(0xff808080), shape: BoxShape.circle),
child: ImageData(
path: ImagePath.imageSelectIcon,
width: 60,
)),
Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(4.0),
decoration: const BoxDecoration(
color: Color(0xff808080), shape: BoxShape.circle),
child: ImageData(
path: ImagePath.cameraIcon,
width: 60,
)),
],
)
],
);
}
...
Container 위젯의 padding과 margin을 이용해서 자식위젯과 외부위젯사이의 간격을 조절할 수 있습니다. 그리고 shape에 BoxShape.circle을 사용하면 Container 위젯을 사각형에서 원으로 바꿀 수 있습니다.
이렇게 중단의 header 영역도 완성했습니다.
images 영역은 사진들을 갤러리처럼 볼 수 있어야합니다. GridView위젯을 사용하면 손쉽게 구현할 수 있을 것 같습니다. 아직 디바이스의 갤러리와 연결시키지 않을것이기 때문에 단순하게 Container를 이용해서 GridView를 구현하겠습니다.
...
body: Column(
children: [
_preview(),
_header(),
//images 영역 추가
_images(),
],
),
);
}
...
Widget _images() {
return Expanded(
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4, mainAxisSpacing: 1.0, crossAxisSpacing: 1.0),
itemCount: 50,
itemBuilder: (context, index) => Container(
color: Colors.blue,
)));
}
...
미디어 파일은 가로에 4개씩 보여지고 있기 때문에 SliverGridDelegateWithFixedCrossAxisCount의 crossAxisCount는 4가되어야 합니다. 미디어들의 간격은 1.0이 적절한 것 같아서 1.0으로 전달해주었습니다.
이제, 업로드화면의 UI는 모두 완성했습니다.