Flutter

[Flutter] 연습하기3 - Flutter profile app

연화 2025. 1. 10. 14:17

 

이 블로그 포스팅에서는 Flutter를 이용해 프로필 페이지를 구현하는 방법을 설명합니다.
각 섹션을 나누어 테마 설정부터 프로필 헤더, 버튼, 그리고 탭을 사용하는 방식까지 순차적으로 구성해보겠습니다.

1. 앱 테마 설정하기 (theme.dart)

앱의 테마는 전체 디자인에서 중요한 부분을 차지합니다. Flutter에서는 ThemeData 클래스를 사용하여 앱의 테마를 설정할 수 있습니다. 이를 통해 색상, 글꼴, 위젯 스타일 등을 일관되게 적용할 수 있습니다.

import 'package:flutter/material.dart';
// 보통 협업 --> 디자이너
// 전역 함수로 만들어 보자.

const MaterialColor primaryWhite = MaterialColor(
  0xFFFFFFFF, // Base color: White
  <int, Color>{
    50: Color(0xFFFFFFFF), // 50% opacity
    100: Color(0xFFF9F9F9), // Lighter shade
    200: Color(0xFFF2F2F2),
    300: Color(0xFFE6E6E6),
    400: Color(0xFFD9D9D9),
    500: Color(0xCCCCCC), // Mid-light gray
    600: Color(0xB3B3B3), // Darker gray
    700: Color(0x999999), // Darker shade
    800: Color(0x808080), // Even darker shade
    900: Color(0x666666), // Very dark gray
  },
);

// 전역 함수
ThemeData theme() {
  // 앱 전반적인 테마(색상, 글꼴, 위젯 스타일등)를 정의하는 클래스
  // 입니다. --> 일관된 디자인을 유지하기 위해 사용한다.
  return ThemeData(
    // 앱의 기본 색상 팔레트를 설정하는 속성입니다.
    primarySwatch: primaryWhite,
    appBarTheme: const AppBarTheme(
      iconTheme: IconThemeData(color: Colors.blue),
    ),
  );
}

2. 메인 앱 설정하기 (main.dart)

main.dart는 앱을 실행하는 주요 파일입니다. MaterialApp을 사용하여 전체 앱의 스타일과 레이아웃을 설정합니다.

import 'package:flutter/material.dart';
import 'package:flutter_profile_app/theme.dart';

import 'pages/profile_page.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  // 객체를 const 사용하려면 생성자가 const 생성자이어야 한다
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: theme(),
      home: ProfilePage(),
    );
  }
}

3. 메인 페이지 설정하기 (profile_page.dart)

profile_page.dart는 페이지를 정의하는 파일입니다. SafeArea를 사용하여 앱의 안전구역을 설정하고, Scaffold 위젯을 활용해 페이지를 구성하는 데 필요한 위젯들을 정의합니다.

import 'package:flutter/material.dart';

import '../components/my_profile_buttons.dart';
import '../components/profile_buttons.dart';
import '../components/profile_count_info.dart';
import '../components/profile_header.dart';
import '../components/profile_tab_bar.dart';
import '../components/side_bar.dart';

// 페이지 단위의 위젯을 만들어 보자 --> 클래스로
// 우리들의 규칙 2 Scaffold
class ProfilePage extends StatelessWidget {
  const ProfilePage({super.key});

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        appBar: AppBar(
          centerTitle: true,
          title: Text('Profile'),
        ),
        endDrawer: SideBar(),
        body: Column(
          children: [
            const SizedBox(height: 20),
            ProfileHeader(),
            const SizedBox(height: 20),
            profileCountInfo(),
            const SizedBox(height: 20),
            // MyProfileButtons(),
            ProfileButtons(),
            Expanded(child: ProfileTab()),
          ],
        ),
      ),
    );
  }
}

4. 프로필 헤더 만들기

프로필의 상단에는 사용자의 이름과 직업, 사진을 보여주는 헤더가 필요합니다. 이를 Row 위젯을 이용해 간단히 구성할 수 있습니다.

import 'package:flutter/material.dart';

class ProfileHeader extends StatelessWidget {
  const ProfileHeader({super.key});

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        const SizedBox(width: 20),
        _buildHeaderAvatar(),
        const SizedBox(width: 20),
        _buildHeaderProfile()
      ],
    );
  }

  Column _buildHeaderProfile() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          '홍길동',
          style: TextStyle(
            fontSize: 25,
            fontWeight: FontWeight.w700,
          ),
        ),
        Text(
          '프로그래머 / 작가',
          style: TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.w700,
          ),
        ),
        Text(
          '데어 프로그래밍',
          style: TextStyle(
            fontSize: 15,
            fontWeight: FontWeight.w700,
          ),
        )
      ],
    );
  }

  SizedBox _buildHeaderAvatar() {
    return SizedBox(
      height: 100,
      width: 100,
      child: CircleAvatar(
        // 에셋이미지는 보통 위젯의 배경으로 동작할 때, 꾸밀 때 많이 활용하는 위젯이다
        backgroundImage: AssetImage('assets/avatar.png'),
      ),
    );
  }
}

5. 프로필 카운트 정보

사용자의 게시물, 좋아요, 공유 등과 같은 통계 정보를 보여주는 위젯을 만들 수 있습니다. Row와 Column 위젯을 활용하여 각 항목을 표시할 수 있습니다.

import 'package:flutter/material.dart';

// 프로필 카운트 인포 위젯 만들어 보기
class profileCountInfo extends StatelessWidget {
  const profileCountInfo({super.key});

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        _buildInfo('50', 'Posts'),
        _buildLine(),
        _buildInfo('10', 'Likes'),
        _buildLine(),
        _buildInfo('3', 'Share'),
      ],
    );
  }

  Container _buildLine() {
    return Container(
      width: 2,
      height: 50,
      color: Colors.blueAccent,
    );
  }

  Widget _buildInfo(String count, String title) {
    return Column(
      children: [
        Text(
          count,
          style: TextStyle(fontSize: 15),
        ),
        const SizedBox(height: 5),
        Text(
          title,
          style: TextStyle(fontSize: 15),
        ),
      ],
    );
  }
}

5. 프로필 버튼 만들기

Follow와 Message 버튼을 만들어 봅니다. InkWell 위젯을 사용하여 터치 이벤트를 처리하고, 버튼의 스타일을 Container와 BoxDecoration을 활용하여 꾸밉니다.

import 'package:flutter/material.dart';

// 버튼 만들어 보기
class ProfileButtons extends StatelessWidget {
  const ProfileButtons({super.key});

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        _buildFollowButton(),
        _buildMessageButton(),
      ],
    );
  }

  InkWell _buildMessageButton() {
    // InkWell: 터치 이벤트(탭, 더블탭) 감지하고 시각적 피드백도 제공합니다.
    // 터치하고자하는 영역을 감싸서 만들 수 있다.
    return InkWell(
      onTap: () {
        print('버튼 클릭');
      },
      child: Container(
        alignment: Alignment.center,
        width: 160,
        height: 40,
        child: Text(
          'Message',
          style: TextStyle(color: Colors.black87),
        ),
        decoration: BoxDecoration(
          color: Colors.white,
          border: Border.all(color: Colors.grey),
          borderRadius: BorderRadius.circular(10),
        ),
      ),
    );
  }

  InkWell _buildFollowButton() {
    return InkWell(
      onTap: () {
        print('버튼 클릭');
      },
      child: Container(
        alignment: Alignment.center,
        width: 160,
        height: 40,
        child: Text(
          'Follow',
          style: TextStyle(color: Colors.white),
        ),
        decoration: BoxDecoration(
          color: Colors.blueAccent,
          border: Border.all(color: Colors.white),
          borderRadius: BorderRadius.circular(10),
        ),
      ),
    );
  }
}

* 아래의 코드는 OutlineButton을 메서드로 만들어 처리해본 코드입니다.

import 'package:flutter/material.dart';

class MyProfileButtons extends StatelessWidget {
  const MyProfileButtons({super.key});

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        _buildOutlineButton(Colors.blueAccent, Colors.white, 'Follow', 160),
        _buildOutlineButton(Colors.white, Colors.black87, 'Message', 160),
      ],
    );
  }

  SizedBox _buildOutlineButton(
      Color buttonColor, Color textColor, String title, double width) {
    return SizedBox(
      width: width,
      child: OutlinedButton(
        style: OutlinedButton.styleFrom(
          backgroundColor: buttonColor,
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.all(
              Radius.circular(10),
            ),
          ),
        ),
        onPressed: () {
          print('팔로우합니다.');
        },
        child: Text(
          title,
          style: TextStyle(color: textColor),
        ),
      ),
    );
  }
}

6. 프로필 탭 구현하기

프로필 화면에서는 두 개의 탭을 만들어 TabBar와 TabBarView를 사용하여 각각 다른 화면을 표시합니다.

import 'package:flutter/material.dart';

// 상태가 있는 위젯을 만들어 보자
// 1. StatefulWidget 위젯을 상속받았다.
// 두 개의 클래스가 한 묶음이다

class ProfileTab extends StatefulWidget {
  const ProfileTab({super.key});

  @override
  State<ProfileTab> createState() => _ProfileTabState();
}

class _ProfileTabState extends State<ProfileTab>
    with SingleTickerProviderStateMixin {
  // 멤버 변수
  // tabController는 TabBar와 TabBarView를 동기화하는 컨트롤러입니다.
  TabController? _tabController;

  // 단 한 번 호출되는 메서드이다.
  @override
  void initState() {
    super.initState();
    print('프로필 탭 내부 클래스 init 호출했다');
    // length 는 탭의 개수를 의미한다.
    // vsync는 자연스러운 애니메이션 전환을 위해서 TickerProvider를 활용한다.
    _tabController = TabController(length: 2, vsync: this);
  }

  // build 메서드는 기본적으로 그림을 그릴 때 호출이 된다.
  @override
  Widget build(BuildContext context) {
    // 화면을 그려주는 영역
    print('빌드 호출');
    return Column(
      children: [
        _buildTabBar(),
        Expanded(
          child: _buildTabBarView(),
        ),
      ],
    );
  }

  TabBarView _buildTabBarView() {
    return TabBarView(
      controller: _tabController,
      children: [
        GridView.builder(
          itemCount: 40,
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
            crossAxisSpacing: 1, // 열 (컬럼) 사이의 간격을 10으로 설정
            mainAxisSpacing: 1,
          ),
          itemBuilder: (context, index) {
            return Image.network(
              'https://picsum.photos/id/${index}/200',
              //fit: BoxFit.cover,
            );
          },
        ),
        ListView.separated(
          itemCount: 28,
          itemBuilder: (context, index) {
            return Padding(
              padding: const EdgeInsets.symmetric(vertical: 10),
              child: ListTile(
                  leading: CircleAvatar(
                    radius: 28,
                    backgroundImage: NetworkImage(
                        'https://picsum.photos/id/${index + 100}/200'),
                  ),
                  title: Text('My Friends ${index + 1}'),
                  subtitle: Text('새로운 친구를 만들어 봐요.'),
                  trailing: Wrap(
                    children: [
                      _sendMessageButton(context, index),
                      _addFriendsButton(context, index),
                    ],
                  )),
            );
          },
          separatorBuilder: (context, index) {
            return Divider(
              thickness: 0.5,
            );
          },
        ),
      ],
    );
  }

  IconButton _addFriendsButton(BuildContext context, int index) {
    return IconButton(
      onPressed: () {
        showDialog(
          context: context,
          builder: (context) {
            return AlertDialog(
              title: Text('My Friends ${index + 1} 님을 친구로 추가하시겠습니까?'),
              content: Text(
                '친구가 수락하면 친구 관계가 형성됩니다!',
                style: TextStyle(fontSize: 18),
              ),
              actions: [
                TextButton(
                    onPressed: () => Navigator.of(context).pop(),
                    child: const Text("Yes")),
              ],
            );
          },
        );
      },
      icon: Icon(
        Icons.add,
        color: Colors.grey,
      ),
    );
  }

  IconButton _sendMessageButton(BuildContext context, int index) {
    return IconButton(
      onPressed: () {
        showDialog(
          context: context,
          builder: (context) {
            return AlertDialog(
              title: Text('My Friends ${index + 1} 님에게 빠른 메세지를 보내시겠습니까?'),
              content: TextField(),
              actions: [
                TextButton(
                    onPressed: () => Navigator.of(context).pop(),
                    child: const Text("Send")),
              ],
            );
          },
        );
      },
      icon: Icon(
        Icons.send,
        color: Colors.grey,
      ),
    );
  }

  TabBar _buildTabBar() {
    return TabBar(
      // 중간 매개체로 연결
      controller: _tabController,
      tabs: [
        Tab(
          icon: Icon(Icons.ac_unit),
        ),
        Tab(
          icon: Icon(Icons.people_alt),
        ),
      ],
    );
  }
}

7. 최종 화면

 

 

이와 같이 Flutter를 사용하여 프로필 페이지를 구성할 수 있습니다.
각 부분은 StatelessWidget 또는 StatefulWidget으로 구성되어 있으며,  이번에는 StatefulWidget 도 활용해서 flutter의 상태를 연습해보았습니다. TabController를 사용하여 탭 기능을 구현했습니다.
이를 통해 프로필 화면에서 다양한 정보를 효과적으로 표현할 수 있습니다.

 

 

아래의 문헌을 참고하여 작성된 포스팅입니다.
최주호, 김근호, 이지원(공저) 『만들면서 배우는 플러터 앱 프로그래밍』, 앤써북, 2023.