DAY 15 | Flutter 데모 앱 만들기: 홈 화면

Pt J·2026년 8월 3일
post-thumbnail

DAY 15 | Flutter 데모 앱 만들기: 홈 화면

애플리케이션의 홈 화면을 구성하고, 작성한 widget을 custom widget으로 빼서 관리해 보자.

홈 화면 구성하기

먼저 lib/workout_home_page.dart 파일을 만들고 StatefulWidget 으로 WorkoutHomePage widget을 생성한 뒤, 필요한 widget을 순차적으로 작성한다.

우선은 여백을 고려하지 않고 전체적인 틀을 작성한 뒤, 이후에 여백을 조정하도록 하겠다.

part 01: 상단 바

AppBar widget을 사용하지 않고 자체적인 상단 바를 사용한다. 이것은 좌측 로고 아이콘과 우측 알림 아이콘으로 구성된다.

AppBar widget을 사용하지 않기에 Scaffold 아래에 SafeArea 를 넣고 그 아래에 Column 을 넣는다.

lib/workout_home_page.dart > WorkoutHomePage

// Scaffold > SafeArea > Column
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
      Image.asset('assets/home/runner_icon.png', width: 24),
      Image.asset('assets/home/notifications_icon.png', width: 24),
    ],
),

part 02: 프로필

프로필 부분은 텍스트와 이미지로 구성된다. 텍스트 부분에는 여러 가지 스타일이 섞여 사용되기 때문에 TextSpan widget을 사용한다. 이미지 부분은 배경 위에 사진을 올릴 것이기에 Stack widget을 사용한다. 이 때, 프로필 사진을 둥글게 자르는 건 WidgetListPage 에서 했던 것처럼 Container 를 원형으로 꾸며주면 된다.

TextTheme 는 매번 Theme.of(context) 에서 꺼내 쓰지 않고 변수로 할당해서 사용하도록 하겠다.

텍스트에 스타일을 지정할 때 특정 부분만 textTheme 에서 가져오고 나머지는 직접 작성하거나 기본값을 사용하고자 한다면 다음과 같이 작성한다.

style: TextStyle(
  fontSize: textTheme.titleMedium?.fontSize,
  fontWeight: FontWeight.bold,
)

반면, 특정 부분만 직접 작성하고 나머지는 textTheme 에서 가져오고 싶다면 style property에 직접 전달 후 따로 지정하고 싶은 부분만 copyWith 로 전달한다.

style: textTheme.titleMedium?.copyWith(
  fontWeight: FontWeight.bold,
)

copyWith 는 object의 일부만 수정하여 복사할 때 사용하는 녀석으로, 자주 쓰이니 기억해 두도록 하자.

이전 영역에서는 두 아이콘 사이에 여백을 주는 것이기에 mainAxisAlignment: MainAxisAlignment.spaceBetween 으로 작성했다. 여기서는 텍스트와 이미지 사이에 여백을 주기보다는 텍스트 쪽에 여백을 주고자 하여 Expanded 로 감싸는 방식을 사용했다. (보여지는 모습은 같지만 개념적인 차이다.)

lib/workout_home_page.dart > WorkoutHomePage

// Scaffold > SafeArea > Column
Row(
  children: [
    Expanded(
      child: Text.rich(
        TextSpan(
          children: [
            TextSpan(
              text: '반가워요.',
              style: textTheme.titleMedium?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            TextSpan(
              text: '건강을 위한 한 걸음\n',
              style: textTheme.titleMedium?.copyWith(
                fontWeight: FontWeight.bold,
                color: Colors.blue,
              ),
            ),
            TextSpan(
              text: '오늘도 힘차게 운동을 해 볼까요?\n',
              style: textTheme.bodyMedium,
            ),
            TextSpan(
              text: '> 내 프로필',
              style: textTheme.bodySmall?.copyWith(
                color: Colors.grey,
              ),
            ),
          ],
        ),
      ),
    ),
    Stack(
      children: [
        Image.asset('assets/home/half_circle.png', width: 132),
          Positioned(
            left: 16,
            child: Container(
            width: 80,
            height: 80,
            decoration: BoxDecoration(
              image: DecorationImage(
                image: AssetImage('assets/home/me.png'),
                fit: BoxFit.cover,
              ),
              shape: BoxShape.circle,
              border: Border.all(color: Colors.blue, width: 1),
            ),
          ),
        ),
      ],
    ),
  ],
),

part 03: 운동 요약

여기서부터 세 개 영역은 Expanded widget과 flex property를 이용하여 3:2:2 비율로 세로 화면을 가득 채우도록 작성할 것이다.

그 중에서도 운동 요약 영역은 오늘의 운동과 주간 운동이 가로로 반씩 차지하고 있는 영역이다.

대시보드 화면에 들어가는 모든 카드는DashboardCard 라는 이름의 custom widget을 만들어 통일감을 줄 것이다.

lib/dashboard_card.dart

import 'package:flutter/material.dart';

class DashboardCard extends StatelessWidget {

  const DashboardCard({super.key});

  
  Widget build(BuildContext context) {
    ColorScheme colorScheme = Theme.of(context).colorScheme;

    return Container(
      margin: EdgeInsets.all(8),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: colorScheme.outlineVariant),
      ),
      child: Placeholder(),
    );
  }
}

lib/workout_home_page.dart > WorkoutHomePage

// Scaffold > SafeArea > Column
 Expanded(
  flex: 3,
  child: Row(
    children: [
      Expanded(
        child: DashboardCard(),
      ),
      Expanded(
        child: DashboardCard(),
      ),
    ],
  ),
),

part 04: 운동 추천

운동 추천 영역은 가로 스크롤 영역에 추천하는 운동 카드를 순차적으로 배치하는 영역이다.

SingleChildScrollView widget 을 통해 스크롤 가능하게 하고 스크롤 방향을 가로로 변경한다. 그 안에 앞서 만든 custom widget인 DashboardCard 를 넣는다.

lib/workout_home_page.dart > WorkoutHomePage

// Scaffold > SafeArea > Column
Expanded(
  flex: 2,
  child: SingleChildScrollView(
    scrollDirection: Axis.horizontal,
    child: Row(
    children: [
      AspectRatio(
        aspectRatio: 1 / 1,
        child: DashboardCard(),
      ),
      AspectRatio(
        aspectRatio: 1 / 1,
        child: DashboardCard(),
      ),
      AspectRatio(
        aspectRatio: 1 / 1,
        child: DashboardCard(),
      ),
    ],
  ),
),

수업에서와 다르게 한 점

수업에서는 DashboardCardSizedBox 로 감싸 width 값을 강제하였다. 화면 크기에 따라 종횡비가 달라질 것이다.
나는 DashboardCardAspectRatio 로 감싸 종횡비를 1:1로 강제하였다. 화면 크기에 따라 가로 길이가 달라질 것이다.

part 05: 이어서 하기

이어서 하기 영역은 가로로 가득 채우는 영역이다.

여기에는 앞서 만든 custom widget인 DashboardCard 만으로 가득 채운다.

lib/workout_home_page.dart > WorkoutHomePage

// Scaffold > SafeArea > Column
Expanded(
  flex: 2,
  child: DashboardCard(),
),

세부 구현하기

둥근 사각형의 테두리에 쌓여 있고 우상단에 아이콘과 텍스트 라벨이 있는 것까지가 대시보드 카드들의 공통 사항이다. 구체적인 아이콘과 텍스트, 그리고 그 스타일에는 차이가 있으며, 그 아래로 이어지는 내용도 제각각이다.

따라서 공통 사항을 제외하고는 property로 받아서 사용한다. 우상단에 출력할 Icon, Text 그리고 그 아래로 이어질 Widget 이 필요하다.

lib/dashboard_card.dart

import 'package:flutter/material.dart';

class DashboardCard extends StatelessWidget {
  final Icon _labelIcon;
  final Text _labelText;
  final Widget _info;

  const DashboardCard({
    super.key,
    required this._labelIcon,
    required this._labelText,
    required this._info,
  });

  
  Widget build(BuildContext context) {
    ColorScheme colorScheme = Theme.of(context).colorScheme;

    return Container(
      margin: EdgeInsets.all(8),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(10),
        border: Border.all(color: colorScheme.outlineVariant),
      ),
      child: Column(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [_labelIcon, SizedBox(width: 4), _labelText],
          ),
          Expanded(child: _info),
        ],
      ),
    );
  }
}

part 03: 운동 요약

그래프 부분은 추후에 하기로 하고 텍스트 위주로 배치한다. 오늘의 운동과 월간 운동의 기본 형식은 비슷하니 복사해서 조금만 수정해 주면 된다.

part 04: 운동 추천

좌측에 이미지, 우측에 텍스트가 들어가고 배경에 그라데이션이 들어가는 형태다. 이를 위해 DashboardCard widget에 선택 사항인 _color property를 추가하고, 이 값이 전달되면 해당 색상으로 흘러가는 그라데이션을 깔도록 하겠다.

part 05: 이어서 하기

배경 이미지를 깔고 텍스트를 올리는 형태다. DashboardCarddecoration property를 통해 배경 이미지를 깔아도 되겠지만 이 widget을 최대한 단순하게 유지하기 위해 이건 Stack 으로 쌓았다.

좀 더 꾸며주기

Stack 을 사용하여 그래프를 추가한다. CircularProgressIndicator widget 에 백분율 값을 넣어 원형 그래프를 만들 수 있다.

소모 칼로리 부분에는 지금은 숫자가 하드코딩되어 있어서 천 단위로 콤마를 찍어 주었지만 실제 데이터가 들어오면 자동으로 되지 않고 따로 콤마를 찍어 주어야 한다. intl package를 사용하면 이를 자동으로 설정되게 할 수 있다.

숫자 데이터를 변수로 빼서 이를 구현해 보았다.

그리고 적절한 여백을 주어 화면을 개선해 보면,


>>> GitHub Repository at this point (e577257)

profile
Peter J Online Space - since July 2020 | 아무데서나 채용해줬으면 좋겠다 (지금은 학생 때 하던 거 아무거나 공부하고 있고요, 취업시켜 주시면 그 분야로 공부할게요)

0개의 댓글