diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..0075eb7 --- /dev/null +++ b/lib/README.md @@ -0,0 +1,22 @@ +![Image](https://github.com/user-attachments/assets/179ecb44-0167-4968-ab54-3198782716d0) + +# Clean Architecture in OpenWardrobe + +OpenWardrobe follows the principles of Clean Architecture, which emphasizes separation of concerns and independence of frameworks. Here's how the different components fit into this architecture: + +## UI Layer +The UI layer is responsible for presenting data to the user and handling user interactions. In OpenWardrobe, this is represented by the various screens (e.g., `HomeScreen`, `WardrobeScreen`, `LookbookScreen`). These screens are built using Flutter widgets and are designed to be reactive, updating the UI based on the state of the application. + +## Router Layer +The router layer manages navigation and routing within the application. It defines how users move between different screens and handles deep linking. In OpenWardrobe, the router is implemented using the `go_router` package, which provides a declarative way to manage routes and navigation. + +## Repository Layer +The repository layer acts as an intermediary between the data sources (e.g., databases, APIs) and the rest of the application. It abstracts the data access logic and provides a clean API for the UI and business logic layers. In OpenWardrobe, the `AppRepository` is responsible for fetching user profiles and wardrobe items, ensuring that the UI does not need to know about the underlying data sources. + +## Dependency Injection (DI) Layer +The DI layer is responsible for managing the dependencies of the application. It allows for easy swapping of implementations and promotes testability. OpenWardrobe uses the `get_it` package for dependency injection, which simplifies the process of providing instances of classes (like repositories and controllers) to the parts of the application that need them. + +## Brick Layer +The brick layer contains the data models and business logic. It defines the structure of the data used throughout the application and encapsulates the business rules. In OpenWardrobe, this includes models like `UserProfile` and `WardrobeItem`, which represent the core entities of the application. + +By organizing the application into these distinct layers, OpenWardrobe achieves a clean architecture that is maintainable, testable, and scalable. Each layer can evolve independently, and changes in one layer do not directly impact others, adhering to the principles of separation of concerns and dependency inversion. diff --git a/lib/di/service_locator.dart b/lib/di/service_locator.dart index ce33f19..940006a 100644 --- a/lib/di/service_locator.dart +++ b/lib/di/service_locator.dart @@ -7,5 +7,7 @@ final getIt = GetIt.instance; void setupLocator() { // Register the AppRepository instance. // Ensure that AppRepository.configure(...) is called before or within its constructor. - getIt.registerLazySingleton(() => AppRepository()); -} \ No newline at end of file + getIt.registerSingleton(AppRepository()); +} + +AppRepository get appRepository => getIt(); diff --git a/lib/domain/controllers/home_controller.dart b/lib/domain/controllers/home_controller.dart new file mode 100644 index 0000000..ac888dd --- /dev/null +++ b/lib/domain/controllers/home_controller.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:openwardrobe/brick/models/user_profile.model.dart'; +import 'package:openwardrobe/repositories/app_repository.dart'; + +class HomeController extends ChangeNotifier { + final AppRepository _appRepository = GetIt.instance(); + + UserProfile? _userProfile; + UserProfile? get userProfile => _userProfile; + + Future fetchUserProfile() async { + try { + final profiles = await _appRepository.get(); + if (profiles.isNotEmpty) { + _userProfile = profiles.first; + notifyListeners(); + } + } catch (e) { + // Handle error + print('Error fetching user profile: $e'); + } + } + + void handleAddItems(BuildContext context) { + Navigator.pushNamed(context, '/camera'); + } + + void handleCreateOutfit() { + // Handle create outfit logic + } + + void handleScheduleOutfit() { + // Handle schedule outfit logic + } + + void handleViewStats() { + // Handle view stats logic + } +} diff --git a/lib/domain/controllers/lookbook_controller.dart b/lib/domain/controllers/lookbook_controller.dart new file mode 100644 index 0000000..4c4e033 --- /dev/null +++ b/lib/domain/controllers/lookbook_controller.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:openwardrobe/domain/models/lookbook.model.dart'; +import 'package:openwardrobe/data/repositories/app_repository.dart'; + +class LookbookController extends ChangeNotifier { + final AppRepository _appRepository = GetIt.instance(); + + List? _lookbooks; + List? get lookbooks => _lookbooks; + + Future> fetchLookbooks() async { + try { + final lookbooks = await _appRepository.get(); + _lookbooks = lookbooks; + notifyListeners(); + return lookbooks; + } catch (e) { + // Handle error + print('Error fetching lookbooks: $e'); + return []; + } + } + + void handleAddLookbook(BuildContext context) { + Navigator.pushNamed(context, '/lookbook/add'); + } + + void handleEditLookbook(Lookbook lookbook) { + // Handle edit lookbook logic + } + + void handleDeleteLookbook(Lookbook lookbook) { + // Handle delete lookbook logic + } +} diff --git a/lib/domain/controllers/wardrobe_controller.dart b/lib/domain/controllers/wardrobe_controller.dart new file mode 100644 index 0000000..6583a86 --- /dev/null +++ b/lib/domain/controllers/wardrobe_controller.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:openwardrobe/domain/models/wardrobe_item.model.dart'; +import 'package:openwardrobe/data/repositories/app_repository.dart'; +import 'package:get_it/get_it.dart'; + +class WardrobeController extends ChangeNotifier { + final AppRepository _appRepository = GetIt.instance(); + + List? _wardrobeItems; + List? get wardrobeItems => _wardrobeItems; + + Future> fetchWardrobeItems() async { + try { + final items = await _appRepository.get(); + _wardrobeItems = items; + notifyListeners(); + return items; + } catch (e) { + // Handle error + print('Error fetching wardrobe items: $e'); + return []; + } + } + + Future addItem(WardrobeItem item) async { + try { + await _appRepository.upsert(item); + fetchWardrobeItems(); // Refresh the list after adding an item + } catch (e) { + // Handle error + print('Error adding wardrobe item: $e'); + } + } + + Future deleteItem(WardrobeItem item) async { + try { + await _appRepository.delete(item); + fetchWardrobeItems(); // Refresh the list after deleting an item + } catch (e) { + // Handle error + print('Error deleting wardrobe item: $e'); + } + } + + Future updateItem(WardrobeItem item) async { + try { + await _appRepository.upsert(item); + fetchWardrobeItems(); // Refresh the list after updating an item + } catch (e) { + // Handle error + print('Error updating wardrobe item: $e'); + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 004d83c..87ad817 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,11 +21,6 @@ Future main() async { await AppRepository.configure(databaseFactory); - - - - await AppRepository().initialize(); - setupLocator(); runApp(const MyApp()); diff --git a/lib/repositories/app_repository.dart b/lib/repositories/app_repository.dart index ffde7d6..6a65741 100644 --- a/lib/repositories/app_repository.dart +++ b/lib/repositories/app_repository.dart @@ -1,9 +1,6 @@ -// Saved in my_app/lib/src/brick/repository.dart import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart'; import 'package:brick_sqlite/brick_sqlite.dart'; import 'package:brick_sqlite/memory_cache_provider.dart'; -// This hide is for Brick's @Supabase annotation; in most cases, -// supabase_flutter **will not** be imported in application code. import 'package:brick_supabase/brick_supabase.dart' hide Supabase; import 'package:openwardrobe/brick/db/schema.g.dart'; import 'package:sqflite_common/sqlite_api.dart'; diff --git a/lib/router/app_router.dart b/lib/router/app_router.dart index b718738..2697d03 100644 --- a/lib/router/app_router.dart +++ b/lib/router/app_router.dart @@ -11,6 +11,7 @@ import '../ui/screens/home/page.dart'; import '../ui/screens/wardrobe/page.dart'; import '../ui/screens/wardrobe/add/page.dart'; import '../ui/widgets/scaffold_with_navbar.dart'; +import 'package:openwardrobe/di/service_locator.dart'; class AppRouter { static final GlobalKey _rootNavigatorKey = diff --git a/lib/ui/screens/home/home_controller.dart b/lib/ui/screens/home/home_controller.dart new file mode 100644 index 0000000..ac888dd --- /dev/null +++ b/lib/ui/screens/home/home_controller.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:openwardrobe/brick/models/user_profile.model.dart'; +import 'package:openwardrobe/repositories/app_repository.dart'; + +class HomeController extends ChangeNotifier { + final AppRepository _appRepository = GetIt.instance(); + + UserProfile? _userProfile; + UserProfile? get userProfile => _userProfile; + + Future fetchUserProfile() async { + try { + final profiles = await _appRepository.get(); + if (profiles.isNotEmpty) { + _userProfile = profiles.first; + notifyListeners(); + } + } catch (e) { + // Handle error + print('Error fetching user profile: $e'); + } + } + + void handleAddItems(BuildContext context) { + Navigator.pushNamed(context, '/camera'); + } + + void handleCreateOutfit() { + // Handle create outfit logic + } + + void handleScheduleOutfit() { + // Handle schedule outfit logic + } + + void handleViewStats() { + // Handle view stats logic + } +} diff --git a/lib/ui/screens/home/page.dart b/lib/ui/screens/home/view.dart similarity index 97% rename from lib/ui/screens/home/page.dart rename to lib/ui/screens/home/view.dart index bfb1be0..f476c88 100644 --- a/lib/ui/screens/home/page.dart +++ b/lib/ui/screens/home/view.dart @@ -6,8 +6,8 @@ import 'package:openwardrobe/repositories/app_repository.dart'; import 'package:openwardrobe/ui/widgets/dashboard/link.dart'; import 'package:openwardrobe/ui/widgets/user_profile/user_profile_component.dart'; -class HomeScreen extends StatelessWidget { - HomeScreen({super.key}); +class HomeView extends StatelessWidget { + HomeView({super.key}); final appRepo = GetIt.instance(); diff --git a/lib/ui/screens/lookbook/lookbook_controller.dart b/lib/ui/screens/lookbook/lookbook_controller.dart new file mode 100644 index 0000000..0a19dd9 --- /dev/null +++ b/lib/ui/screens/lookbook/lookbook_controller.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:openwardrobe/brick/models/lookbook.model.dart'; +import 'package:openwardrobe/repositories/app_repository.dart'; + +class LookbookController extends ChangeNotifier { + final AppRepository _appRepository = GetIt.instance(); + + List? _lookbooks; + List? get lookbooks => _lookbooks; + + Future> fetchLookbooks() async { + try { + final lookbooks = await _appRepository.get(); + _lookbooks = lookbooks; + notifyListeners(); + return lookbooks; + } catch (e) { + // Handle error + print('Error fetching lookbooks: $e'); + return []; + } + } + + void handleAddLookbook(BuildContext context) { + Navigator.pushNamed(context, '/lookbook/add'); + } + + void handleEditLookbook(Lookbook lookbook) { + // Handle edit lookbook logic + } + + void handleDeleteLookbook(Lookbook lookbook) { + // Handle delete lookbook logic + } +} diff --git a/lib/ui/screens/lookbook/page.dart b/lib/ui/screens/lookbook/page.dart index 8b30583..5cace21 100644 --- a/lib/ui/screens/lookbook/page.dart +++ b/lib/ui/screens/lookbook/page.dart @@ -1,71 +1,65 @@ import 'package:flutter/material.dart'; import 'package:openwardrobe/brick/models/lookbook.model.dart'; -import 'package:openwardrobe/repositories/app_repository.dart'; -import 'package:get_it/get_it.dart'; import 'package:openwardrobe/ui/widgets/lookbook/lookbook_component.dart'; +import 'package:openwardrobe/ui/screens/lookbook/lookbook_controller.dart'; class LookbookScreen extends StatelessWidget { - LookbookScreen({super.key}); + LookbookScreen({super.key}); - final appRepo = GetIt.instance(); - + final LookbookController controller = LookbookController(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - // Welcome, username and profile picture title: const Text('Lookbook'), ), body: SingleChildScrollView( - - child: IntrinsicHeight( + child: IntrinsicHeight( child: Align( alignment: Alignment.topCenter, child: Column( - // DashboardLink - children: [ - // Max width for row - const SizedBox(height: 20), - Expanded( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 500), - child: FutureBuilder>( - future: appRepo.get(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center( - child: Text('Error: ${snapshot.error}'), - ); - } else if (!snapshot.hasData || snapshot.data!.isEmpty) { - return const Center(child: Text('No items found')); - } else { - final items = snapshot.data!; - return SingleChildScrollView( - child: Wrap( - spacing: 8.0, - runSpacing: 8.0, - alignment: WrapAlignment.start, - children: items.map((item) => - Container( - width: 150, - child: LookbookComponent(item: item), - ) - ).toList(), - ), - ); - } - } - ), - ), - ) - ], + children: [ + const SizedBox(height: 20), + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: FutureBuilder>( + future: controller.fetchLookbooks(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center( + child: Text('Error: ${snapshot.error}'), + ); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text('No items found')); + } else { + final items = snapshot.data!; + return SingleChildScrollView( + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + alignment: WrapAlignment.start, + children: items.map((item) => + Container( + width: 150, + child: LookbookComponent(item: item), + ) + ).toList(), + ), + ); + } + } + ), + ), + ) + ], + ), ), - ) - ), - ) + ), + ), ); } } diff --git a/lib/ui/screens/lookbook/view.dart b/lib/ui/screens/lookbook/view.dart new file mode 100644 index 0000000..77a1350 --- /dev/null +++ b/lib/ui/screens/lookbook/view.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:openwardrobe/brick/models/lookbook.model.dart'; +import 'package:openwardrobe/ui/widgets/lookbook/lookbook_component.dart'; +import 'package:openwardrobe/ui/screens/lookbook/lookbook_controller.dart'; + +class LookbookView extends StatelessWidget { + LookbookView({super.key}); + + final LookbookController controller = LookbookController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Lookbook'), + ), + body: SingleChildScrollView( + child: IntrinsicHeight( + child: Align( + alignment: Alignment.topCenter, + child: Column( + children: [ + const SizedBox(height: 20), + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: FutureBuilder>( + future: controller.fetchLookbooks(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center( + child: Text('Error: ${snapshot.error}'), + ); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text('No items found')); + } else { + final items = snapshot.data!; + return SingleChildScrollView( + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + alignment: WrapAlignment.start, + children: items.map((item) => + Container( + width: 150, + child: LookbookComponent(item: item), + ) + ).toList(), + ), + ); + } + } + ), + ), + ) + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui/screens/wardrobe/page.dart b/lib/ui/screens/wardrobe/page.dart index abc22e0..a69f69a 100644 --- a/lib/ui/screens/wardrobe/page.dart +++ b/lib/ui/screens/wardrobe/page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:openwardrobe/brick/models/wardrobe_item.model.dart'; -import 'package:openwardrobe/repositories/app_repository.dart'; -import 'package:get_it/get_it.dart'; +import 'package:openwardrobe/di/service_locator.dart'; import 'package:openwardrobe/ui/widgets/wardrobe_item/wardrobe_item_component.dart'; +import 'package:openwardrobe/ui/screens/wardrobe/wardrobe_controller.dart'; class WardrobeScreen extends StatefulWidget { const WardrobeScreen({super.key}); @@ -12,14 +12,14 @@ class WardrobeScreen extends StatefulWidget { } class _WardrobeScreenState extends State { - final appRepo = GetIt.instance(); + final WardrobeController _controller = WardrobeController(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Wardrobe')), body: FutureBuilder>( - future: appRepo.get(), + future: _controller.fetchWardrobeItems(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -79,4 +79,4 @@ class _WardrobeScreenState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/ui/screens/wardrobe/view.dart b/lib/ui/screens/wardrobe/view.dart new file mode 100644 index 0000000..fd20609 --- /dev/null +++ b/lib/ui/screens/wardrobe/view.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:openwardrobe/brick/models/wardrobe_item.model.dart'; +import 'package:openwardrobe/ui/widgets/wardrobe_item/wardrobe_item_component.dart'; +import 'package:openwardrobe/ui/screens/wardrobe/wardrobe_controller.dart'; + +class WardrobeView extends StatefulWidget { + const WardrobeView({super.key}); + + @override + _WardrobeViewState createState() => _WardrobeViewState(); +} + +class _WardrobeViewState extends State { + final WardrobeController _controller = WardrobeController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Wardrobe')), + body: FutureBuilder>( + future: _controller.fetchWardrobeItems(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text('No items found')); + } else { + final items = snapshot.data!; + + // Compute some stats + final int totalItems = items.length; + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Wardrobe Statistics + Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Total Items: $totalItems', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // Wardrobe Items Grid + Expanded( + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + ), + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return WardrobeItemComponent(item: item); + }, + ), + ), + ], + ), + ); + } + }, + ), + ); + } +} diff --git a/lib/ui/screens/wardrobe/wardrobe_controller.dart b/lib/ui/screens/wardrobe/wardrobe_controller.dart new file mode 100644 index 0000000..04b3eb1 --- /dev/null +++ b/lib/ui/screens/wardrobe/wardrobe_controller.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:openwardrobe/brick/models/wardrobe_item.model.dart'; +import 'package:openwardrobe/di/service_locator.dart'; + +class WardrobeController extends ChangeNotifier { + Future> fetchWardrobeItems() async { + try { + return await appRepository.getAll(); + } catch (e) { + // Handle error + return []; + } + } + + Future addItem(WardrobeItem item) async { + try { + await appRepository.upsert(item); + } catch (e) { + // Handle error + } + } + + Future deleteItem(WardrobeItem item) async { + try { + await appRepository.delete(item); + } catch (e) { + // Handle error + } + } + + Future updateItem(WardrobeItem item) async { + try { + await appRepository.upsert(item); + } catch (e) { + // Handle error + } + } +}