Menu
CoddyTech
flag Ar iconالعربيةdown icon

متتبع العادات: سلسلة الالتزام

جزء من قسم المشاريع في رحلة Flutter على Coddy. الدرس 32 من 36.

السلاسل المتتالية تجعل الناس يعودون: فالأيام المتتالية تبدو شيئًا يستحق الحفاظ عليه. هنا، السلسلة المتتالية هي أطول مجموعة من الأيام المنجزة خلال الأسبوع. شرب الماء، منجز من الاثنين إلى الأربعاء ويوم الجمعة، له سلسلة متتالية من 3 أيام.

يتطلب حساب سلسلة متتالية المرور على الأيام بالترتيب باستخدام رقمين: run، عدد الأيام المتتالية حتى الآن، وbest، أطول سلسلة متتالية تم رصدها. يضيف اليوم المنجز يومًا إلى السلسلة؛ أما اليوم المفتوح فيعيدها إلى 0.

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;
}

أعِد best، لا run: ففي نهاية الحلقة، لا تمثل run سوى السلسلة الأخيرة، وتكون 0 إذا كان يوم الأحد مفتوحًا.

اعرضها أسفل الأسبوع مع أيقونة لهب، باللون البرتقالي ما دامت هناك سلسلة متتالية، وباللون الرمادي الخاص بـoutline في السمة عند عدم وجودها. تبدو عبارة «سلسلة متتالية من 0 يوم» غريبة، لذا قل «لا توجد سلسلة متتالية بعد» حينها.

challenge icon

التحدي

صعب

اعرض سلاسل الاستمرارية:

  • أضف المُحصِّل streak إلى Habit
  • أسفل الأسبوع، وبعد 12 بكسل، أضف Row: ‏Icons.local_fire_department بحجم 20 (باللون البرتقالي عندما تكون سلسلة الاستمرارية أكبر من 0، وإلا بلون outline الخاص بالسمة)، و4 بكسلات، ثم النص
  • النص هو 3 day streak (مع الرقم الفعلي)، أو No streak yet عندما تكون القيمة 0

جرّب بنفسك

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;
}

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),
                  ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

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,
          ),
        ),
      ),
    );
  }
}
quiz iconاختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس المشاريع

تدرّب بنفسك: مترجم Flutter عبر الإنترنت