습관 추적기: 습관 추가하기
Coddy Flutter 여정의 프로젝트 섹션에 포함된 레슨. 36개 중 33번째.
사람마다 기록하는 습관이 다르므로 앱에는 습관을 추가하는 방법이 필요합니다. 모서리에 있는 둥근 FloatingActionButton을 누르면 이름을 묻는 대화상자가 열립니다.
showDialog는 대화상자가 반환하는 값을 담은 Future를 반환합니다. 여기서 대화상자는 추가를 누르면 true를, 취소를 누르면 false를 반환하고, 입력란은 onChanged를 사용해 입력한 내용을 변수에 저장합니다.
String name = '';
final added = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('New habit'),
content: TextField(onChanged: (value) => name = value, ...),
actions: [...],
),
);
if (added != true || name.trim().isEmpty) return;added는 bool?입니다. 대화상자 바깥을 누르면 null을 반환하며 닫힙니다. added != true는 취소한 경우와 이 경우를 한 번에 처리합니다.
새 습관은 새 한 주로 시작합니다. List.filled(7, false)를 사용합니다. const [false, ...]는 사용하지 마세요. const 목록은 절대 변경할 수 없으므로 첫 번째 체크 표시를 누르면 앱이 중단됩니다.
챌린지
어려움습관 추가:
_addHabit을 호출하는Icons.add가 있는FloatingActionButton을Scaffold에 추가하세요Future<void> _addHabit() async를 작성하세요. 제목이New habit이고autofocus: true가 설정된TextField(레이블은Habit name),CancelTextButton,AddFilledButton이 있는AlertDialog를 표시하세요- 이름이 비어 있지 않은 상태로
Add를 누른 후, 앞뒤 공백을 제거한 이름과 비어 있는 한 주로 습관을 추가하세요
직접 해보기
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
),
home: const HabitScreen(),
);
}
}
const List<String> dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
class Habit {
Habit(this.name, this.days);
final String name;
final List<bool> days;
int get done => days.where((day) => day).length;
int get streak {
int best = 0;
int run = 0;
for (bool day in days) {
if (day) {
run++;
if (run > best) best = run;
} else {
run = 0;
}
}
return best;
}
}
class HabitScreen extends StatefulWidget {
const HabitScreen({super.key});
@override
State<HabitScreen> createState() => _HabitScreenState();
}
class _HabitScreenState extends State<HabitScreen> {
final List<Habit> _habits = [
Habit('Drink water', [true, true, true, false, true, false, false]),
Habit('Read 10 pages', [true, false, true, true, false, false, false]),
Habit('Stretch', List.filled(7, false)),
];
void _toggle(Habit habit, int day) {
setState(() {
habit.days[day] = !habit.days[day];
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Habits')),
body: ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 88),
children: [
for (Habit habit in _habits)
HabitCard(
habit: habit,
onToggle: (day) => _toggle(habit, day),
),
],
),
);
}
}
class HabitCard extends StatelessWidget {
const HabitCard({super.key, required this.habit, required this.onToggle});
final Habit habit;
final void Function(int day) onToggle;
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Card(
margin: const EdgeInsets.only(top: 12),
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(child: Text(habit.name, style: textTheme.titleMedium)),
Text('${habit.done} of 7 days', style: textTheme.bodySmall),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
for (int i = 0; i < 7; i++)
DayCircle(
label: dayNames[i],
done: habit.days[i],
onTap: () => onToggle(i),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Icon(
Icons.local_fire_department,
size: 20,
color: habit.streak > 0 ? Colors.orange : Theme.of(context).colorScheme.outline,
),
const SizedBox(width: 4),
Text(habit.streak == 0 ? 'No streak yet' : '${habit.streak} day streak'),
],
),
],
),
),
);
}
}
class DayCircle extends StatelessWidget {
const DayCircle({super.key, required this.label, required this.done, required this.onTap});
final String label;
final bool done;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
child: Container(
width: 40,
height: 40,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: done ? colors.primary : colors.surfaceContainerHighest,
),
child: Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: done ? colors.onPrimary : colors.onSurfaceVariant,
),
),
),
);
}
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.
프로젝트의 모든 레슨
직접 연습해 보세요: 온라인 Flutter 컴파일러