diff --git a/packages/leancode_cubit_utils/README.md b/packages/leancode_cubit_utils/README.md index 974996c..aea96ef 100644 --- a/packages/leancode_cubit_utils/README.md +++ b/packages/leancode_cubit_utils/README.md @@ -13,45 +13,35 @@ Import the package: # Usage -The collection of utilities in the package can be divided into two subsets. [Single Request Utils](#single-request-utils) are used for creating pages where a single request is made to retrieve data, which is then displayed. [Pagination Utils](#pagination-utils) are used for creating pages containing paginated lists. +The collection of utilities in the package can be divided into two subsets. [Single Request Utils](#single-request-utils) are used for creating pages where a single request is made to retrieve data which is then displayed. [Pagination Utils](#pagination-utils) are used for creating pages containing paginated lists. For both cases it is possible to implement variants that use different API clients. -Implementation of cubits for handling [CQRS](https://pub.dev/packages/cqrs) queries is covered in [`leancode_cubit_utils_cqrs`][leancode_cubit_utils_cqrs] but for both cases it is possible to implement variants that use different API clients. +Implementation of cubits for handling [CQRS](https://pub.dev/packages/cqrs) queries is covered in [`leancode_cubit_utils_cqrs`][leancode_cubit_utils_cqrs]. ## Single Request Utils ### `RequestCubit` -`RequestCubit` is used to execute a single API request. Example implementation of RequestCubit looks like this: +`RequestCubit` is used to execute a single API request. It has four generic arguments: +- `TRes` specifies what the request returns, +- `TData` specifies what is kept in TRes as response body, +- `TOut` determines which model we want to emit as data in the state, +- `TError` defines error's type. In the example below. + +`HttpRequestCubit` in the example below provides the generic http implementation that can be used while defining all needed `RequestCubits`. ```dart -// RequestCubit has four generic arguments: TRes, TData, TOut and TError. TRes specifies what the request returns, TData specifies what is kept in TRes as response body, TOut determines which model we want to emit as data in the state, TError defines error's type. -class ProjectDetailsCubit - extends RequestCubit { - ProjectDetailsCubit({ - required this.client, - required this.id, - }) : super('ProjectDetailsCubit'); +/// Base class for http request cubits. +abstract class HttpRequestCubit + extends RequestCubit { + HttpRequestCubit(super.loggerTag, {required this.client}); final http.Client client; - final String id; - - @override - // This method allows to map the given TRes into TOut. - ProjectDetailsDTO map(String data) => - ProjectDetailsDTO.fromJson(jsonDecode(data) as Map); - - @override - // In this method we should perform the request and return it in form of http.Response. - // http.Response is then internally handled by handleResult. - Future request() { - return client.get(Uri.parse('base-url/$id')); - } @override - // In this method we check the request's state - // and return the result on success or call handleError on failure. - Future> handleResult( - http.Response result) async { + /// Client-specific method needed for handling the API response. + Future> handleResult( + http.Response result, + ) async { if (result.statusCode == 200) { logger.info('Request success. Data: ${result.body}'); return RequestSuccessState(map(result.body)); @@ -70,6 +60,29 @@ class ProjectDetailsCubit } ``` +Example implementation of `RequestCubit` using defined `HttpRequestCubit` looks like this: + +```dart +class ProjectDetailsCubit extends HttpRequestCubit { + ProjectDetailsCubit({ + required super.client, + required this.id, + }) : super('ProjectDetailsCubit'); + + final String id; + + @override + // This method allows to map the given TRes into TOut. + ProjectDetailsDTO map(String data) => + ProjectDetailsDTO.fromJson(jsonDecode(data) as Map); + + @override + // In this method we should perform the request and return it in form of http.Response + // which is then internally handled by handleResult. + Future request() => client.get(Uri.parse('base-url/$id')); +} +``` + The cubit itself handles the things like: - emitting the corresponding state (loading, error, success, refresh), - deduplication of the requests - you can decide whether, in the event that a user triggers sending a new request before the previous one is completed, you should abort the previous one or cancel the next one. You can set the `requestMode` when you create a single cubit, or you can set it globally using [`RequestLayoutConfigProvider`](#requestlayoutconfigprovider). By default it is set to ignore the next request while previous is being processed, @@ -123,7 +136,7 @@ RequestCubitBuilder( ) ``` -As you may see `onInitial`, `onLoading` and `onError` are marked as optional parameter. In many projects each of those widgets are the same for each page. So in order to eliminate even more boilerplate code, instead of passing them all each time you want to use `RequestCubitBuilder`, you can define them globally and provider in the whole app using [`RequestLayoutConfigProvider`](#requestlayoutconfigprovider). +As you may see `onInitial`, `onLoading` and `onError` are marked as optional parameter. In many projects each of those widgets are the same for each page. So in order to eliminate even more boilerplate code, instead of passing them all each time you want to use `RequestCubitBuilder`, you can define them globally and provide in the whole app using [`RequestLayoutConfigProvider`](#requestlayoutconfigprovider). ### `RequestLayoutConfigProvider` @@ -220,7 +233,7 @@ It also takes optional `controller`, `physics` and numerous optional builders: - `nextPageLoadingBuilder` - builds a widget which is displayed under the last element of the list while next page is being fetched, - `nextPageErrorBuilder` - builds a widget which is displayed under the last element of the list if fetching the next page fails. -You can provider most of those builder globally in the whole app using [`PaginatedLayoutConfig`](#paginatedlayoutconfig). +You can provide most of these builders globally in the whole app using [`PaginatedLayoutConfig`](#paginatedlayoutconfig). ### `PaginatedCubitBuilder` `PaginatedCubitBuilder` is a widget which rebuilds itself when state of the paginated cubit changes. It takes two required parameter: @@ -262,36 +275,24 @@ In case you need a search functionality you may use the built in support in `Pag You can configure search debounce time and number of characters which needs to be inserted to start searching. In order to do it read about [Paginated Cubit Configuration](#paginated-cubit-configuration). ### Pre-request + Pre-requests allow you to perform an operation before making a request for the first page. This could be, for example, fetching available filters. #### `PreRequest` -`PreRequest` is a class that serves as an implementation of a pre-request. To utilize it, create a class that extends `PreRequest`. +`PreRequest` is a class that serves as an implementation of a pre-request. To utilize it, create an abstract base class that extends `PreRequest` and then create classes specific for each pre-request. An example base class: ```dart -class FiltersPreRequest - extends PreRequest { - FiltersPreRequest({required this.api}); - - final Api api; - - @override - Future request(PaginatedState state) { - return api.getFilters(); - } - - @override - Filters map( - String res, - PaginatedState state, - ) => - Filters.fromJson(jsonDecode(res) as Map); - +/// Base class for http pre-request use cases. +abstract class HttpPreRequest + extends PreRequest { @override - Future> run( - PaginatedState state) async { + /// This method performs the pre-request and returns the new state. + Future> run( + PaginatedState state) async { try { final result = await request(state); + if (result.statusCode == 200) { return state.copyWith( data: map(result.body, state), @@ -315,6 +316,27 @@ class FiltersPreRequest } ``` +Example implementation of `PreRequest` using defined `HttpPreRequest` looks like this: + +```dart +class FiltersPreRequest extends HttpPreRequest { + FiltersPreRequest({required this.api}); + + final Api api; + + @override + Future request(PaginatedState state) => + api.getFilters(); + + @override + Filters map( + String res, + PaginatedState state, + ) => + Filters.fromJson(jsonDecode(res) as Map); +} +``` + Then you need to create an instance of defined `FiltersPreRequest` in `PaginatedCubit` constructor. @@ -371,4 +393,4 @@ PaginatedResult, KratosIdentityDTO> { } ``` -[leancode_cubit_utils_cqrs]: https://pub.dev/packages/leancode_cubit_utils_cqrs \ No newline at end of file +[leancode_cubit_utils_cqrs]: https://pub.dev/packages/leancode_cubit_utils_cqrs diff --git a/packages/leancode_cubit_utils/example/lib/main.dart b/packages/leancode_cubit_utils/example/lib/main.dart index 4987f3d..cacbf8b 100644 --- a/packages/leancode_cubit_utils/example/lib/main.dart +++ b/packages/leancode_cubit_utils/example/lib/main.dart @@ -13,7 +13,6 @@ import 'package:provider/provider.dart'; class Routes { static const home = '/'; static const simpleRequest = '/simple-request'; - static const simpleRequestHook = '/simple-request-hook'; static const paginatedCubit = '/paginated-cubit'; } @@ -90,7 +89,6 @@ class MainApp extends StatelessWidget { routes: { Routes.home: (_) => const HomePage(), Routes.simpleRequest: (_) => const RequestScreen(), - Routes.simpleRequestHook: (_) => const RequestHookScreen(), Routes.paginatedCubit: (_) => const PaginatedCubitScreen(), }, ); diff --git a/packages/leancode_cubit_utils/example/lib/pages/common.dart b/packages/leancode_cubit_utils/example/lib/pages/common.dart new file mode 100644 index 0000000..b5213de --- /dev/null +++ b/packages/leancode_cubit_utils/example/lib/pages/common.dart @@ -0,0 +1,62 @@ +import 'package:example/http/status_codes.dart'; +import 'package:http/http.dart' as http; +import 'package:leancode_cubit_utils/leancode_cubit_utils.dart'; + +/// Base class for http request cubits. +abstract class HttpRequestCubit + extends RequestCubit { + HttpRequestCubit(super.loggerTag, {required this.client}); + + final http.Client client; + + @override + Future> handleResult( + http.Response result, + ) async { + if (result.statusCode == StatusCode.ok.value) { + logger.info('Request success. Data: ${result.body}'); + return RequestSuccessState(map(result.body)); + } else { + logger.severe('Request error. Status code: ${result.statusCode}'); + try { + return await handleError(RequestErrorState(error: result.statusCode)); + } catch (e, s) { + logger.severe( + 'Processing error failed. Exception: $e. Stack trace: $s', + ); + return RequestErrorState(exception: e, stackTrace: s); + } + } + } +} + +/// Base class for http pre-request use cases. +abstract class HttpPreRequest + extends PreRequest { + @override + Future> run( + PaginatedState state) async { + try { + final result = await request(state); + + if (result.statusCode == StatusCode.ok.value) { + return state.copyWith( + data: map(result.body, state), + preRequestSuccess: true, + ); + } else { + try { + return handleError(state.copyWithError(result.statusCode)); + } catch (e) { + return state.copyWithError(e); + } + } + } catch (e) { + try { + return handleError(state.copyWithError(e)); + } catch (e) { + return state.copyWithError(e); + } + } + } +} diff --git a/packages/leancode_cubit_utils/example/lib/pages/home_page.dart b/packages/leancode_cubit_utils/example/lib/pages/home_page.dart index 07830f9..74fd477 100644 --- a/packages/leancode_cubit_utils/example/lib/pages/home_page.dart +++ b/packages/leancode_cubit_utils/example/lib/pages/home_page.dart @@ -20,11 +20,6 @@ class HomePage extends StatelessWidget { child: const Text('Simple request page'), ), const SizedBox(height: 16), - ElevatedButton( - onPressed: () => pushNamed(Routes.simpleRequestHook), - child: const Text('Simple request hook page'), - ), - const SizedBox(height: 16), ElevatedButton( onPressed: () => pushNamed(Routes.paginatedCubit), child: const Text('Paginated cubit page'), diff --git a/packages/leancode_cubit_utils/example/lib/pages/paginated/simple_paginated_cubit.dart b/packages/leancode_cubit_utils/example/lib/pages/paginated/simple_paginated_cubit.dart index 4fc3c9f..d3dd38c 100644 --- a/packages/leancode_cubit_utils/example/lib/pages/paginated/simple_paginated_cubit.dart +++ b/packages/leancode_cubit_utils/example/lib/pages/paginated/simple_paginated_cubit.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:example/http/status_codes.dart'; +import 'package:example/pages/common.dart'; import 'package:http/http.dart' as http; import 'package:equatable/equatable.dart'; import 'package:example/pages/paginated/api.dart'; @@ -36,8 +37,7 @@ class AdditionalData with EquatableMixin { ); } -class FiltersPreRequest - extends PreRequest { +class FiltersPreRequest extends HttpPreRequest { FiltersPreRequest({ required this.api, }); @@ -64,33 +64,6 @@ class FiltersPreRequest .toSet(), ); } - - @override - Future> run( - PaginatedState state) async { - try { - final result = await request(state); - - if (result.statusCode == StatusCode.ok.value) { - return state.copyWith( - data: map(result.body, state), - preRequestSuccess: true, - ); - } else { - try { - return handleError(state.copyWithError(result.statusCode)); - } catch (e) { - return state.copyWithError(e); - } - } - } catch (e) { - try { - return handleError(state.copyWithError(e)); - } catch (e) { - return state.copyWithError(e); - } - } - } } class SimplePaginatedCubit diff --git a/packages/leancode_cubit_utils/example/lib/pages/request/request_page.dart b/packages/leancode_cubit_utils/example/lib/pages/request/request_page.dart index 28e39d4..6fba8cc 100644 --- a/packages/leancode_cubit_utils/example/lib/pages/request/request_page.dart +++ b/packages/leancode_cubit_utils/example/lib/pages/request/request_page.dart @@ -1,89 +1,21 @@ import 'dart:convert'; import 'package:example/http/client.dart'; -import 'package:example/http/status_codes.dart'; +import 'package:example/pages/common.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:http/http.dart' as http; import 'package:leancode_cubit_utils/leancode_cubit_utils.dart'; -import 'package:leancode_hooks/leancode_hooks.dart'; -class UserRequestCubit extends RequestCubit { - UserRequestCubit( - this._request, - ) : super('UserRequestCubit'); - - final Request _request; +class UserRequestCubit extends HttpRequestCubit { + UserRequestCubit({required super.client}) : super('UserRequestCubit'); @override - Future request() => _request(); + Future request() => client.get(Uri.parse('success')); @override User map(String data) => User.fromJson(jsonDecode(data) as Map); - - @override - Future> handleResult( - http.Response result, - ) async { - if (result.statusCode == StatusCode.ok.value) { - logger.info('Request success. Data: ${result.body}'); - return RequestSuccessState(map(result.body)); - } else { - logger.severe('Request error. Status code: ${result.statusCode}'); - try { - return await handleError(RequestErrorState(error: result.statusCode)); - } catch (e, s) { - logger.severe( - 'Processing error failed. Exception: $e. Stack trace: $s', - ); - return RequestErrorState(exception: e, stackTrace: s); - } - } - } -} - -class RequestHookScreen extends StatelessWidget { - const RequestHookScreen({super.key}); - - @override - Widget build(BuildContext context) => const RequestHookPage(); -} - -class RequestHookPage extends HookWidget { - const RequestHookPage({super.key}); - - @override - Widget build(BuildContext context) { - final userCubit = useBloc( - () => UserRequestCubit( - () => context.read().get(Uri.parse('success')), - )..run(), - [], - ); - - return Scaffold( - appBar: AppBar( - title: const Text('Simple request page'), - ), - body: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: RequestCubitBuilder( - cubit: userCubit, - builder: (context, data) => Text('${data.name} ${data.surname}'), - ), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: userCubit.refresh, - child: const Text('Refresh'), - ), - ], - ), - ); - } } class RequestScreen extends StatelessWidget { @@ -91,9 +23,8 @@ class RequestScreen extends StatelessWidget { @override Widget build(BuildContext context) => BlocProvider( - create: (context) => UserRequestCubit( - () => context.read().get(Uri.parse('success')), - )..run(), + create: (context) => + UserRequestCubit(client: context.read())..run(), child: const RequestPage(), ); } diff --git a/packages/leancode_cubit_utils/example/pubspec.lock b/packages/leancode_cubit_utils/example/pubspec.lock index c448d06..6b007c7 100644 --- a/packages/leancode_cubit_utils/example/pubspec.lock +++ b/packages/leancode_cubit_utils/example/pubspec.lock @@ -249,7 +249,7 @@ packages: path: ".." relative: true source: path - version: "0.0.4" + version: "0.1.0" leancode_hooks: dependency: "direct main" description: diff --git a/packages/leancode_cubit_utils_cqrs/example/pubspec.lock b/packages/leancode_cubit_utils_cqrs/example/pubspec.lock index 144ced1..894835e 100644 --- a/packages/leancode_cubit_utils_cqrs/example/pubspec.lock +++ b/packages/leancode_cubit_utils_cqrs/example/pubspec.lock @@ -257,7 +257,7 @@ packages: path: "../../leancode_cubit_utils" relative: true source: path - version: "0.0.4" + version: "0.1.0" leancode_cubit_utils_cqrs: dependency: "direct main" description: @@ -559,5 +559,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" + dart: ">=3.3.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54"