새로운 강의는 이제 https://memi.dev 에서 진행합니다.
memi가 Vue & Firebase로 직접 만든 새로운 사이트를 소개합니다.

바로가기


Flutter와 Firebase로 Android iOS 둘 다 만들기 22 프로필 페이지 꾸미기

4 분 소요

플러터 레이아웃을 이용해 프로필 페이지를 간단히 꾸며봅니다.

개요

플러터로 앱을 제작할 때 가장 힘든 것은 역시 레이아웃입니다.

이유는 너무나도 많은 방법으로 같은 결과물을 얻을 수 있기 때문입니다.

많은 방법이 득이 될 때보다 실이 될 수 있는 좋은 예인 것 같습니다.

잘 할 수 있는 방법은 여러가지 방법으로 많은 실험을 해보고 최대한 코드를 줄여가며 익숙해지는 방법 밖에는 없는 것 같습니다.

프로필페이지

프로필페이지 라우터 생성

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),          
        ],
      ),
      // ..
    );
  }

alt profilebutton

이제 상단 우측의 프로필버튼을 클릭하면 프로필페이지로 이동합니다.

프로필페이지 꾸미기

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),
    );
  }
}

alt profile

카드와 리스트 타일을 사용해서 간단히 구색만 맞춰봤습니다.

stateful과는 다르게 stateless 위젯은 context를 하위로 넘겨줘야합니다.

현재 페이지에서 수정을 만들 수도 있지만.. 지저분해 질 것 같아서 수정은 다른 페이지에서 처리하도록 합니다.

세팅버튼을 눌러서 profile-edit 페이지로 이동시킵니다.

프로필수정

라우터 생성자에 추가

위처럼 프로필수정페이지를 만들고 라우터 생성자에 넣어줍니다.

main.dart

case ProfileEditPage.routeName: {
  return MaterialPageRoute(
    builder: (context) => ProfileEditPage(user: settings.arguments)
  );
} break;

다트 데브툴 이용하기

레이아웃이 잘 이해가 가지 않을 때는..

보기 -> 명령팔레트를 누르고 Dart: Open devtools를 눌러서 레이아웃을 확인하는 것도 방법입니다.

alt command

alt dartdev

위처럼 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 비율로 공간이 할당이 됩니다.

alt flex

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!'),
      ],
    ),
  );
}

alt photo1

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, ),
          ],
        )
      ],        
    ),          
  );    
}

alt photo2

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(),
          ),            
        ),          
      ],
    ),
  );    
}

alt name

사진처럼 패딩을 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(),
    );
  }
}

완성된 모습
alt fin

마치며

이렇게 간단한 화면을 구성하는데도 생각처럼 잘 되지 않습니다.

특정 위젯이 사이즈가 없어서 표현이 안되거나 에러가 나는 경우가 부지기수입니다.

급하게 화면을 만드려고 억지로 코드를 우겨넣어서 화면을 만들다보면 나중에 더 힘들어집니다.

컨테이너와 공간에 대한 생각을 끊임 없이 해보고 직접 테스트 해봐야 조금 감이오기 시작합니다.

레이아웃에 대해 제가 너무 잘해서 강의를 진행하는 것이 아닌, 제가 잘 못하기 때문에 좀 더 보강하는 것입니다.

소스

https://github.com/fkkmemi/ff2

영상

댓글남기기