Merge NDM-App-221-fix-widget-api-mismatches-build-errors-after-merge into dev

This commit is contained in:
TrochtaOndrej
2026-03-18 11:32:06 +01:00
12 changed files with 254 additions and 52 deletions
+6
View File
@@ -63,6 +63,12 @@ assets/
API base URL: `http://localhost:8090/api/v1/`
## Widget API Patterns
- **Dual-param widgets**: `shared/widgets/` accept both entity param (e.g. `task: TaskEntity?`) AND individual raw params — private getters resolve which to use. Keep backward compat when extending.
- **`PetType` canonical location**: `domain/entities/pet_type.dart` — never redeclare locally.
- **`todayTasksProvider`** is a top-level alias for `tasksProvider` (no date filter in MVP).
- **Pet identity vs. pet state**: `petType`/`petName` belong to `userProvider`; `petStateProvider` tracks energy/streak/rewards only.
## Naming Conventions
- **Files**: `snake_case.dart`
- **Classes**: `PascalCase`
+126
View File
@@ -0,0 +1,126 @@
# Widget API Mismatches — Fix
**Branch:** `NDM-App-221-fix-widget-api-mismatches-build-errors-after-merge`
**Commits:** `371e826`, `adc2f72`
## Overview
After multiple feature branches were merged independently, the shared widgets and domain entities had diverged APIs causing 8 compilation errors. This fix aligns widget constructors with how `TasksScreen` and `ZenScreen` use them, adds missing entity properties, and resolves provider reference errors.
---
## Problems Fixed
| # | Error | Location |
|---|-------|----------|
| 1 | `EnergyBar` has no param `energyLevel` | `TasksScreen:65` |
| 2 | `PetBubble` missing required param `petName` | `TasksScreen:69` |
| 3 | `StreakDots` has no param `streakDays` | `TasksScreen:77` |
| 4 | `TaskCard` has no param `task` | `TasksScreen:118,132` |
| 5 | `todayTasksProvider` not defined | `TasksScreen:24` |
| 6 | `PetStateEntity` missing `petType` getter | `TasksScreen:35` |
| 7 | `EnergyBadge` has no param `energyLevel` | `ZenScreen:132` |
| 8 | `PetStateEntity` constructor missing `updatedAt` | `pet_providers.dart:43` |
---
## Files Changed
### `lib/shared/widgets/energy_bar.dart`
Added optional `energyLevel: int?` parameter alongside the existing `fraction: double?`.
Both are now nullable; an `assert` ensures exactly one is provided.
Private getter `_effectiveFraction` converts `energyLevel` → fraction via `(level / 3.0).clamp(0.0, 1.0)`.
```dart
EnergyBar(energyLevel: petState.energyToday) // new usage
EnergyBar(fraction: 0.66) // existing usage still works
```
### `lib/shared/widgets/energy_badge.dart`
Added optional `energyLevel: int?` parameter alongside the existing `level: EnergyLevel?`.
Private getter `_effectiveLevel` falls back to `EnergyLevel.fromInt(energyLevel ?? 1)`.
Also added `EnergyLevel.fromInt()` static factory (maps 1→low, 2→medium, 3→high).
```dart
EnergyBadge(energyLevel: task.energyLevel) // new usage
EnergyBadge(level: EnergyLevel.medium) // existing usage still works
EnergyBadge.fromInt(2) // factory convenience
```
### `lib/shared/widgets/streak_dots.dart`
Added optional `streakDays: int?` parameter.
`_generateDays(int streak)` computes a `List<DayStatus>` from today's weekday index and streak count.
Both `days` and `streakCount` are now optional (default to empty/0 when `streakDays` is provided).
```dart
StreakDots(streakDays: petState.streakDays) // new
StreakDots(days: myList, streakCount: 3) // existing
```
### `lib/shared/widgets/task_card.dart`
Added optional `task: TaskEntity?` and `onComplete: VoidCallback?` parameters.
Private getters (`_title`, `_energyLevel`, `_isDone`, `_isSelfCare`, `_isOverdue`, `_toggleCallback`) delegate to `task` fields when set, falling back to individual params.
`_isOverdue` computes from `task.deadline` when a task is provided.
```dart
TaskCard(task: entity, onComplete: () => ref.read(...).completeTask(id)) // new
TaskCard(title: 'Buy milk', energyLevel: 1, isDone: false) // existing
```
### `lib/shared/widgets/pet_bubble.dart`
Removed local `PetType` enum (was duplicated here and in `domain/entities/pet_type.dart`).
Now imports `PetType` from the domain layer — single source of truth.
### `lib/domain/entities/task_entity.dart`
Added computed getter:
```dart
bool get isCompleted => status == TaskStatus.done;
```
Widgets and screens use this instead of manually comparing `status == TaskStatus.done`.
### `lib/features/tasks/providers/task_providers.dart`
Added `uncompleteTask(int id)` method to `TasksNotifier` — sets status back to `pending`.
Added top-level alias:
```dart
final todayTasksProvider = tasksProvider;
```
MVP does not filter by date; screens watch `todayTasksProvider` and split pending/completed themselves.
### `lib/features/tasks/providers/pet_providers.dart`
Fixed `PetStateEntity` constructor: `id`/`userId` are `String` (`'1'`), `updatedAt` is required.
Added `onTaskCompleted()` method as a public alias for `incrementTasksDone()`.
### `lib/features/tasks/screens/tasks_screen.dart`
- Watches `userProvider` (for `petType`, `petName`) and `petMoodProvider` (for mood string).
- `PetBubble` now receives `petName` from `userState` and `mood` from `petMoodProvider`.
- `_EmptyState` now accepts `PetType` (enum) instead of `String`, uses `petType.emoji`.
- Pet type resolved from `userState.petType`, not `petState.petType` (pet state doesn't own identity).
---
## Key Decisions
**Dual-param widget pattern**: Widgets accept both an entity param (`task: TaskEntity?`) and individual params (`title`, `energyLevel`, etc.). Private getters resolve which to use. This keeps backward compatibility for any callers passing raw values while enabling entity-driven usage from screens.
**`PetType` canonical location**: `domain/entities/pet_type.dart` is the single source. `PetBubble` used to redeclare it locally — that local copy is removed.
**`todayTasksProvider` is an alias**: Not a filtered provider. In MVP all tasks are "today" tasks. Screens handle the pending/completed split via `.where()`. If date-filtering is needed later, only this alias needs to change.
**Pet identity vs. pet state**: `petType` and `petName` belong to the `User` entity (`userProvider`), not `PetStateEntity`. `PetStateEntity` tracks energy/streak/rewards only.
---
## Tests Added
83 new tests across 6 files:
| File | Tests |
|------|-------|
| `test/domain/entities/task_entity_test.dart` | 12 |
| `test/domain/entities/pet_type_test.dart` | 12 |
| `test/domain/entities/pet_state_entity_test.dart` | 8 |
| `test/features/tasks/providers/task_providers_test.dart` | 16 |
| `test/features/tasks/providers/pet_providers_test.dart` | 24 |
| `test/shared/widgets/energy_badge_test.dart` | 13 |
All 83 new tests pass. 2 pre-existing failures in `test/data/repositories/` require `build_runner` regeneration and are unrelated to this fix.
+2
View File
@@ -28,6 +28,8 @@ class TaskEntity {
required this.updatedAt,
});
bool get isCompleted => status == TaskStatus.done;
TaskEntity copyWith({
int? id,
int? userId,
@@ -40,18 +40,21 @@ final userProvider =
class PetStateNotifier extends Notifier<PetStateEntity> {
@override
PetStateEntity build() => const PetStateEntity(
id: 1,
userId: 1,
PetStateEntity build() => PetStateEntity(
id: '1',
userId: '1',
energyToday: 2,
streakDays: 3,
totalTasksDone: 12,
unlockedItems: 'masle,pelisek',
updatedAt: DateTime.now(),
);
void incrementTasksDone() {
state = state.copyWith(totalTasksDone: state.totalTasksDone + 1);
}
void onTaskCompleted() => incrementTasksDone();
}
final petStateProvider =
@@ -21,6 +21,19 @@ class TasksNotifier extends Notifier<List<TaskEntity>> {
];
}
void uncompleteTask(int id) {
state = [
for (final task in state)
if (task.id == id)
task.copyWith(
status: TaskStatus.pending,
updatedAt: DateTime.now(),
)
else
task,
];
}
void skipTask(int id) {
final skipped = state.firstWhere((t) => t.id == id);
state = [
@@ -34,6 +47,10 @@ class TasksNotifier extends Notifier<List<TaskEntity>> {
final tasksProvider =
NotifierProvider<TasksNotifier, List<TaskEntity>>(TasksNotifier.new);
/// Alias for [tasksProvider]. Returns all tasks — no date filtering in MVP.
/// Screens are responsible for splitting into pending/completed.
final todayTasksProvider = tasksProvider;
/// Pending tasks only, ordered by sort order.
final pendingTasksProvider = Provider<List<TaskEntity>>((ref) {
final tasks = ref.watch(tasksProvider);
@@ -1,4 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// Provider for current user name.
final userNameProvider = Provider<String>((ref) => 'Zuf');
import 'pet_providers.dart';
/// Derived provider for the current user's display name.
final userNameProvider = Provider<String>((ref) => ref.watch(userProvider).name);
+12 -13
View File
@@ -6,6 +6,7 @@ import 'package:google_fonts/google_fonts.dart';
import '../../../core/router/routes.dart';
import '../../../core/theme/kittie_colors.dart';
import '../../../domain/entities/pet_type.dart';
import '../../../shared/utils/czech_date.dart';
import '../../../shared/widgets/energy_bar.dart';
import '../../../shared/widgets/pet_bubble.dart';
@@ -23,6 +24,8 @@ class TasksScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final tasks = ref.watch(todayTasksProvider);
final petState = ref.watch(petStateProvider);
final userState = ref.watch(userProvider);
final petMood = ref.watch(petMoodProvider);
final userName = ref.watch(userNameProvider);
final pendingTasks = tasks.where((t) => !t.isCompleted).toList();
@@ -32,7 +35,7 @@ class TasksScreen extends ConsumerWidget {
return Scaffold(
body: SafeArea(
child: tasks.isEmpty
? _EmptyState(petType: petState.petType)
? _EmptyState(petType: userState.petType)
: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 100),
child: Column(
@@ -67,9 +70,10 @@ class TasksScreen extends ConsumerWidget {
// 4. Pet area
PetBubble(
petType: petState.petType,
mood: petState.mood,
message: _petMessage(petState.mood, petState.petName),
petType: userState.petType,
petName: userState.petName,
mood: petMood.name,
message: _petMessage(petMood.name, userState.petName),
),
const SizedBox(height: 16),
@@ -154,26 +158,21 @@ class TasksScreen extends ConsumerWidget {
static String _petMessage(String mood, String petName) {
return switch (mood) {
'happy' => '$petName je stastna! Jde ti to skvele \u{1F44F}',
'neutral' => '$petName te ceka! Co dnes zvladneme? \u{2728}',
'sad' => '$petName te podporuje. Zkus jeden ukol \u{1F495}',
'sleepy' => '$petName odpociva... Klid je taky dulezity \u{1F4A4}',
_ => '$petName te ceka! Co dnes zvladneme? \u{2728}',
_ => '$petName je tady pro tebe \u{1F60A}',
};
}
}
class _EmptyState extends StatelessWidget {
final String petType;
final PetType petType;
const _EmptyState({required this.petType});
@override
Widget build(BuildContext context) {
final emoji = switch (petType) {
'dog' => '\u{1F436}',
'cat' => '\u{1F431}',
'capybara' => '\u{1F9AB}',
_ => '\u{1F431}',
};
final emoji = petType.emoji;
return Center(
child: Column(
+11 -3
View File
@@ -27,14 +27,22 @@ enum EnergyLevel {
/// - medium: orange bg, orange text, sun icon — "Stredni"
/// - high: red bg, red text, fire icon — "Narocne"
class EnergyBadge extends StatelessWidget {
const EnergyBadge({super.key, required this.level});
const EnergyBadge({super.key, this.level, this.energyLevel})
: assert(level != null || energyLevel != null,
'Provide either level or energyLevel');
final EnergyLevel level;
final EnergyLevel? level;
/// Energy level as raw int (13); used when [level] is not provided.
final int? energyLevel;
/// Convenience constructor from raw int (1/2/3).
factory EnergyBadge.fromInt(int level) =>
EnergyBadge(level: EnergyLevel.fromInt(level));
EnergyLevel get _effectiveLevel =>
level ?? EnergyLevel.fromInt(energyLevel ?? 1);
@override
Widget build(BuildContext context) {
final (bgColor, textColor, icon, label) = _props;
@@ -61,7 +69,7 @@ class EnergyBadge extends StatelessWidget {
}
(Color bg, Color text, IconData icon, String label) get _props =>
switch (level) {
switch (_effectiveLevel) {
EnergyLevel.low => (
KittieColors.greenLight,
KittieColors.greenDark,
+15 -4
View File
@@ -8,19 +8,30 @@ import '../../core/theme/kittie_typography.dart';
///
/// Shows a track with creamDark background and amber fill
/// that animates to the current [fraction] (0.01.0).
/// Alternatively, pass [energyLevel] (13) to compute the fraction.
class EnergyBar extends StatelessWidget {
const EnergyBar({
super.key,
required this.fraction,
this.fraction,
this.energyLevel,
this.label = 'ENERGIE',
}) : assert(fraction >= 0.0 && fraction <= 1.0);
}) : assert(fraction != null || energyLevel != null,
'Provide either fraction or energyLevel');
/// Fill amount from 0.0 (empty) to 1.0 (full).
final double fraction;
final double? fraction;
/// Energy level 13; used when [fraction] is not provided.
final int? energyLevel;
/// Label displayed above the bar.
final String label;
double get _effectiveFraction {
if (energyLevel != null) return (energyLevel! / 3.0).clamp(0.0, 1.0);
return (fraction ?? 0.0).clamp(0.0, 1.0);
}
@override
Widget build(BuildContext context) {
return Column(
@@ -52,7 +63,7 @@ class EnergyBar extends StatelessWidget {
AnimatedContainer(
duration: AppConstants.animationNormal,
curve: Curves.easeInOut,
width: constraints.maxWidth * fraction,
width: constraints.maxWidth * _effectiveFraction,
color: KittieColors.amber,
),
],
+1 -10
View File
@@ -3,16 +3,7 @@ import 'package:flutter/material.dart';
import '../../core/constants/app_constants.dart';
import '../../core/theme/kittie_colors.dart';
import '../../core/theme/kittie_typography.dart';
/// Pet type determines the displayed emoji.
enum PetType {
dog('\u{1F436}'),
cat('\u{1F431}'),
capybara('\u{1F9AB}');
const PetType(this.emoji);
final String emoji;
}
import '../../domain/entities/pet_type.dart';
/// Pet area card with a speech bubble.
///
+28 -7
View File
@@ -17,18 +17,39 @@ enum DayStatus { done, today, future }
class StreakDots extends StatelessWidget {
const StreakDots({
super.key,
required this.days,
required this.streakCount,
}) : assert(days.length == 7, 'days must have exactly 7 entries');
this.days,
this.streakCount = 0,
this.streakDays,
});
/// Status for each day MonSun.
final List<DayStatus> days;
/// Status for each day MonSun. Computed from [streakDays] if omitted.
final List<DayStatus>? days;
/// Current consecutive-day streak count.
final int streakCount;
/// Streak as an integer; used to compute [days] and count when provided.
final int? streakDays;
static const _dayLabels = ['Po', 'Ut', 'St', 'Ct', 'Pa', 'So', 'Ne'];
static List<DayStatus> _generateDays(int streak) {
final todayIndex = DateTime.now().weekday - 1; // 0=Mon, 6=Sun
return List.generate(7, (i) {
if (i == todayIndex) return DayStatus.today;
if (i < todayIndex) {
final daysAgo = todayIndex - i;
return daysAgo <= streak ? DayStatus.done : DayStatus.future;
}
return DayStatus.future;
});
}
List<DayStatus> get _effectiveDays =>
streakDays != null ? _generateDays(streakDays!) : (days ?? List.filled(7, DayStatus.future));
int get _effectiveCount => streakDays ?? streakCount;
@override
Widget build(BuildContext context) {
return Column(
@@ -41,7 +62,7 @@ class StreakDots extends StatelessWidget {
const Text('\u{1F525}', style: TextStyle(fontSize: 16)),
const SizedBox(width: 4),
Text(
'$streakCount',
'$_effectiveCount',
style: KittieTypography.labelLarge.copyWith(
color: KittieColors.brown,
),
@@ -55,7 +76,7 @@ class StreakDots extends StatelessWidget {
children: List.generate(7, (i) {
return Padding(
padding: EdgeInsets.only(left: i > 0 ? 8 : 0),
child: _DayDot(label: _dayLabels[i], status: days[i]),
child: _DayDot(label: _dayLabels[i], status: _effectiveDays[i]),
);
}),
),
+26 -10
View File
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import '../../core/constants/app_constants.dart';
import '../../core/theme/kittie_colors.dart';
import '../../core/theme/kittie_typography.dart';
import '../../domain/entities/task_entity.dart';
import 'energy_badge.dart';
/// A task list item with circular checkbox, title, energy badge, and time.
@@ -18,25 +19,40 @@ import 'energy_badge.dart';
class TaskCard extends StatelessWidget {
const TaskCard({
super.key,
required this.title,
required this.energyLevel,
this.task,
this.title,
this.energyLevel,
this.isDone = false,
this.isSelfCare = false,
this.isOverdue = false,
this.timeText,
this.onComplete,
this.onToggle,
this.onTap,
});
final String title;
final int energyLevel;
/// Optional task entity; its fields take precedence over individual params.
final TaskEntity? task;
final String? title;
final int? energyLevel;
final bool isDone;
final bool isSelfCare;
final bool isOverdue;
final String? timeText;
final VoidCallback? onComplete;
final VoidCallback? onToggle;
final VoidCallback? onTap;
String get _title => task?.title ?? title ?? '';
int get _energyLevel => task?.energyLevel ?? energyLevel ?? 2;
bool get _isDone => task?.isCompleted ?? isDone;
bool get _isSelfCare => task?.isSelfCare ?? isSelfCare;
bool get _isOverdue {
if (task?.deadline != null) return task!.deadline!.isBefore(DateTime.now());
return isOverdue;
}
VoidCallback? get _toggleCallback => onComplete ?? onToggle;
@override
Widget build(BuildContext context) {
return GestureDetector(
@@ -45,20 +61,20 @@ class TaskCard extends StatelessWidget {
constraints: const BoxConstraints(minHeight: AppConstants.minTouchTarget),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: isOverdue
color: _isOverdue
? KittieColors.redLight
: KittieColors.creamDark,
borderRadius: BorderRadius.circular(AppConstants.cardBorderRadius),
),
child: Row(
children: [
_Checkbox(isDone: isDone, onToggle: onToggle),
_Checkbox(isDone: _isDone, onToggle: _toggleCallback),
const SizedBox(width: 12),
Expanded(child: _Content(
title: title,
energyLevel: energyLevel,
isDone: isDone,
isSelfCare: isSelfCare,
title: _title,
energyLevel: _energyLevel,
isDone: _isDone,
isSelfCare: _isSelfCare,
timeText: timeText,
)),
],