first
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/// In-memory sample catalog used when the app is offline or in mock mode.
|
||||
library;
|
||||
|
||||
import '../models/item.dart';
|
||||
|
||||
/// Static starter data so the UI is useful without a network.
|
||||
abstract final class FakeItemData {
|
||||
static final List<Item> items = [
|
||||
Item(
|
||||
id: 'local-1',
|
||||
title: 'Material 3 surfaces',
|
||||
subtitle: 'Use tonal layers instead of heavy drop shadows.',
|
||||
description:
|
||||
'Material 3 prefers surface tints and short, soft shadows. Keep cards '
|
||||
'on surfaceContainerLow, bump the hero to surfaceContainerHighest, and '
|
||||
'reserve elevation for sheets and dialogs.',
|
||||
category: ItemCategory.design,
|
||||
createdAt: DateTime(2026, 4, 12),
|
||||
),
|
||||
Item(
|
||||
id: 'local-2',
|
||||
title: 'Provider at the root',
|
||||
subtitle: 'Lift shared state above the widget that consumes it.',
|
||||
description:
|
||||
'Wrap MaterialApp with MultiProvider so ThemeController and '
|
||||
'ItemController survive route changes. Keep UI widgets dumb: they '
|
||||
'read context.watch and call controller methods.',
|
||||
category: ItemCategory.development,
|
||||
createdAt: DateTime(2026, 5, 3),
|
||||
),
|
||||
Item(
|
||||
id: 'local-3',
|
||||
title: 'Focus on one task',
|
||||
subtitle: 'A short list beats an overflowing dashboard.',
|
||||
description:
|
||||
'The home screen shows a greeting, a compact stat strip, and a '
|
||||
'ListView.builder of items. Extra tools live one tap away on Search, '
|
||||
'Profile, and Settings.',
|
||||
category: ItemCategory.productivity,
|
||||
createdAt: DateTime(2026, 6, 18),
|
||||
),
|
||||
Item(
|
||||
id: 'local-4',
|
||||
title: 'Breathable spacing',
|
||||
subtitle: '16–24px padding and 12px gaps keep cards from crowding.',
|
||||
description:
|
||||
'On compact phones use 16px page padding. From 600px up, step to 24px '
|
||||
'and cap the content width so lines stay readable on tablets.',
|
||||
category: ItemCategory.lifestyle,
|
||||
createdAt: DateTime(2026, 7, 1),
|
||||
),
|
||||
Item(
|
||||
id: 'local-5',
|
||||
title: 'Hero transitions',
|
||||
subtitle: 'Match a tag on the list tile and the detail header.',
|
||||
description:
|
||||
'Wrap the leading glyph in a Hero with a stable tag derived from the '
|
||||
'item id. The detail screen reuses the same widget so Flutter can '
|
||||
'animate size and position.',
|
||||
category: ItemCategory.design,
|
||||
createdAt: DateTime(2026, 7, 22),
|
||||
),
|
||||
Item(
|
||||
id: 'local-6',
|
||||
title: 'Mock first, HTTP later',
|
||||
subtitle: 'A repository hides whether data is fake or remote.',
|
||||
description:
|
||||
'ItemRepository serves FakeItemData by default and can switch to '
|
||||
'ItemApi (http + JSONPlaceholder) from Settings. Failures fall back '
|
||||
'to the mock list so the UI never goes empty on a demo device.',
|
||||
category: ItemCategory.development,
|
||||
createdAt: DateTime(2026, 8, 4),
|
||||
),
|
||||
Item(
|
||||
id: 'local-7',
|
||||
title: 'Capture ideas quickly',
|
||||
subtitle: 'The add-item form validates before it writes.',
|
||||
description:
|
||||
'Title, summary, and description each have a length rule. Category is '
|
||||
'a required dropdown. On success the new item is inserted at the top '
|
||||
'of ItemController so Home and Search update immediately.',
|
||||
category: ItemCategory.productivity,
|
||||
createdAt: DateTime(2026, 8, 15),
|
||||
),
|
||||
Item(
|
||||
id: 'local-8',
|
||||
title: 'Light and dark equally',
|
||||
subtitle: 'Seeded ColorSchemes keep contrast in both modes.',
|
||||
description:
|
||||
'ThemeController persists ThemeMode in shared_preferences. The '
|
||||
'settings screen offers System, Light, and Dark. Both schemes come '
|
||||
'from the same teal seed color.',
|
||||
category: ItemCategory.lifestyle,
|
||||
createdAt: DateTime(2026, 8, 20),
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/// HTTP client placeholder that talks to a public sample API.
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../models/item.dart';
|
||||
|
||||
/// Fetches catalog items from JSONPlaceholder.
|
||||
///
|
||||
/// This is intentionally a thin, replaceable client. Swap [baseUrl] and
|
||||
/// [Item.fromJson] when you point the app at a real backend.
|
||||
class ItemApi {
|
||||
ItemApi({http.Client? client, this.baseUrl = defaultBaseUrl})
|
||||
: _client = client ?? http.Client();
|
||||
|
||||
static const defaultBaseUrl = 'https://jsonplaceholder.typicode.com';
|
||||
|
||||
final http.Client _client;
|
||||
final String baseUrl;
|
||||
|
||||
/// GET `/posts` and map each record to an [Item].
|
||||
Future<List<Item>> fetchItems({int limit = 12}) async {
|
||||
final uri = Uri.parse('$baseUrl/posts');
|
||||
final response = await _client.get(
|
||||
uri,
|
||||
headers: const {'Accept': 'application/json'},
|
||||
);
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw ItemApiException(
|
||||
'Request failed (${response.statusCode})',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (decoded is! List) {
|
||||
throw const ItemApiException('Unexpected response shape');
|
||||
}
|
||||
|
||||
return decoded
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.take(limit)
|
||||
.map(Item.fromJson)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
void close() => _client.close();
|
||||
}
|
||||
|
||||
class ItemApiException implements Exception {
|
||||
const ItemApiException(this.message, {this.statusCode});
|
||||
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/// Chooses between mock data and the HTTP client, then returns [Item] lists.
|
||||
library;
|
||||
|
||||
import '../models/item.dart';
|
||||
import 'fake_item_data.dart';
|
||||
import 'item_api.dart';
|
||||
|
||||
/// Single entry point for catalog data. UI code should depend on this type,
|
||||
/// not on [ItemApi] or [FakeItemData] directly.
|
||||
class ItemRepository {
|
||||
ItemRepository({
|
||||
ItemApi? api,
|
||||
this.useRemoteData = false,
|
||||
}) : _api = api ?? ItemApi();
|
||||
|
||||
final ItemApi _api;
|
||||
|
||||
/// When true, [fetchItems] hits the network; otherwise it returns local fakes.
|
||||
bool useRemoteData;
|
||||
|
||||
Future<List<Item>> fetchItems() async {
|
||||
if (!useRemoteData) {
|
||||
return List<Item>.from(FakeItemData.items);
|
||||
}
|
||||
|
||||
try {
|
||||
return await _api.fetchItems();
|
||||
} catch (_) {
|
||||
// Keep the demo usable if the device is offline or the API fails.
|
||||
return List<Item>.from(FakeItemData.items);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a locally-created item (no network round-trip).
|
||||
Item createLocal({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required String description,
|
||||
required ItemCategory category,
|
||||
}) {
|
||||
return Item(
|
||||
id: 'local-${DateTime.now().microsecondsSinceEpoch}',
|
||||
title: title.trim(),
|
||||
subtitle: subtitle.trim(),
|
||||
description: description.trim(),
|
||||
category: category,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/// Thin wrapper around [SharedPreferences] for local key-value persistence.
|
||||
library;
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../utils/constants.dart';
|
||||
|
||||
/// Loads and stores user preferences. Call [init] once at startup.
|
||||
class StorageService {
|
||||
StorageService({this._preferences});
|
||||
|
||||
SharedPreferences? _preferences;
|
||||
|
||||
Future<void> init() async {
|
||||
_preferences ??= await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
SharedPreferences get _prefs {
|
||||
final prefs = _preferences;
|
||||
if (prefs == null) {
|
||||
throw StateError('StorageService.init() must be called before use.');
|
||||
}
|
||||
return prefs;
|
||||
}
|
||||
|
||||
String get themeModeName =>
|
||||
_prefs.getString(StorageKeys.themeMode) ?? 'system';
|
||||
|
||||
Future<void> setThemeModeName(String value) {
|
||||
return _prefs.setString(StorageKeys.themeMode, value);
|
||||
}
|
||||
|
||||
bool get useRemoteData => _prefs.getBool(StorageKeys.useRemoteData) ?? false;
|
||||
|
||||
Future<void> setUseRemoteData(bool value) {
|
||||
return _prefs.setBool(StorageKeys.useRemoteData, value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user