diff --git a/lib/app.dart b/lib/app.dart index 1284a8a..868e856 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:thesis_shop/bloc/product_bloc.dart'; import 'package:thesis_shop/bloc/user_bloc.dart'; import 'package:thesis_shop/bloc_provider.dart'; import 'package:thesis_shop/route_key.dart'; @@ -14,6 +15,7 @@ class ThesisShopApp extends StatelessWidget { BlocProvider setupBlocs() { return BlocProvider( + productBloc: ProductBloc(productService), userBloc: UserBloc(), ); } diff --git a/lib/bloc/product_bloc.dart b/lib/bloc/product_bloc.dart new file mode 100644 index 0000000..9f3580a --- /dev/null +++ b/lib/bloc/product_bloc.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:thesis_shop/models/product.dart'; +import 'package:thesis_shop/service/product_service.dart'; +import 'package:thesis_shop/utils/disposable.dart'; + +part 'product_bloc_events.dart'; +part 'product_bloc_states.dart'; + +class ProductBloc implements Disposable { + final ProductService _productService; + + var _productsState = ProductsState.loading(); + ProductsState get productsState => _productsState; + + final _productsStateController = StreamController.broadcast(); + Sink get _productsStateSink => _productsStateController.sink; + Stream get productsStateStream => + _productsStateController.stream; + + final _loadProductEventController = + StreamController.broadcast(); + Sink get loadProductSink => + _loadProductEventController.sink; + + ProductBloc(this._productService) { + _handleLoadProductEvents(); + } + + void _handleLoadProductEvents() { + _loadProductEventController.stream.listen((_) async { + _productsState = ProductsState.loading(); + _productsStateSink.add(_productsState); + + try { + final products = await _productService.fetchProducts(); + _productsState = ProductsState.loaded(products); + _productsStateSink.add(_productsState); + } catch (err) { + _productsState = ProductsState.error(err.toString()); + _productsStateSink.add(_productsState); + } + }); + } + + @override + Future dispose() { + return Future.wait([ + _productsStateController.close(), + _loadProductEventController.close(), + ]); + } +} diff --git a/lib/bloc/product_bloc_events.dart b/lib/bloc/product_bloc_events.dart new file mode 100644 index 0000000..e465b64 --- /dev/null +++ b/lib/bloc/product_bloc_events.dart @@ -0,0 +1,5 @@ +part of 'product_bloc.dart'; + +class LoadProductEvent { + const LoadProductEvent(); +} diff --git a/lib/bloc/product_bloc_states.dart b/lib/bloc/product_bloc_states.dart new file mode 100644 index 0000000..13ec7b2 --- /dev/null +++ b/lib/bloc/product_bloc_states.dart @@ -0,0 +1,31 @@ +part of 'product_bloc.dart'; + +abstract class ProductsState { + const ProductsState._(); + + factory ProductsState.loading() => const ProductsLoading(); + factory ProductsState.error(String errorMessage) => + ProductsError(errorMessage); + factory ProductsState.loaded(List products) => + ProductsLoaded(products); + + bool isLoading() => this is ProductsLoading; + bool isLoaded() => this is ProductsLoaded; + bool hasError() => this is ProductsError; + + T as() => this as T; +} + +class ProductsLoading extends ProductsState { + const ProductsLoading() : super._(); +} + +class ProductsError extends ProductsState { + final String errorMessage; + const ProductsError(this.errorMessage) : super._(); +} + +class ProductsLoaded extends ProductsState { + final List products; + const ProductsLoaded(this.products) : super._(); +} diff --git a/lib/bloc_provider.dart b/lib/bloc_provider.dart index 0e335ba..bb1ba19 100644 --- a/lib/bloc_provider.dart +++ b/lib/bloc_provider.dart @@ -1,17 +1,23 @@ import 'package:flutter/widgets.dart'; +import 'package:thesis_shop/bloc/product_bloc.dart'; import 'package:thesis_shop/bloc/user_bloc.dart'; import 'utils/disposable.dart'; class BlocProvider implements Disposable { final UserBloc userBloc; + final ProductBloc productBloc; - const BlocProvider({required this.userBloc}); + const BlocProvider({ + required this.userBloc, + required this.productBloc, + }); @override Future dispose() async { await Future.wait([ userBloc.dispose(), + productBloc.dispose(), ]); } } diff --git a/lib/screens/product_list/product_list_screen.dart b/lib/screens/product_list/product_list_screen.dart index 30ce3a8..e64872c 100644 --- a/lib/screens/product_list/product_list_screen.dart +++ b/lib/screens/product_list/product_list_screen.dart @@ -1,31 +1,46 @@ import 'package:flutter/material.dart'; -import 'package:thesis_shop/models/product.dart'; +import 'package:thesis_shop/bloc/product_bloc.dart'; +import 'package:thesis_shop/bloc_provider.dart'; import 'package:thesis_shop/widgets/user_switch.dart'; import 'cart_button_overlay.dart'; import 'product_list.dart'; -const _productPlaceholder = [ - Product(title: 'Bananen', price: 3), - Product(title: 'Äpfel', price: 2), - Product(title: 'Birnen', price: 2.5), - Product(title: 'Kirschen', price: 1.2), -]; - class ProductListScreen extends StatelessWidget { const ProductListScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - const products = _productPlaceholder; + final productBloc = AppState.of(context).blocProvider.productBloc; + if (productBloc.productsState.isLoading()) { + productBloc.loadProductSink.add(const LoadProductEvent()); + } return Scaffold( appBar: AppBar( title: const Text('Thesis Shop'), actions: [UserSwitch(isOn: true, onChanged: (_) {})], ), - body: const CartButtonOverlay( - child: ProductList(products: products), - ), + body: StreamBuilder( + stream: productBloc.productsStateStream, + initialData: productBloc.productsState, + builder: (context, snapshot) { + final state = snapshot.requireData; + if (state.isLoaded()) { + return CartButtonOverlay( + child: + ProductList(products: state.as().products), + ); + } else if (state.hasError()) { + return Center( + child: Text(state.as().errorMessage), + ); + } else if (state.isLoading()) { + return const Center( + child: CircularProgressIndicator.adaptive(), + ); + } + throw UnimplementedError(); + }), ); } }