-
Flutter - 인별 클론 코딩 V1.0, 팔로우/언팔로우목록, 로그인페이지개발/Flutter 2021. 10. 12. 16:08반응형
검색 창 대신 팔로/언팔로 하는 목록 만들기
기존 검색 페이지는 서버쪽의 일이 더 많이 필요한것으로 보임. 그래서 유저 목록을 보여주고 팔로/언팔로를 보여주는 페이지를 만들 거임.
screen 폴더에 search_page.dart를 만들어주고 stateless 위젯으로 만들어 준다. return에는 SafeArea로 만들어 주고 child에 ListVies.builder를 넣어준다. 거기에는 (context, index)를 받아서 사용하고 return에 _item을 넣어준다. 그안에 유저 정보를 Stirng 으로 받는 users[index]로 넣어준다. 그 users 에 대한 부분은 상단에 선언해준다.
final List<String> users = List.generate(10, (i) => 'user $i');
_item 은 ListTile를 사용한다. leading 부분에 CircleAvatar로 프로필 사진을 보여준다. radius와 NetworkImage를 했던데로 그대로 사용하면 된다. title을 줘서 아이디를 넣어주고 subtitle로 아이디 밑에 정보를 보여줄 수 있다. trailing으로 끝부분(화면상 오른쪽)에 추가로 아이콘을 추가해 줄 수 있다. 여기에 decoration 옵션에 BoxDecoration을 줘서 버튼처럼 보이게 Border를 준다. 그리고 child 옵션에는 Text로 following을 넣어주고 Text에 Style을 맞춰주면 된다. 나중에는 이부분이 누르면 follow와 unfollow로 바뀌게 백엔드 부분에서 해줄 부분이다. 그리고 ListView.builder를 separated로 줘서 칸을 나눠서 보이게 해줄수도 있다. separated로 바꿔주면 separatorBuilder 옵션을 추가할 수 있는데 여기에 Divider를 주면 칸처럼 나눠보이게 할 수 있다.
class SearchPage extends StatelessWidget { final List<String> users = List.generate(10, (i) => 'user $i'); @override Widget build(BuildContext context) { return SafeArea( child: ListView.separated(//ListView.builder로 줘도 됨. 칸 나누려면 ListView.separated로 주면 됨 itemBuilder: (context, index) { return _item(users[index]); }, itemCount: 10, separatorBuilder: (context, index){// separated에서 쓰이는 부분이고 divider로 나눈 부분임. return Divider(thickness: 1,color: Colors.grey[300],); }, ), ); } ListTile _item(String user) { return ListTile( leading: CircleAvatar( radius: profile_radius, backgroundImage: NetworkImage(getProfileImgPath(user)), ), title: Text(user), subtitle: Text('this is $user bio'), trailing: Container( height: 30, width: 80, alignment: Alignment.center, decoration: BoxDecoration( color: Colors.red[50], border: Border.all( color: Colors.black54, width: 0.5, ), borderRadius: BorderRadius.circular(6), ), child: Text( 'following', style: TextStyle( fontWeight: FontWeight.bold, color: Colors.red[700], ), ), ), //타일에 뒤쪽에 넣어줄수 있음(오른쪽부분이라 생각하면됨) ); } }
카메라 페이지 만들기
screens 폴더에 camera_page.dart를 생성. statefull 위젯으로 만들어 준다. Scaffold로 만들어 준다. 그런데 이 부분은 새로운 화면을 불러오는것이라 생각하면 된다. 그래서 AppBar부분에 뒤로가기 버튼은 이 부분을 끄는 버튼이라 생각하면 된다. 이때 사용하는 것은 Navigator를 사용한다. Navigator는 각각의 Stack으로 되어있다. 이 부분은 다른 강의 참조. 일단 상단에 현재 Index가 어디인지 알 수 있는 _selectedIndex와 페이지를 컨트롤 하는 _pageController 을 선언해준다.
int _selectedIndex = 1; // 카메라는 새창으로 띄워서 하는데, 사진을 선택하는 Gallary, 사진찍는 Photo, 영상찍는 Video 부분으로 이 부분을 선택하기 위한 변수이다. 기본이 Photo var _pageController = PageController( initialPage: 1); // PageView를 컨트롤 하는 부분,initialPage는 시작부분 페이지
그리고 Scaffold안에 actions에는 IconButton을 해줘서 뒤로 가기 버튼을 반들어 주고 onPressed에 Navigator.pop(context)를 해줘서 전페이지로 돌아갈 수 있게 해준다. body에는 PageView로 페이지를 보여준다. onPageChanged로 현재 인덱스를 변화시켜주고 controller은 _pageController을 줘서 페이지를 바꿀수 있게 해준다. children에 보여줄 페이지를 0,1,2 순서대로 넣어준다. 그리고 이 페이지는 기존의 BottomNavigationBar와는 다른것을 쓴다. 그리고 인스타가 아이콘을 안쓰는데 그래서 iconsize를 0으로 준다. selectedLabelStyle, unselectedItemColor,selectedItemColor,로 선택된 버튼과 안된 버튼에 차이를 준다. type는 fixed를 줘서 기존 내비게이션은 누르면 그 부분이 커지거나 했지면 여기는 변화를 주지 않게 한다. items안에 BottomNavigationBarItem을 줘서 차례대로 버튼을 만들어 준다. icon옵션이지만 사이즈를0으로 줬기때문에 아무 이미지나 넣어주면 된다. 그리고 currentIndex에 _selectedIndex를 줘서 현재 인덱스가 뭔지 알려준다. onTap 옵션에 _onItemTapped 메소드를 만들어서 context와 index를 넘겨 줘서 _pageController.animateToPage를 이용하여 해당 인덱스로 애니메이션으로 페이지를 변경하게 만들어 준다. 그리고 각 페이지는 Widget로 하단에 메소드로 만들어 준다. 여기까지 camera_page는 완성이지만 main_page에 _MainPageState에 3번째에 CameraPage를 해주면 뒤로가기가 작동하지 않는데 이는 전페이지 정보를 받아오지 않아서 인데, MainPage 하단에 카메라 버튼을 눌렀을때 Navigator.push로 정보를 넘겨줘야 한다. context와 MaterialPageRoute로 CameraPage를 부르면 된다. 그리고 _onItemTapped에 index == 2의 if문을 만들어줘서 openCamera(context)를 불러오면되고 아니면 기존값으로 주면된다. 여기서 body에 onPageChanged와 BottomNavigationBar의 onTap이 같은 기능처럼 보이지만 자세히 들여다보면, onTap은 버튼을 눌러서 화면을 바꾸는 기능만 있고 onPageChanged는 드래그해서 페이지가 변경이 가능하므로 둘다 해주면 좋다.
class _CameraPageState extends State<CameraPage> { int _selectedIndex = 1; // 카메라는 새창으로 띄워서 하는데, 사진을 선택하는 Gallary, 사진찍는 Photo, 영상찍는 Video 부분으로 이 부분을 선택하기 위한 변수이다. 기본이 Photo var _pageController = PageController( initialPage: 1); // PageView를 컨트롤 하는 부분,initialPage는 시작부분 페이지 @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( 'Photo', style: TextStyle( fontWeight: FontWeight.bold, ), ), actions: [ IconButton( onPressed: () { Navigator.pop(context); }, icon: Icon(Icons.close), ) ], ), body: PageView( //body에 컨트롤러를 줘야 변경해줄수 있음. controller: _pageController, onPageChanged: (index){ setState(() { _selectedIndex = index; }); },//바꿨을때 뭘 해줄지건 children: [ _gallaryPage(), _takePhotoPage(), _takeVideoPage(), ], ), bottomNavigationBar: BottomNavigationBar( iconSize: 0, selectedLabelStyle: TextStyle(fontWeight: FontWeight.bold), unselectedItemColor: Colors.grey[400], selectedItemColor: Colors.black, type: BottomNavigationBarType.fixed, // 내비게이션 버튼 누르면 약간 바뀌는데 이 옵션은 고정해주는것 backgroundColor: Colors.grey[50], items: const <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: ImageIcon( AssetImage("assets/grid.png"), //아무거나 줘도됨 어차피 안보임 ), label: 'GALLERY', ), BottomNavigationBarItem( icon: ImageIcon( AssetImage("assets/grid.png"), //아무거나 줘도됨 어차피 안보임 ), label: 'PHOTO', ), BottomNavigationBarItem( icon: ImageIcon( AssetImage("assets/grid.png"), //아무거나 줘도됨 어차피 안보임 ), label: 'VIDEO', ), ], currentIndex: _selectedIndex, //현재 선택된 인덱스를 넣어줘야 화면이 변함. onTap: (index) => _onItemTapped(context, index), ), //원래는 아이콘이 있어야 하지만 인스타랑 똑같이하려고 숨김, 사이즈를 0으로 주면됨 ); } void _onItemTapped(BuildContext context, int index) { _pageController.animateToPage( //애니메이션을 줘서 변경하는 메소드 index, duration: Duration(milliseconds: 200), curve: Curves.easeInOut, ); } Widget _gallaryPage(){ return Container(color:Colors.green,); }Widget _takePhotoPage(){ return Container(color:Colors.purple,); }Widget _takeVideoPage(){ return Container(color:Colors.deepOrange,); } }
로그인
가입버튼 만들기
screens에 signin_page.dart를 만들어 준다. stateful로 위젯을 만들어 준다. 그리고 우선 페이지를 확인하기 위해 main.dart의 home 부분을 SignInPage()로 바꿔준다. 그 안에 SafeArea를 만들어주고 Join 과 Login 두 페이지를 위해서 child는 Stack로 해준다. 이 부분이 로그인 페이지 부분인데 가장 하단에 가입부분으로 가는 부분을 만든다. Positioned의 위젯을 만들어 주고 left 0, right 0, bottom 0, height 40인 Positioned를 return 해준다. child는 FlatButton으로 만들어 준다. 경계선을 위해서 shape에 Border을 만들어주고 top에만 BorderSide 옵션으로 색을 준다. child는 textSpan에 style이 2개이상 쓰이기 때문에 RichText로 사용한다.text 옵션에 TextSapn으로 준다.(여기서 style 옵션에 빈 TextStyle만 주는데 이유는 모르겠음.) children에 TextSpan을 주고 맞는 text와 style을 주면 된다.
class _SignInPageState extends State<SignInPage> { @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Stack( children: [ _goToSignUpPageBtn(context), ], ), ), ); } Positioned _goToSignUpPageBtn(BuildContext context) { //제일하단 부분 return Positioned( left: 0, right: 0, bottom: 0, height: 40, child: FlatButton( shape: Border( top: BorderSide(color: Colors.grey[300],), ), onPressed: () {}, child: RichText( // Text 한가지에 2가지 스타일이 있어서 씀씀 textAlign: TextAlign.center, text: TextSpan( style: const TextStyle(), //이거는 2개 따로주는데에 먹히지 않음 안주면 작동안함 그냥 이대로 넣어두어야함, 왜인지는 모름 children: [ TextSpan( text: "Don't have an account?", style: TextStyle( fontWeight: FontWeight.w300, color: Colors.black54,), ), TextSpan( text: " Sign Up", style: TextStyle( fontWeight: FontWeight.bold, color: Colors.blue[600],), ) ], ), ), ), ); } }
로그인 Form 생성
로그인폼 부분은 우선 widgets 폴더에 sign_in_form.dart를 만들어주고 stateful 위젯으로 만들어 준다.
GlobalKey<FormState> _formKey = GlobalKey<FormState>(); TextEditingController _emailController = TextEditingController(); TextEditingController _pwCotroller = TextEditingController();
상단에 위의 코드를 선언해주는데, _formKey는 Form에서 사용하고 상태를 알 수 있다. Controller들도 유효성 검사나 TextForm에서 쓰이는 것들이다. 그리고 dispose도 선언해준다. 선언해 주지 않으면 페이지가 닫히더라도 계속 동작하므로 dispose를 통해 닫아준다. 그리고 Scaffold를 만들어 주고 그 안에 resizeToAvoidBottomInset를 true로 해줘서 키보드가 올라올때 같이 위로 올라오게 해준다. 그리고 sign_page의 Scaffold에는 같은 옵션에 false를 줘서 가입 버튼이 올라오지 않게 해준다. 그리고 Form을 선언해주고 key를 _formKey로 해줘서 Form안에서 유효성검사등을 확인할 수 있다. 그 안에는 Column으로 세로축으로 만들어준다. mainAxisAlignment은 center, mainAxisSize는 max, crossAxisAlignment는 strech 옵션을 주고 children에 만들면 된다. Spacer로 중간에 공간을 줘서 넓이를 맞춰준다. 로고를 넣어주고 TextFormField로 이메일 필드를 만들어 준다. controller에 _emailController을 넣어주고 decoration은 메소드를 만들어서 스타일을 해주면 된다. 암호란과 똑같아서 메소드로 해주면 된다. 그리고 validator를 해줘서 유효성을 검사해준다. 이 부분은 버튼이 눌릴때 _keyForm으로 상태를 확인하고 밑에 힌트메시지를 띄워주기도 한다. 암호도 똑같이 만들어준다. decoration 부분은 아래에 메소드로 만들어주고, InputDecoration으로 해준다. hintText는 포커스전에 떠있는 글씨부분이다. enabledBorder 옵션과 focusedBorder 옵션의 스타일을 똑같이 해준다. radius도 정해주고 fillcolor의 색을 정해준후 filled를 true로 해준다. 옵션을 안넣거나 false로 하면 색이 채워지지 않는다. 암호 입력 부분 밑에 암호를 잃어버렸을때의 기능을 우선 그냥 Text로만 해둔다. 그리고 로그인 버튼을 FlatButton으로 만들어 준다. onPressed에 if를 써서 _formKey의 currentState에서 validate로 현재 상태를 받는다. 각 TextFormField에서 calidator가 전부 정상으로 작동하면 true를 넘겨준다. 그때 메인페이지로 넘어가는 기능을 만들어 준다. MaterialPageRoute에 builder에서 (context)=>MainPage()하여 메인 페이지로 보내주고 Navigator로 그냥 push가 아닌 pushReplacement(context, route)로 페이지를 그냥 쌓아두게하는것이 아닌 교체로 넣어준다. 그냥 push면 로그아웃이나 로그인을 할때 그 페이지가 남기 때문이다. 그리고 로그인 부분과 페이스북로그인 사이에 or를 만들어 주는데 이 부분은 따로 기능이 없어 Stack를 이용한다. 우선 회색선을 넣어주고 그위에 짧은 길이의 배경색과 같은 선을 넣어준후 OR글씨를 넣어주면 된다. 그리고 그 하단에 페이스북 로그인버튼을 만들어 준다. FlatButton.icon으로 만들어 준다.onPressed에는 현재 기능은 없지만 정상작동하나 알기 위해서 SnackBar를 이용해본다. simpleSnackbar(context, 'facebook pressed');로 context와 메시지를 넘겨준다. 여기서 context는 Scaffold의 context여야 한다. signin_page 처럼 scaffold보다 위에서 context를 받아서 스낵바를 사용하면 에러가 난다. 그리고 utils에 simple_snack_bar.dart를 만들어 준다.
void simpleSnackbar(BuildContext context, String txt) { //스낵바는 Scaffold에 쓰여야 하지만 context를 가져와서 이렇게 하면 보여줄 수 있음음 final snackBar = SnackBar(content: Text(txt)); Scaffold.of(context).showSnackBar(snackBar); //context를 잘 받아와야함 SignInPage에서 스낵바를 하려면 Context가 Scaffold 안에 없다고 에러가 남 }
위와 같이 작성해서 사용하면 스낵바가 정상 작동하는 것을 볼 수 있다. 그리고 메인 페이지에서 사이드메뉴에 로그아웃 버튼에서도 route와 pushReplacement로 SignInPage()로 이동시켜주는 기능을 넣는다.
그리고 마지막으로 로딩화면 기능을 따로 빼준다. widgets에 my_progress_indicator.dart를 만들어준다. 원래 로딩있던 부분을 가져와준다. 그리고 로딩이미지 크기, 현재 그 이미지가 동작할 박스의 크기를 옵션으로 사용할 수 있게 해준다.
class MyProgressIndicator extends StatelessWidget { final double containerSize; final double progressSize; const MyProgressIndicator({ Key key, this.containerSize, this.progressSize = 60, }) : super(key: key); @override Widget build(BuildContext context) { return Container( // 화면 전체를 받아버림 조그만 이미지 프사나 버튼에서도 사용하고 싶어서 옵션을 줄것임 width: containerSize, height: containerSize, child: Center( child: SizedBox( width: progressSize, height: progressSize, child: Image.asset('assets/loading_img.gif'), ), ), ); } }
위와 같이 작성해주면 된다.
사인폼의 전체 코드이다.
class _SignInFormState extends State<SignInForm> { GlobalKey<FormState> _formKey = GlobalKey<FormState>(); TextEditingController _emailController = TextEditingController(); TextEditingController _pwCotroller = TextEditingController(); @override void dispose() { // 페이지가 닫힐때 필요없는 부분 닫는거, dispose 안해주면 계속 동작으로 실행되서 좋지 않음 _emailController.dispose(); _pwCotroller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: true, // 키보드가 올라올때 화면이 올라가는 부분, 전체가 올라가는게 아님 다른데서 만든 밑에 가입 버튼은 키보드에 가려지고 여기에서 만든 부분만 됨 body: Padding( padding: const EdgeInsets.all(common_gap), child: Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, //정렬,위,아래,가운데 정하는것 mainAxisSize: MainAxisSize.max, //세로 공간 다 차지하게 crossAxisAlignment: CrossAxisAlignment.stretch, //column일때는 가로축 Row일때는 세로축 cross임, stretch는 좌우로 늘려서 공간 차지 children: [ Spacer( flex: 6, //공간차지 ), Image.asset("assets/insta_text_logo.png"), Spacer( flex: 1, ), TextFormField( controller: _emailController, decoration: getTextFieldDecor('email'), validator: (String value) { if (value.isEmpty || !value.contains("@")) { return 'Please enter your email address!'; } return null; }, //유효성 검사하는 부분 ), Spacer( flex: 1, ), TextFormField( controller: _pwCotroller, decoration: getTextFieldDecor('Password'), validator: (String value) { if (value.isEmpty) { return 'Please enter any password!'; } return null; }, ), Spacer( flex: 1, ), Text( "Forgotten password?", textAlign: TextAlign.end, style: TextStyle( color: Colors.blue[700], fontWeight: FontWeight.w600, ), ), Spacer( flex: 2, ), FlatButton( onPressed: () { if (_formKey.currentState.validate()) { // Form 안에 validator가 작동을 함. final route = MaterialPageRoute(builder: (context)=>MainPage()); Navigator.pushReplacement(context, route); } }, child: Text( "Log in", style: TextStyle( color: Colors.white, ), ), color: Colors.blue, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), ), disabledColor: Colors.blue[100], ), Spacer( flex: 2, ), Stack( // or 부분은 따로 만드는 부분이 없어서 선위에 흰선을 겹친거임, 그위에 or 를 써서 인스타랑 비슷하게 만듬 alignment: Alignment.center, children: [ Positioned( // 선을 끝까지 주기 위해서하는 부분 left 0, right 0을 줌 left: 0, right: 0, height: 1, child: Container( color: Colors.grey[300], height: 1, ), ), Container( height: 3, width: 50, color: Colors.grey[50], //배경색이랑 같음 ), Text( "OR", style: TextStyle( color: Colors.grey, fontWeight: FontWeight.bold, ), ), ], ), Spacer( flex: 2, ), FlatButton.icon( //페이스북 로그인 부분 textColor: Colors.blue, onPressed: () { simpleSnackbar(context, 'facebook pressed'); }, icon: ImageIcon(AssetImage("assets/icon/facebook.png")), label: Text("Login with Facebook"), ), Spacer( flex: 2, ), Spacer( flex: 6, ), ], ), ), ), ); } InputDecoration getTextFieldDecor(String hint) { return InputDecoration( hintText: hint, enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.grey[300], width: 1, ), borderRadius: BorderRadius.circular(12), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.grey[300], width: 1, ), borderRadius: BorderRadius.circular(12), ), fillColor: Colors.grey[100], filled: true, // true로 해줘야 fillcolor이 나타남 ); } }
로그인 부분은 끝이다.
반응형'개발 > Flutter' 카테고리의 다른 글
Flutter - 인별 클론 코딩 V1.0,백엔드 종료,강의가 오래되서 버전이 맞지않음 (0) 2021.10.19 Flutter - 인별 클론 코딩 V1.0,가입페이지 (0) 2021.10.19 Flutter - 인별 클론 코딩 V1.0, 프로필 화면-4 (0) 2021.10.08 Flutter - 인별 클론 코딩 V1.0, 프로필 화면-3 (0) 2021.10.08 Flutter - 인별 클론 코딩 V1.0, 프로필 화면-2 (0) 2021.10.06 댓글