Rastreador de hábitos: Anillo de progreso
Parte de la sección Proyectos del Journey de Flutter de Coddy. Lección 35 de 36.
¿Cómo va la semana en general? Un anillo en la parte superior responde de un vistazo: se va llenando a medida que marcas casillas, con el porcentaje en el centro. Es un CircularProgressIndicator con un value, el equivalente circular de la barra de progreso del cuestionario.
Los números provienen de todos los hábitos: las casillas marcadas, de un total de siete por hábito. Un getter los suma con fold, como hacía el total de la tienda:
int get _ticks => _habits.fold(0, (sum, habit) => sum + habit.done);
int get _possible => _habits.length * 7;El anillo y el porcentaje se superponen, así que van en un Stack con fit: StackFit.expand dentro de un SizedBox de 72 por 72. El anillo llena el cuadro y un Center coloca el texto en el centro.
Si se eliminan todos los hábitos, _possible es 0, y 0 / 0 no es un número. En ese caso, muestra 0: possible == 0 ? 0.0 : ticks / possible. Para la etiqueta, (progress * 100).round() da un porcentaje entero.
Desafío
DifícilMuestra el progreso de la semana:
- añade
_ticksy_possibleal estado de la pantalla - escribe un
ProgressCardque recibaticksypossible: unCardconmargin: EdgeInsets.zeroen elprimaryContainerdel tema, con 20 píxeles de relleno alrededor de unRow - la fila: el anillo de 72 píxeles (
strokeWidth: 8,strokeCap: StrokeCap.round,backgroundColor: colors.surface) con33%en negrita ytitleMediumen el centro, 20 píxeles y después unaColumnExpandedconThis weekentitleLarge, 4 píxeles y7 of 21 ticks - haz que sea el primer elemento secundario de la lista
Pruébalo tú mismo
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];
});
}
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: [
for (Habit habit in _habits)
HabitCard(
habit: habit,
onToggle: (day) => _toggle(habit, day),
onLongPress: () => _deleteHabit(habit),
),
],
),
);
}
}
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,
),
),
),
);
}
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Proyectos
1Proyecto: Lista de tareas
Lista de tareas: La lista de tareasLista de tareas: Añadir una tarea4Proyecto: Rastreador de hábitos
Rastreador de hábitos: Tus hábitosRastreador de hábitos: La semana2Proyecto: Cuestionario
Cuestionario: La preguntaCuestionario: Correcto o incorrectoPractica por tu cuenta: Compilador de Flutter online