
애플리케이션의 홈 화면을 구성하고, 작성한 widget을 custom widget으로 빼서 관리해 보자.
먼저 lib/workout_home_page.dart 파일을 만들고 StatefulWidget 으로 WorkoutHomePage widget을 생성한 뒤, 필요한 widget을 순차적으로 작성한다.
우선은 여백을 고려하지 않고 전체적인 틀을 작성한 뒤, 이후에 여백을 조정하도록 하겠다.
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), ], ),
프로필 부분은 텍스트와 이미지로 구성된다. 텍스트 부분에는 여러 가지 스타일이 섞여 사용되기 때문에 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), ), ), ), ], ), ], ),
여기서부터 세 개 영역은 Expanded widget과 flex property를 이용하여 3:2:2 비율로 세로 화면을 가득 채우도록 작성할 것이다.
그 중에서도 운동 요약 영역은 오늘의 운동과 주간 운동이 가로로 반씩 차지하고 있는 영역이다.
대시보드 화면에 들어가는 모든 카드는DashboardCard 라는 이름의 custom widget을 만들어 통일감을 줄 것이다.
lib/dashboard_card.dartimport '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(), ), ], ), ),
운동 추천 영역은 가로 스크롤 영역에 추천하는 운동 카드를 순차적으로 배치하는 영역이다.
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(), ), ], ), ),
수업에서와 다르게 한 점
수업에서는
DashboardCard를SizedBox로 감싸width값을 강제하였다. 화면 크기에 따라 종횡비가 달라질 것이다.
나는DashboardCard를AspectRatio로 감싸 종횡비를 1:1로 강제하였다. 화면 크기에 따라 가로 길이가 달라질 것이다.
이어서 하기 영역은 가로로 가득 채우는 영역이다.
여기에는 앞서 만든 custom widget인 DashboardCard 만으로 가득 채운다.
lib/workout_home_page.dart>WorkoutHomePage// Scaffold > SafeArea > Column Expanded( flex: 2, child: DashboardCard(), ),
둥근 사각형의 테두리에 쌓여 있고 우상단에 아이콘과 텍스트 라벨이 있는 것까지가 대시보드 카드들의 공통 사항이다. 구체적인 아이콘과 텍스트, 그리고 그 스타일에는 차이가 있으며, 그 아래로 이어지는 내용도 제각각이다.
따라서 공통 사항을 제외하고는 property로 받아서 사용한다. 우상단에 출력할 Icon, Text 그리고 그 아래로 이어질 Widget 이 필요하다.
lib/dashboard_card.dartimport '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), ], ), ); } }
그래프 부분은 추후에 하기로 하고 텍스트 위주로 배치한다. 오늘의 운동과 월간 운동의 기본 형식은 비슷하니 복사해서 조금만 수정해 주면 된다.
좌측에 이미지, 우측에 텍스트가 들어가고 배경에 그라데이션이 들어가는 형태다. 이를 위해 DashboardCard widget에 선택 사항인 _color property를 추가하고, 이 값이 전달되면 해당 색상으로 흘러가는 그라데이션을 깔도록 하겠다.

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

Stack 을 사용하여 그래프를 추가한다. CircularProgressIndicator widget 에 백분율 값을 넣어 원형 그래프를 만들 수 있다.
소모 칼로리 부분에는 지금은 숫자가 하드코딩되어 있어서 천 단위로 콤마를 찍어 주었지만 실제 데이터가 들어오면 자동으로 되지 않고 따로 콤마를 찍어 주어야 한다. intl package를 사용하면 이를 자동으로 설정되게 할 수 있다.
숫자 데이터를 변수로 빼서 이를 구현해 보았다.
그리고 적절한 여백을 주어 화면을 개선해 보면,