새로운 강의는 이제 https://memi.dev 에서 진행합니다.
memi가 Vue & Firebase로 직접 만든 새로운 사이트를 소개합니다.
Flutter와 Firebase로 Android iOS 둘 다 만들기 22 프로필 페이지 꾸미기
플러터 레이아웃을 이용해 프로필 페이지를 간단히 꾸며봅니다.
개요
플러터로 앱을 제작할 때 가장 힘든 것은 역시 레이아웃입니다.
이유는 너무나도 많은 방법으로 같은 결과물을 얻을 수 있기 때문입니다.
많은 방법이 득이 될 때보다 실이 될 수 있는 좋은 예인 것 같습니다.
잘 할 수 있는 방법은 여러가지 방법으로 많은 실험을 해보고 최대한 코드를 줄여가며 익숙해지는 방법 밖에는 없는 것 같습니다.
프로필페이지
프로필페이지 라우터 생성
main.dart
case ProfilePage.routeName: {
return MaterialPageRoute(
builder: (context) => ProfilePage(user: settings.arguments)
);
} break;
프로필페이지에도 user정보를 넘겨줍니다.
프로필페이지 버튼 꾸미기
pages/home.dart
Widget _buildProfile(context) {
return Padding(
padding: EdgeInsets.all(8),
child: InkWell(
child: CircleAvatar(
backgroundImage: NetworkImage(user.photoUrl),
),
onTap: () {
Navigator.pushNamed(context, '/profile', arguments: user);
},
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('HomePage'),
actions: <Widget>[
_buildProfile(context),
],
),
// ..
);
}
이제 상단 우측의 프로필버튼을 클릭하면 프로필페이지로 이동합니다.
프로필페이지 꾸미기
pages/profile.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
class ProfilePage extends StatelessWidget {
const ProfilePage({Key key, this.user}) : super(key: key);
static const routeName = '/profile';
final FirebaseUser user;
Widget _buildCard(BuildContext context) {
return Card(
child: ListTile(
leading: Image.network(user.photoUrl),
title: Text(user.displayName),
subtitle: Text(user.email),
trailing: IconButton(
icon: Icon(Icons.settings),
onPressed: () {
Navigator.pushNamed(context, '/profile-edit', arguments: user);
},
),
),
);
}
Widget _buildSignOut(BuildContext context) {
return ButtonBar(
alignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: Text('Sign out'),
onPressed: () async {
await FirebaseAuth.instance.signOut();
Navigator.pushNamedAndRemoveUntil(context, '/auth', (route) => false);
// Navigator.pushReplacementNamed(context, '/auth');
},
)
],
);
}
Widget _buildBody(BuildContext context) {
return SingleChildScrollView(
child: Container(
padding: EdgeInsets.all(10),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
_buildCard(context),
_buildSignOut(context),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Profile'),),
body: _buildBody(context),
);
}
}
카드와 리스트 타일을 사용해서 간단히 구색만 맞춰봤습니다.
stateful과는 다르게 stateless 위젯은 context를 하위로 넘겨줘야합니다.
현재 페이지에서 수정을 만들 수도 있지만.. 지저분해 질 것 같아서 수정은 다른 페이지에서 처리하도록 합니다.
세팅버튼을 눌러서 profile-edit 페이지로 이동시킵니다.
프로필수정
라우터 생성자에 추가
위처럼 프로필수정페이지를 만들고 라우터 생성자에 넣어줍니다.
main.dart
case ProfileEditPage.routeName: {
return MaterialPageRoute(
builder: (context) => ProfileEditPage(user: settings.arguments)
);
} break;
다트 데브툴 이용하기
레이아웃이 잘 이해가 가지 않을 때는..
보기 -> 명령팔레트를 누르고 Dart: Open devtools를 눌러서 레이아웃을 확인하는 것도 방법입니다.
위처럼 Debug paint, Paint Baselines등으로 바로 확인이 가능합니다.
프로필 수정 페이지 초안
pages/profile_edit.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class ProfileEditPage extends StatefulWidget {
ProfileEditPage({Key key, this.user}) : super(key: key);
static const routeName = '/profile-edit';
final FirebaseUser user;
@override
_ProfileEditPageState createState() => _ProfileEditPageState();
}
class _ProfileEditPageState extends State<ProfileEditPage> {
Widget _buildBody() {
return SingleChildScrollView(
child: Container(
color: Colors.redAccent,
height: 200,
padding: EdgeInsets.all(10),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Profile Edit'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.save),
onPressed: () {},
),
],
),
body: _buildBody(),
);
}
}
컨테이너에는 크기와 색상을 넣어 둡니다.
이렇게 해야 레이아웃이 눈에 보여서 구성하기가 좋습니다.
여기에서 제일 중요한 것은 height 200입니다.
200 높이를 지정하고 그 안에서 자식들이 위치를 잡게 합니다.
사진, 이름 자리 잡기
pages/profile_edit.dart
Widget _buildBody() {
return SingleChildScrollView(
child: Container(
color: Colors.redAccent,
height: 200,
padding: EdgeInsets.all(10),
child: Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
flex: 2,
child: Container(color: Colors.blueAccent,),
),
Expanded(
flex: 3,
child: Container(color: Colors.brown,),
),
],
),
),
),
);
}
Row로 사진과 이름이 들어갈 자리를 만들어둡니다.
Expanded의 flex로 2/3 비율로 공간이 할당이 됩니다.
flex의 경우 웹프레임워크 대부분이 10, 12, 16등을 나눠서 쓰는데(3 + 3 + 6 = 12) 플러터의 경우 특이하게 비율로 되어있어서 더 나은 것 같습니다.
사진 꾸미기 초안
Widget _buildPhoto() {
return Container(
padding: EdgeInsets.all(10),
color: Colors.teal,
child: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
Container(
child: Image.network(widget.user.photoUrl),
),
Text('hi yo!'),
],
),
);
}
Stack이라는 위젯은 자식들끼리 겹치게 할 수 있는 위젯입니다.
제일 중요한 옵션이 alignment인데요 바닥 가운데로 지정하게 하여 글씨와 사진이 겹치게 구성되어 있습니다.
그런데 사진 사이즈가 작아서 원하는데로 꽉차지가 않습니다.
사진 꾸미기 마무리
Widget _buildPhoto() {
return Container(
padding: EdgeInsets.all(10),
color: Colors.teal,
child: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
Container(
color: Colors.teal,
constraints: BoxConstraints.expand(),
child: Image.network(widget.user.photoUrl, fit: BoxFit.cover),
),
Container(
color: Colors.black54,
height: 40,
),
ButtonBar(
alignment: MainAxisAlignment.end,
children: <Widget>[
Icon(Icons.photo_camera, color: Colors.white, ),
Icon(Icons.photo_library, color: Colors.white, ),
],
)
],
),
);
}
constraints으로 확장시키고 Image의 fit으로 조정해서 남은 사이즈에 꽉차게 했습니다.
버튼바는 좌우 100% 확장 성질이 있습니다.
반투명한 컨테이너를 버튼바에 겹치게 만들어 봤습니다.
이름 꾸미기
Widget _buildName() {
return Container(
padding: EdgeInsets.all(10),
color: Colors.brown,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
TextFormField(
decoration: InputDecoration(
labelText: 'First name*',
hintText: 'John',
border: OutlineInputBorder(),
),
),
TextFormField(
decoration: InputDecoration(
labelText: 'Last name*',
hintText: 'Doe',
border: OutlineInputBorder(),
),
),
],
),
);
}
사진처럼 패딩을 10 줘서 균형을 잡고.. Column으로 남은 공간을 적당하게 분배해서 입력폼을 넣습니다.
마무리
레이아웃을 보기위해 지정했던 색상을 모두 지웁니다.
pages/profile_edit.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class ProfileEditPage extends StatefulWidget {
ProfileEditPage({Key key, this.user}) : super(key: key);
static const routeName = '/profile-edit';
final FirebaseUser user;
@override
_ProfileEditPageState createState() => _ProfileEditPageState();
}
class _ProfileEditPageState extends State<ProfileEditPage> {
Widget _buildPhoto() {
return Container(
padding: EdgeInsets.all(10),
child: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
Container(
constraints: BoxConstraints.expand(),
child: Image.network(widget.user.photoUrl, fit: BoxFit.cover),
),
Container(
color: Colors.black54,
height: 40,
),
ButtonBar(
alignment: MainAxisAlignment.end,
children: <Widget>[
Icon(Icons.photo_camera, color: Colors.white, ),
Icon(Icons.photo_library, color: Colors.white, ),
],
)
],
),
);
}
Widget _buildName() {
return Container(
padding: EdgeInsets.all(10),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
TextFormField(
decoration: InputDecoration(
labelText: 'First name*',
hintText: 'John',
border: OutlineInputBorder(),
),
),
TextFormField(
decoration: InputDecoration(
labelText: 'Last name*',
hintText: 'Doe',
border: OutlineInputBorder(),
),
),
],
),
);
}
Widget _buildBody() {
return SingleChildScrollView(
child: Container(
height: 200,
padding: EdgeInsets.all(10),
child: Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
flex: 2,
child: _buildPhoto(),
),
Expanded(
flex: 3,
child: _buildName(),
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Profile Edit'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.save),
onPressed: () {},
),
],
),
body: _buildBody(),
);
}
}
완성된 모습
마치며
이렇게 간단한 화면을 구성하는데도 생각처럼 잘 되지 않습니다.
특정 위젯이 사이즈가 없어서 표현이 안되거나 에러가 나는 경우가 부지기수입니다.
급하게 화면을 만드려고 억지로 코드를 우겨넣어서 화면을 만들다보면 나중에 더 힘들어집니다.
컨테이너와 공간에 대한 생각을 끊임 없이 해보고 직접 테스트 해봐야 조금 감이오기 시작합니다.
레이아웃에 대해 제가 너무 잘해서 강의를 진행하는 것이 아닌, 제가 잘 못하기 때문에 좀 더 보강하는 것입니다.
소스
https://github.com/fkkmemi/ff2
댓글남기기