방명록
- [Flutter] "모두가 할 수 있는 플러터 UI 입문" - 모두의숙소 웹 만들기 | 플러터 웹 | 빌드 실패 문제2022년 02월 08일 10시 24분 52초에 업로드 된 글입니다.작성자: DandyNow728x90반응형
"최주호, 정호준, & 정동진. (2021). 모두가 할 수 있는 플러터 UI 입문. 앤써북"으로 플러터 공부를 하고 있다. 9장에서는 모두의숙소 웹을 만들었다. 플로터로 웹도 만들 수 있다. [그림 1]은 완성된 페이지이다.
앱으로도 빌드를 시도했는데 [그림 2]와 같은 에러가 발생했다. 최신의 Kotlin Gradle plugin이 필요하다고 한다. 기존에 만들었던 앱 프로젝트를 빌드해봐도 동일한 현상이 발생했기 때문에 이번에 작성한 코드의 문제는 아닌 것으로 보인다. [그림 2]는 VS Code에서 보여주는 에러 메시지이며, 안드로이드 스튜디오에도 역시 빌드되지 않았다. 이렇게 저렇게 만져 보았지만 아직은 해결방법을 찾지 못했다(2022년 02월 09일 해결함).
[!] Your project requires a newer version of the Kotlin Gradle plugin.
다른 PC에서 GitHub에 Push된 동일한 코드를 Pull 하여 실행해 보았다. [그림 3]과 같이 AVD에서 정상적으로 실행되었다. 하지만 기존 PC에서는 위 문제가 해결되지 않고 있다.
// main.dart import 'package:flutter/material.dart'; import 'package:flutter_airbnb/pages/home_page.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } }
// constants.dart import 'package:flutter/material.dart'; // 색상 정의 const kAccentColor = Color(0xFFFF385C);
// size.dart import 'package:flutter/material.dart'; // 간격 const double gap_xl = 40; const double gap_l = 30; const double gap_m = 20; const double gap_s = 10; const double gap_xs = 5; // 헤더 높이 const double header_height = 620; // MediaQuery 클래스로 화면 사이즈를 받을 수 있다. double getBodyWidth(BuildContext context) { return MediaQuery.of(context).size.width * 0.7; }
// styles.dart import 'package:flutter/material.dart'; TextStyle h4({Color mColor = Colors.black}) { return TextStyle(fontSize: 34, fontWeight: FontWeight.bold, color: mColor); } TextStyle h5({Color mColor = Colors.black}) { return TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: mColor); } TextStyle subtitle1({Color mColor = Colors.black}) { return TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: mColor); } TextStyle subtitle2({Color mColor = Colors.black}) { return TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: mColor); } TextStyle overLine({Color mColor = Colors.black}) { return TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: mColor); } TextStyle body1({Color mColor = Colors.black}) { return TextStyle(fontSize: 16, color: mColor); }
// pages/home_page.dart import 'package:flutter/material.dart'; import 'package:flutter_airbnb/components/home/home_body.dart'; import 'package:flutter_airbnb/components/home/home_header.dart'; class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: ListView( children: [ HomeHeader(), HomeBody(), ], ), ); } }
// common/common_form_field.dart import 'package:flutter/material.dart'; import 'package:flutter_airbnb/styles.dart'; class CommonFormField extends StatelessWidget { final prefixText; final hintText; const CommonFormField({required this.prefixText, required this.hintText}); @override Widget build(BuildContext context) { return Stack( children: [ TextFormField( textAlignVertical: TextAlignVertical.bottom, decoration: InputDecoration( // TextFormField 내부에 패딩을 줄 수 있다. contentPadding: EdgeInsets.only(top: 30, left: 20, bottom: 10), hintText: hintText, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide( color: Colors.black, width: 2, ), ), ), ), // Positioned를 사용한 이유는 TextFormField 공간에 글자를 삽입하기 위해서 이다. Positioned( top: 8, left: 20, child: Text( prefixText, style: overLine(), ), ), ], ); } }
// home/home_header.dart import 'package:flutter/material.dart'; import 'package:flutter_airbnb/components/home/home_header_appbar.dart'; import 'package:flutter_airbnb/components/home/home_header_form.dart'; import 'package:flutter_airbnb/size.dart'; class HomeHeader extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( width: double.infinity, height: header_height, child: Container( decoration: BoxDecoration( image: DecorationImage( image: AssetImage("assets/background.jpg"), fit: BoxFit.cover, ), ), child: Column( children: [ HomeHeaderAppBar(), HomeHeaderForm(), ], )), ); } }
// home/home_header_appbar.dart import 'package:flutter/material.dart'; import 'package:flutter_airbnb/constants.dart'; import 'package:flutter_airbnb/size.dart'; import 'package:flutter_airbnb/styles.dart'; import 'package:flutter_svg/flutter_svg.dart'; // logo.svg 적용 위해 외부 라이브러리 사용 class HomeHeaderAppBar extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(gap_m), child: Row( children: [ _buildAppBarLogo(), Spacer(), _buildAppBarMenu(), ], ), ); } Widget _buildAppBarLogo() { return Row( children: [ SvgPicture.asset("assets/logo.svg", // logo.svg 적용하였다. width: 30, height: 30, color: kAccentColor), SizedBox(width: gap_s), Text("RoomOfAll", style: h5(mColor: Colors.white)), ], ); } Widget _buildAppBarMenu() { return Row( children: [ // 클릭 이벤트 적용은 InkWell, TextButton 위젯을 사용하면 된다. Text("회원가입", style: subtitle1(mColor: Colors.white)), SizedBox(width: gap_m), Text("로그인", style: subtitle1(mColor: Colors.white)), ], ); } }
// home/home_header_form.dart import 'package:flutter/material.dart'; import 'package:flutter_airbnb/constants.dart'; import 'package:flutter_airbnb/size.dart'; import 'package:flutter_airbnb/styles.dart'; import 'package:flutter_airbnb/components/common/common_form_field.dart'; class HomeHeaderForm extends StatelessWidget { @override Widget build(BuildContext context) { double screenWidth = MediaQuery.of(context).size.width; return Padding( padding: const EdgeInsets.only(top: gap_m), // AppBar와 간격 // 정렬 위젯 child: Align( alignment: // Alignment(-0.6, 0), screenWidth < 520 ? Alignment(0, 0) : Alignment(-0.6, 0), // 변경 child: Container( width: 420, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), ), child: Form( child: Padding( padding: const EdgeInsets.all(gap_l), child: Column( children: [ _buildFormTitle(), // Form 위젯 제목 영역 _buildFormField(), // Form 위젯 Text 입력 양식 영역 _buildFormSubmit(), // Form 위젯 전송 버튼 영역 ], )), ), ), ), ); } Widget _buildFormTitle() { return Column( children: [ Text( "모두의 숙소에서 숙소를 검색하세요.", style: h4(), ), SizedBox(height: gap_xs), Text( "기분 좋은 만남, 아름다운 추억, 모두의 숙소와 함께하세요.", style: body1(), ), SizedBox(height: gap_m), ], ); } Widget _buildFormField() { return Column( children: [ CommonFormField( prefixText: "위치", hintText: "근처 추천 장소", ), SizedBox(height: gap_s), Row( children: [ Expanded( child: CommonFormField( prefixText: "체크인", hintText: "날짜 입력", )), Expanded( child: CommonFormField( prefixText: "체크 아웃", hintText: "날짜 입력", )), ], ), SizedBox(height: gap_s), Row( children: [ Expanded( child: CommonFormField( prefixText: "성인", hintText: "2", )), Expanded( child: CommonFormField( prefixText: "어린이", hintText: "0", )), ], ), SizedBox(height: gap_m), ], ); } Widget _buildFormSubmit() { return SizedBox( width: double.infinity, height: 50, child: TextButton( style: TextButton.styleFrom( backgroundColor: kAccentColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), )), onPressed: () { print("서브밋 클릭됨"); }, child: Text( "검색", style: subtitle1(mColor: Colors.white), ), ), ); } }
// home/home_body.dart import 'package:flutter/material.dart'; import 'package:flutter_airbnb/components/home/home_body_banner.dart'; import 'package:flutter_airbnb/components/home/home_body_popular.dart'; import 'package:flutter_airbnb/size.dart'; class HomeBody extends StatelessWidget { @override Widget build(BuildContext context) { double bodyWidth = getBodyWidth(context); // SizedBox 위젯을 가운데 정렬하기 위해 Align을 사용한다(Center 위젯도 가능함). return Align( child: SizedBox( width: bodyWidth, // 화면의 70%만 차지하게 하기 위해 Column의 영역을 강제시킨다. child: Column( children: [ HomeBodyBanner(), HomeBodyPopular(), ], ), ), ); } }
// home/home_body_banner.dart import 'package:flutter/material.dart'; import 'package:flutter_airbnb/size.dart'; import 'package:flutter_airbnb/styles.dart'; class HomeBodyBanner extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: gap_m), child: Stack( children: [ _buildBannerImage(), _buildBannerCaption(), ], ), ); } } Widget _buildBannerImage() { return ClipRRect( borderRadius: BorderRadius.circular(20), child: Image.asset( "assets/banner.jpg", fit: BoxFit.cover, width: double.infinity, height: 320, ), ); } Widget _buildBannerCaption() { return Positioned( top: 40, left: 40, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( constraints: BoxConstraints( maxWidth: 200, ), child: Text( "이제, 여행은 이탈리아로", style: h4(mColor: Colors.white), ), ), SizedBox(height: gap_m), Container( constraints: BoxConstraints( maxHeight: 250, ), child: Text( "새로운 공간에 머물러 보세요. 살아보기, 출장, 여행 등 다양한 목적에 맞는 숙소를 찾아보세요.", style: subtitle1(mColor: Colors.white), ), ), SizedBox(height: gap_m), SizedBox( height: 35, width: 170, child: TextButton( style: TextButton.styleFrom( backgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), ), onPressed: () {}, child: Text( "가까운 여행지 둘러보기", style: subtitle2(), ), ), ), ], ), ); }
// home/home_body_popular.dart import 'package:flutter/material.dart'; import 'package:flutter_airbnb/components/home/home_body_popular_item.dart'; import 'package:flutter_airbnb/size.dart'; import 'package:flutter_airbnb/styles.dart'; class HomeBodyPopular extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( // 상단에 마진을 준다. padding: const EdgeInsets.only(top: gap_m), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildPopularTitle(), _buildPopularList(), ], ), ); } Widget _buildPopularTitle() { return Column( children: [ Text( "이탈리아 숙소에 직접 다녀간 게스트의 후기", style: h5(), ), Text( "게스트 후기 2,500,000개 이상, 평균 평점 4.7점(5점 만점)", style: body1(), ), SizedBox(height: gap_m), ], ); } Widget _buildPopularList() { // 전체 화면사이즈가 1000이라면, // _buildPopularList의 넓이는 화면의 70%이므로 700이다. // HomeBodyPopularItem의 넓이는 700의 1/3 인 233.33 -5의 크기이므로 228.33이다. // 228.33의 인기 아이템 3개가 배치되면 684.99 크기이고 남은 크기는 15.01이 남는다. // 그래서 각 HomeBodyPopularItem 위젯 사이에 SizeBox를 7.5 줄 수 있다. // Wrap 위젯은 공간이 충분하지 않을 때 Overflow 에러가 발생하지 않도록 하고 다음 줄로 넘어가게 한다(교차축 정렬). return Wrap( children: [ // id 값은 사진을 선택하기 위해 필요하다. HomeBodyPopularItem(id: 0), SizedBox(width: 7.5), HomeBodyPopularItem(id: 1), SizedBox(width: 7.5), HomeBodyPopularItem(id: 2), ], ); } }
// home/home_body_popular_item.dart import 'package:flutter/material.dart'; import 'package:flutter_airbnb/constants.dart'; import 'package:flutter_airbnb/size.dart'; import 'package:flutter_airbnb/styles.dart'; class HomeBodyPopularItem extends StatelessWidget { final id; final popularList = [ "p1.jpg", "p2.jpg", "p3.jpg", ]; HomeBodyPopularItem({required this.id}); @override Widget build(BuildContext context) { // 인기 아이템은 전체화면의 70%의 1/3만큼의 사이즈의 -5의 크기를 가진다. double popularItemWidth = getBodyWidth(context) / 3 - 5; return Padding( padding: const EdgeInsets.only(bottom: gap_xl), child: Container( constraints: BoxConstraints( minWidth: 320, ), child: SizedBox( width: popularItemWidth, child: Column( children: [ _buildPopularItemImage(), _buildPopularItemStar(), _buildPopularItemComment(), _buildPopularItemUserInfo(), ], ), ), ), ); } Widget _buildPopularItemImage() { return Column( children: [ ClipRRect( borderRadius: BorderRadius.circular(10), child: Image.asset("assets/${popularList[id]}", fit: BoxFit.cover), ), SizedBox(height: gap_s), ], ); } Widget _buildPopularItemStar() { return Column( children: [ Row( children: [ Icon(Icons.star, color: kAccentColor), Icon(Icons.star, color: kAccentColor), Icon(Icons.star, color: kAccentColor), Icon(Icons.star, color: kAccentColor), Icon(Icons.star, color: kAccentColor), ], ), SizedBox(height: gap_s), ], ); } Widget _buildPopularItemComment() { return Column( children: [ Text( "너무 좋아요 너무 좋아요 너무 좋아요 너무 좋아요 너무 좋아요 너무 좋아요 너무 좋아요 너무 좋아요 너무 좋아요 너무 좋아요 너무 좋아요 너무 좋아요 너무 좋아요", style: body1(), maxLines: 3, overflow: TextOverflow.ellipsis, // 글자가 3 라인을 벗어나면 ... 처리된다. ), SizedBox(height: gap_s), ], ); } Widget _buildPopularItemUserInfo() { return Row( children: [ CircleAvatar( backgroundImage: AssetImage("assets/profile.jpg"), ), SizedBox(width: gap_s), Column( children: [ Text( "Sewol", style: subtitle1(), ), Text("한국"), ], ) ], ); } }
728x90반응형'언어·프레임워크 > Flutter' 카테고리의 다른 글
다음글이 없습니다.이전글이 없습니다.댓글