아 진짜 적응 안 되네..
일단 내가 사용할 디렉토리 구조, 하는 일을 정리해보자..
pubspec.yaml
flutter dependencies에 사용할 플러그인 버전들을 등록한다.
get: ^4.6.5
flutter_svg: ^2.0.0+1
intl: ^0.18.0
/main.dart
테마를 적용하고 initialBinding, initialRoute를 설정하고 getPage 설정
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
title: "Youtube Clone App",
theme: ThemeData(
primaryColor: Colors.white,
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialBinding: InitBinding(),
initialRoute: '/',
getPages: [
GetPage(name: '/', page: ()=>App()),
GetPage(name: '/detail/:videoId', page: () => YoutubeDetail()),
],
);
}
}
/src/app.dart
//GetView<>를 사용해서 initialBinding 에 올려둔 해당 컨트롤러를 불러와서 사용한다.
//app.dart 파일에 body와 bottom navigation을 정의하고 body 안에서 appcontroller에서 받아온
//index에 따라서 페이지전환이 되도록 한다. Obx 사용
class App extends GetView<Appcontroller> {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Obx(() {
switch(RoutName.values[controller.currentIndex.value]){
case RoutName.Home:
return Home();
break;
case RoutName.Explore:
return Explore();
break;
case RoutName.Add:
//bottom sheet
break;
case RoutName.Subs:
return Subscribe();
break;
case RoutName.Library:
return Library();
break;
}
return Container();
}),
bottomNavigationBar: Obx(
() => BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: controller.currentIndex.value,
// 반응형 인덱스 정보를 반영하기 위해 Obx로 둘러 싸고 이렇게 변경
showSelectedLabels: true,
selectedItemColor: Colors.black,
onTap: controller.changePageIndex,
// 상단 상속 받는 클래스를 GetView<Appcontroller>로 바꾸고 onTap 을 이렇게 고친다.
items: [
BottomNavigationBarItem(
icon: SvgPicture.asset('assets/svg/icons/home_off.svg'),
activeIcon: SvgPicture.asset('assets/svg/icons/home_on.svg'),
label: '홈'),
BottomNavigationBarItem(
icon: SvgPicture.asset(
'assets/svg/icons/compass_off.svg',
width: 22,
),
activeIcon: SvgPicture.asset(
'assets/svg/icons/compass_on.svg',
width: 22,
),
label: '탐색'),
BottomNavigationBarItem(
icon: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: SvgPicture.asset(
'assets/svg/icons/plus.svg',
width: 35,
),
),
label: '',
),
BottomNavigationBarItem(
icon: SvgPicture.asset('assets/svg/icons/subs_off.svg'),
activeIcon: SvgPicture.asset('assets/svg/icons/subs_on.svg'),
label: '구독'),
BottomNavigationBarItem(
icon: SvgPicture.asset('assets/svg/icons/library_off.svg'),
activeIcon: SvgPicture.asset('assets/svg/icons/library_on.svg'),
label: '보관함'),
],
),
),
);
}
}
/src/bindings/
앱에서 주로 사용할 컨트롤러를 올려둘 바인딩 파일을 올려둔다.
// implements로 인터페이스를 상속 받는데, implements의 경우 꼭 @override로 재정의 해야 함
class InitBinding implements Bindings {
@override
void dependencies() {
Get.put(YoutubeRepository(), permanent: true); // 계속 사용할 컨트롤러는 permanent true를 준다
Get.put(Appcontroller());
}
}
/src/pages/
각각의 페이지들 파일이 있는 곳
바인딩파일에 등록하지 않고 해당 페이지에서 사용해야 하는 컨트롤러를 Get.put으로 등록한다.
컴포넌트에 만들어둔 위젯들을 불러온다.
class Home extends StatelessWidget {
Home({Key? key}) : super(key: key);
final HomeController controller = Get.put(HomeController()); //페이지 뜰 때 홈컨트롤러 등록
@override
Widget build(BuildContext context) {
// 앱바가 스크롤 되어서 위로 가면 사라지고 내려오면 나타나는 ui
return SafeArea(
child: Obx(() => CustomScrollView(
slivers: [
SliverAppBar(
title: CustomAppBar(),
// 이 부분이 내려갈 때 앱바 나오게 하는 부분
floating: true,
snap: true,
// 이 부분이 내려갈 때 앱바 나오게 하는 부분
backgroundColor: Colors.white,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return GestureDetector(
onTap: (){
Get.toNamed('/detail/239587');
print('detail');
},
child: VideoWidget(
video: controller.youtubeResult.value.items[index] // 받은 비디오 데이타를 넘겨준다.
),
);
},
childCount: controller.youtubeResult.value.items==null ? 0 : controller.youtubeResult.value.items.length, //items거 널이면 0을 넣어준다.
// childCount: 1000,
),
),
],
),
),
);
}
}
/src/controller/
페이지나 위젯에 변경되는 함수나 클래스를 작성한다.
// 바텀네비게이션에서 누른 index 값에 따라서 페이지를 이동하게 하는 예
enum RoutName {Home, Explore, Add, Subs, Library}
class Appcontroller extends GetxService {
static Appcontroller get to => Get.find();
RxInt currentIndex = 0.obs;
void changePageIndex(int index){
if (index==2){
_showBottomSheet();
}else{
currentIndex(index);
}
}
void _showBottomSheet() {
Get.bottomSheet(YoutubeBottomSheet());
}
}
/src/components/
각각의 페이지 안에서 사용할 각각의 위젯 덩어리들을 정의 / 위젯을 꾸미고 api로 데이터를 불러오고..
class YoutubeDetail extends StatelessWidget {
const YoutubeDetail({Key? key}) : super(key: key);
Widget _titleZone() {
return Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('개발하는 남자 유튜브 영상 더보기', style: TextStyle(fontSize: 15)),
Row(
children: [
Text(
'조회수 1000회',
style: TextStyle(
fontSize: 13, color: Colors.black.withOpacity(0.5)),
),
],
),
],
),
);
}
Widget _descriptionZone(){
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 30),
child: Text('안녕하세요. 개발하는 남자 개남입니다.', style: TextStyle(fontSize: 14), ),
);
}
Widget _buttonOne(String iconPath, String text){
return Column(
children: [
Column(
children: [
SvgPicture.asset('assets/svg/icons/$iconPath.svg'),
Text(text),
],
),
],
);
}
Widget _buttonZone(){
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buttonOne('like', '1000'),
_buttonOne('dislike', '0'),
_buttonOne('share', '공유'),
_buttonOne('save', '저장'),
],
);
}
Widget get line => Container(
height: 1,
color: Colors.black.withOpacity(0.1),
);
Widget _ownerZone(){
return Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
child: Row(
children: [
CircleAvatar(
radius: 30,
backgroundColor: Colors.grey.withOpacity(0.5),
backgroundImage: Image.network('https://yigam.co.kr/img/logo_210517d.jpg').image,
),
SizedBox(width: 15,),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('개발하는 남자', style: TextStyle(fontSize: 15),),
Text('구독자 10000', style: TextStyle(fontSize: 14, color: Colors.black.withOpacity(0.6)), ),
],
),
),
GestureDetector(
child: Text('구독', style: TextStyle(color: Colors.red, fontSize: 16), ),
)
],
),
);
}
Widget _description() {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_titleZone(),
Container(
height: 1,
color: Colors.black.withOpacity(0.1),
),
_descriptionZone(),
_buttonZone(),
SizedBox(
height: 20,
),
line,
_ownerZone(),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: [
Container(
height: 250,
color: Colors.grey.withOpacity(0.5),
),
Expanded(
child: _description(),
)
],
),
);
}
}
/src/models/
api에서 받아온 데이타를 jSon 형태로 처리하게 하는 파일들을 모아두는 곳
불러온 데이타는 jSon to Dart를 통해서 자동으로 클래스파일을 만든다.
사용할 item을 따로 map 으로 변형해서 repository에서 불러와서 사용한다.
//json to dart로 변환한 파일 임포트
import 'package:youtube/src/models/video.dart';
// repository에서 사용할 수 있게 가공
class YoutubeVideoResult {
int totalResults;
int resultsPerPage;
String nextPagetoken;
List<Video> items;
YoutubeVideoResult({required this.totalResults, required this.resultsPerPage, required this.nextPagetoken, required this.items });
// 아직 잘 모르겠지만, factory를 사용했다. 음..
factory YoutubeVideoResult.fromJson(Map<String, dynamic> json) =>
YoutubeVideoResult(
totalResults: json["pageInfo"]["totalResults"],
resultsPerPage: json["pageInfo"]["resultsPerPage"],
nextPagetoken: json["pageInfo"]["nextPagetoken"] ?? "",
items: List<Video>.from(json["items"].map((data)=>Video.fromJson(data)))
);
}
/src/repositories/
GetConnect, httpClient를 통해서 api로 데이타를 불러와서 models에 가공한 값을 돌려주는 클래스
import 'package:youtube/src/models/youtube_video_result.dart';
class YoutubeRepository extends GetConnect {
static YoutubeRepository get to => Get.find();
@override
void onInit(){
httpClient.baseUrl = "https://www.googleapis.com";
super.onInit();
}
Future<YoutubeVideoResult?> loadVideos() async {
String url = "/youtube/v3/search?part=snippet&maxResults=10&order=date&type=video&videoDefinition=high&key=AIzaSyAFRAvFot5LCgUGdWsBiHg8SPQO-wibUNA&channelId=UC8Au4X76OUpaTFMTnptJfew";
final response = await get(url);
if(response.status.hasError){
return Future.error(response.statusText as Object);
}else{
if(response.body["items"]!=null && response.body["items"].length>0){
return YoutubeVideoResult.fromJson(response.body);
}
// print(response.body['items']);
}
}
}