Skip to content

Commit 8034323

Browse files
authored
Initialize Riverpod state management and responsive utilities (#271)
* feat: Initialize Riverpod state management and responsive utilities * Remove redundant dashboard team name provider * Expose theme via Riverpod * Add Freezed based dashboard state * Adopt Riverpod codegen and reorganize models
1 parent bfac91a commit 8034323

13 files changed

Lines changed: 556 additions & 19 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// Centralized layout breakpoints for Ell-ena responsive layouts.
2+
///
3+
/// Widths are logical pixels (same as [MediaQuery.sizeOf] width).
4+
class Breakpoints {
5+
Breakpoints._();
6+
/// Maximum width (exclusive) for [DeviceSize.mobile].
7+
static const double mobileMax = 600;
8+
9+
/// Maximum width (exclusive) for [DeviceSize.tablet].
10+
static const double tabletMax = 1024;
11+
}
12+
13+
/// Device size bucket derived from layout width.
14+
enum DeviceSize {
15+
mobile,
16+
tablet,
17+
desktop,
18+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import 'package:flutter/material.dart';
2+
3+
import 'breakpoints.dart';
4+
import 'responsive_utils.dart';
5+
6+
/// Selects [mobile], [tablet], or [desktop] child based on available width.
7+
///
8+
/// Uses [LayoutBuilder] so it reacts to window resizes (web/desktop) without
9+
/// requiring a full app rebuild.
10+
class ResponsiveLayout extends StatelessWidget {
11+
const ResponsiveLayout({
12+
super.key,
13+
required this.mobile,
14+
this.tablet,
15+
required this.desktop,
16+
});
17+
18+
final Widget mobile;
19+
final Widget? tablet;
20+
final Widget desktop;
21+
22+
@override
23+
Widget build(BuildContext context) {
24+
return LayoutBuilder(
25+
builder: (context, constraints) {
26+
final width = constraints.maxWidth;
27+
switch (ResponsiveUtils.deviceSizeForWidth(width)) {
28+
case DeviceSize.mobile:
29+
return mobile;
30+
case DeviceSize.tablet:
31+
return tablet ?? desktop;
32+
case DeviceSize.desktop:
33+
return desktop;
34+
}
35+
},
36+
);
37+
}
38+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import 'package:flutter/material.dart';
2+
3+
import 'breakpoints.dart';
4+
5+
/// Layout helpers based on screen width.
6+
class ResponsiveUtils {
7+
ResponsiveUtils._();
8+
static DeviceSize deviceSizeForWidth(double width) {
9+
if (width < Breakpoints.mobileMax) return DeviceSize.mobile;
10+
if (width < Breakpoints.tabletMax) return DeviceSize.tablet;
11+
return DeviceSize.desktop;
12+
}
13+
14+
static DeviceSize deviceSizeOf(BuildContext context) {
15+
return deviceSizeForWidth(MediaQuery.sizeOf(context).width);
16+
}
17+
18+
static bool isMobile(BuildContext context) =>
19+
deviceSizeOf(context) == DeviceSize.mobile;
20+
21+
static bool isTablet(BuildContext context) =>
22+
deviceSizeOf(context) == DeviceSize.tablet;
23+
24+
static bool isDesktop(BuildContext context) =>
25+
deviceSizeOf(context) == DeviceSize.desktop;
26+
27+
static bool isMobileWidth(double width) =>
28+
deviceSizeForWidth(width) == DeviceSize.mobile;
29+
30+
static bool isTabletWidth(double width) =>
31+
deviceSizeForWidth(width) == DeviceSize.tablet;
32+
33+
static bool isDesktopWidth(double width) =>
34+
deviceSizeForWidth(width) == DeviceSize.desktop;
35+
}
36+
37+
/// Convenience extensions on [BuildContext] for responsive checks.
38+
extension ResponsiveContext on BuildContext {
39+
DeviceSize get deviceSize => ResponsiveUtils.deviceSizeOf(this);
40+
41+
bool get isMobile => ResponsiveUtils.isMobile(this);
42+
bool get isTablet => ResponsiveUtils.isTablet(this);
43+
bool get isDesktop => ResponsiveUtils.isDesktop(this);
44+
}

lib/main.dart

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart'
3+
hide ChangeNotifierProvider;
24
import 'package:provider/provider.dart';
35
import 'screens/splash_screen.dart';
46
import 'screens/home/home_screen.dart';
57
import 'screens/chat/chat_screen.dart';
68
import 'services/navigation_service.dart';
79
import 'services/supabase_service.dart';
810
import 'services/ai_service.dart';
11+
import 'providers/theme_provider.dart';
912
import 'theme/theme_controller.dart';
1013
import 'theme/app_themes.dart';
1114

@@ -23,20 +26,25 @@ void main() async {
2326

2427
runApp(
2528
WidgetsBindingObserverWidget(
26-
child: ChangeNotifierProvider<ThemeController>.value(
27-
value: themeController,
28-
child: const MyApp(),
29+
child: ProviderScope(
30+
overrides: [
31+
themeControllerProvider.overrideWith((ref) => themeController),
32+
],
33+
child: ChangeNotifierProvider<ThemeController>.value(
34+
value: themeController,
35+
child: const MyApp(),
36+
),
2937
),
3038
),
3139
);
3240

3341
}
34-
class MyApp extends StatelessWidget {
42+
class MyApp extends ConsumerWidget {
3543
const MyApp({super.key});
3644

3745
@override
38-
Widget build(BuildContext context) {
39-
final themeController = context.watch<ThemeController>();
46+
Widget build(BuildContext context, WidgetRef ref) {
47+
final themeController = ref.watch(themeControllerProvider);
4048
return MaterialApp(
4149
title: 'Ell-ena',
4250
debugShowCheckedModeBanner: false,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import 'package:riverpod_annotation/riverpod_annotation.dart';
2+
3+
import 'models/dashboard_state.dart';
4+
import 'models/enums/dashboard_list_filter.dart';
5+
6+
part 'dashboard_controller.g.dart';
7+
8+
/// Single controller for dashboard-related Riverpod state.
9+
@Riverpod(keepAlive: true)
10+
class DashboardController extends _$DashboardController {
11+
@override
12+
DashboardState build() => const DashboardState();
13+
14+
/// Sets chart range: 0 = week, 1 = month.
15+
void setTimeRange(int timeRange) {
16+
state = state.copyWith(timeRange: timeRange);
17+
}
18+
19+
/// Sets the active workspace/team id.
20+
void setSelectedTeam(String? teamId) {
21+
state = state.copyWith(selectedTeamId: teamId);
22+
}
23+
24+
/// Clears the selected team.
25+
void clearSelectedTeam() {
26+
setSelectedTeam(null);
27+
}
28+
29+
/// Sets the dashboard list filter.
30+
void setFilter(DashboardListFilter filter) {
31+
state = state.copyWith(listFilter: filter);
32+
}
33+
34+
/// Resets dashboard state to defaults.
35+
void reset() {
36+
state = const DashboardState();
37+
}
38+
}

lib/providers/dashboard/dashboard_controller.g.dart

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'package:freezed_annotation/freezed_annotation.dart';
2+
3+
import 'enums/dashboard_list_filter.dart';
4+
5+
part 'dashboard_state.freezed.dart';
6+
7+
/// Dashboard UI state managed by [DashboardController].
8+
///
9+
/// Chart range uses the same values as [DashboardScreen]: 0 = week, 1 = month.
10+
/// Team display name is derived from profile/teams data — only [selectedTeamId]
11+
/// is stored here.
12+
@freezed
13+
class DashboardState with _$DashboardState {
14+
const factory DashboardState({
15+
@Default(0) int timeRange,
16+
String? selectedTeamId,
17+
@Default(DashboardListFilter.all) DashboardListFilter listFilter,
18+
}) = _DashboardState;
19+
}

0 commit comments

Comments
 (0)