Alışkanlık Takipçisi: Açık ve Koyu Tema
Coddy'nin Flutter Journey'sinin Projeler bölümünün bir parçası. Ders 36 / 36.
İnsanlar alışkanlıklarını gece geç saatlerde kontrol eder; bu yüzden son özellik, üst çubukta bulunan bir düğmeyle açılıp kapatılan koyu temadır: açmak için ay, tekrar kapatmak için güneş.
Tema MaterialApp içinde bulunduğundan, anahtar da oradadır: MyApp, bool _dark içeren bir StatefulWidget'a dönüşür. MaterialApp her iki temayı da alır ve themeMode ile birini seçer:
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.green,
brightness: Brightness.dark,
),
),
themeMode: _dark ? ThemeMode.dark : ThemeMode.light,Düğme, MyApp'in çok aşağısındaki alışkanlık ekranındadır. Durumu bir kez daha yukarı taşı: HabitScreen, simgesini seçmek için dark değerini, çağırmak için de bir onToggleTheme geri çağırımını alır. Ekran bunları bileşeninden widget.dark ve widget.onToggleTheme olarak okur.
Uygulamadaki her renk temadan geldiği için kartlar, daireler ve halka da temayla birlikte değişir. Alışkanlıkların işaretli kalır; çünkü bunlar temada değil, ekranın durumunda tutulur.
Bu, kurstaki projelerin son dersidir. Denetimler tüm alışkanlık takipçisini kapsar: ekleme, işaretleme, seri, silme, halka ve koyu tema.
Görev
ZorKoyu modu ekle:
MyApp'ibool _dark = falseile birStatefulWidgetyap,MaterialApp'e aynı yeşil tohumdanBrightness.darkiçeren birdarkThemever vethemeMode'u_darkdeğerine göre ayarlaHabitScreen'erequiredbirbool darkveVoidCallback onToggleThemever;MyApp,_darkdeğerini ve bu değerisetStateiçinde tersine çeviren bir işlevi aktarır- üst çubuğun
actionsbölümünde birIconButton, açık moddaIcons.dark_mode, koyu moddaIcons.light_modegösterir vewidget.onToggleThemeişlevini çağırır
Kendin dene
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)),
];
int get _ticks => _habits.fold(0, (sum, habit) => sum + habit.done);
int get _possible => _habits.length * 7;
void _toggle(Habit habit, int day) {
setState(() {
habit.days[day] = !habit.days[day];
});
}
Future<void> _addHabit() async {
String name = '';
final added = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('New habit'),
content: TextField(
autofocus: true,
decoration: const InputDecoration(labelText: 'Habit name'),
onChanged: (value) => name = value,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Add'),
),
],
),
);
if (added != true || name.trim().isEmpty) return;
setState(() {
_habits.add(Habit(name.trim(), List.filled(7, false)));
});
}
Future<void> _deleteHabit(Habit habit) async {
final delete = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Delete ${habit.name}?'),
content: const Text('Its ticks for this week go with it.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Delete'),
),
],
),
);
if (delete != true) return;
setState(() {
_habits.remove(habit);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Habits')),
floatingActionButton: FloatingActionButton(
onPressed: _addHabit,
child: const Icon(Icons.add),
),
body: ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 88),
children: [
ProgressCard(ticks: _ticks, possible: _possible),
for (Habit habit in _habits)
HabitCard(
habit: habit,
onToggle: (day) => _toggle(habit, day),
onLongPress: () => _deleteHabit(habit),
),
],
),
);
}
}
class ProgressCard extends StatelessWidget {
const ProgressCard({super.key, required this.ticks, required this.possible});
final int ticks;
final int possible;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final progress = possible == 0 ? 0.0 : ticks / possible;
return Card(
margin: EdgeInsets.zero,
color: colors.primaryContainer,
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
SizedBox(
width: 72,
height: 72,
child: Stack(
fit: StackFit.expand,
children: [
CircularProgressIndicator(
value: progress,
strokeWidth: 8,
strokeCap: StrokeCap.round,
backgroundColor: colors.surface,
),
Center(
child: Text(
'${(progress * 100).round()}%',
style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
],
),
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('This week', style: textTheme.titleLarge),
const SizedBox(height: 4),
Text('$ticks of $possible ticks'),
],
),
),
],
),
),
);
}
}
class HabitCard extends StatelessWidget {
const HabitCard({
super.key,
required this.habit,
required this.onToggle,
required this.onLongPress,
});
final Habit habit;
final void Function(int day) onToggle;
final VoidCallback onLongPress;
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Card(
margin: const EdgeInsets.only(top: 12),
clipBehavior: Clip.antiAlias,
child: InkWell(
onLongPress: onLongPress,
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,
),
),
),
);
}
}
Bu ders kısa bir quiz içerir. Soruları yanıtlamak ve ilerlemeni kaydetmek için derse başla.
Projeler bölümündeki tüm dersler
1Proje: Yapılacaklar Listesi
Yapılacaklar Listesi: Görev ListesiYapılacaklar Listesi: Görev Ekleme4Proje: Alışkanlık Takipçisi
Alışkanlık Takipçisi: AlışkanlıklarınAlışkanlık Takipçisi: HaftaKendi başına pratik yap: Online Flutter derleyicisi