diff --git a/lib/core/designsystem/organisms/show_error_dialog.dart b/lib/core/designsystem/organisms/show_error_dialog.dart new file mode 100644 index 0000000..8a91983 --- /dev/null +++ b/lib/core/designsystem/organisms/show_error_dialog.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +import '../molecules/buttons/primary_button.dart'; + +Future showErrorDialog( + BuildContext context, + String title, + String content, + String ctaText, + Function()? onCtaPressed, +) { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text( + title, + textAlign: TextAlign.center, + ), + content: Text( + content, + textAlign: TextAlign.center, + ), + actions: [ + Center( + child: PrimaryButton( + title: ctaText, + onPressed: () { + onCtaPressed?.call(); + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + ); +} diff --git a/lib/core/di/di.dart b/lib/core/di/di.dart index 4ed5c76..d93cbb9 100644 --- a/lib/core/di/di.dart +++ b/lib/core/di/di.dart @@ -5,6 +5,6 @@ import 'di.config.dart'; final getIt = GetIt.instance; @InjectableInit() -void configureDependencies() { +Future configureDependencies() async { getIt.init(environment: 'prod'); } diff --git a/lib/layers/data/api/custom_errors.dart b/lib/layers/data/api/custom_errors.dart index 98d496d..65f56fc 100644 --- a/lib/layers/data/api/custom_errors.dart +++ b/lib/layers/data/api/custom_errors.dart @@ -3,3 +3,7 @@ import 'package:dio/dio.dart'; class UnauthorizedError extends DioException { UnauthorizedError({required super.requestOptions}); } + +class DuplicatedReadingError extends Error { + DuplicatedReadingError(); +} diff --git a/lib/layers/data/api/storage_client.dart b/lib/layers/data/api/storage_client.dart new file mode 100644 index 0000000..86c0857 --- /dev/null +++ b/lib/layers/data/api/storage_client.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:injectable/injectable.dart'; +import 'package:mibook/layers/data/api/custom_errors.dart'; +import 'package:mibook/layers/data/models/reading_data.dart'; +import 'package:path_provider/path_provider.dart'; + +abstract class IStorageClient { + Future saveReading(ReadingData readingData); + Future> getReadingList(); +} + +@Singleton(as: IStorageClient) +class StorageClient implements IStorageClient { + Future _getLocalFile(String fileName) async { + final directory = await getApplicationDocumentsDirectory(); + return File('${directory.path}/$fileName.json'); + } + + @override + Future saveReading(ReadingData readingData) async { + final file = await _getLocalFile('reading_list'); + List currentList = await getReadingList(); + + if (currentList.any((item) => item.bookId == readingData.bookId)) { + throw DuplicatedReadingError(); + } + currentList.add(readingData); + final jsonString = jsonEncode(currentList.map((e) => e.toJson()).toList()); + await file.writeAsString(jsonString); + } + + @override + Future> getReadingList() async { + final file = await _getLocalFile('reading_list'); + + if (!await file.exists()) { + return []; + } + + final jsonString = await file.readAsString(); + + if (jsonString.isEmpty) { + return []; + } + + final List decoded = jsonDecode(jsonString); + return decoded.map((e) => ReadingData.fromJson(e)).toList(); + } +} diff --git a/lib/layers/data/datasource/reading_data_source.dart b/lib/layers/data/datasource/reading_data_source.dart index 0067fb0..b38b211 100644 --- a/lib/layers/data/datasource/reading_data_source.dart +++ b/lib/layers/data/datasource/reading_data_source.dart @@ -1,4 +1,5 @@ import 'package:injectable/injectable.dart'; +import 'package:mibook/layers/data/api/storage_client.dart'; import 'package:mibook/layers/data/models/reading_data.dart'; abstract class IReadingDataSource { @@ -10,13 +11,16 @@ abstract class IReadingDataSource { @Injectable(as: IReadingDataSource) class ReadingDataSource implements IReadingDataSource { + final IStorageClient _storageClient; + + ReadingDataSource(this._storageClient); + @override Future startReading({ required ReadingData readingData, - }) async { - // TO DO - } + }) async => _storageClient.saveReading(readingData); @override - Future> getReadingData() async => []; + Future> getReadingData() async => + await _storageClient.getReadingList(); } diff --git a/lib/layers/data/models/reading_data.dart b/lib/layers/data/models/reading_data.dart index 6ad64ce..c7805f1 100644 --- a/lib/layers/data/models/reading_data.dart +++ b/lib/layers/data/models/reading_data.dart @@ -5,25 +5,49 @@ part 'reading_data.g.dart'; @JsonSerializable() class ReadingData { final String bookId; + final String bookName; + final String? bookThumb; final double progress; - ReadingData({ - required this.bookId, - required this.progress, - }); + ReadingData( + this.bookId, + this.bookName, + this.bookThumb, + this.progress, + ); factory ReadingData.fromJson(Map json) => _$ReadingDataFromJson(json); Map toJson() => _$ReadingDataToJson(this); - factory ReadingData.fromDomain(ReadingDomain domain) => ReadingData( - bookId: domain.bookId, - progress: domain.progress, + factory ReadingData.fromDomainModel(ReadingDomain domainModel) => ReadingData( + domainModel.bookId, + domainModel.bookName, + domainModel.bookThumb, + domainModel.progress, ); - ReadingDomain toDomain() => ReadingDomain( + ReadingDomain toDomainModel() => ReadingDomain( bookId: bookId, + bookName: bookName, + bookThumb: bookThumb, progress: progress, ); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ReadingData && + bookId == other.bookId && + bookName == other.bookName && + bookThumb == other.bookThumb && + progress == other.progress; + + @override + int get hashCode => + bookId.hashCode ^ + bookName.hashCode ^ + bookThumb.hashCode ^ + progress.hashCode; } diff --git a/lib/layers/data/repository/reading_repository.dart b/lib/layers/data/repository/reading_repository.dart index a225bbe..aa59180 100644 --- a/lib/layers/data/repository/reading_repository.dart +++ b/lib/layers/data/repository/reading_repository.dart @@ -4,7 +4,7 @@ import 'package:mibook/layers/data/models/reading_data.dart'; import 'package:mibook/layers/domain/models/reading_domain.dart'; import 'package:mibook/layers/domain/repository/reading_repository.dart'; -@Injectable(as: IReadingRepository) +@Singleton(as: IReadingRepository) class ReadingRepository implements IReadingRepository { final IReadingDataSource _dataSource; @@ -15,8 +15,10 @@ class ReadingRepository implements IReadingRepository { required ReadingDomain reading, }) async { final data = ReadingData( - bookId: reading.bookId, - progress: reading.progress, + reading.bookId, + reading.bookName, + reading.bookThumb, + reading.progress, ); await _dataSource.startReading(readingData: data); } @@ -24,6 +26,6 @@ class ReadingRepository implements IReadingRepository { @override Future> getReadings() async { final data = await _dataSource.getReadingData(); - return data.map((e) => e.toDomain()).toList(); + return data.map((e) => e.toDomainModel()).toList(); } } diff --git a/lib/layers/domain/models/reading_domain.dart b/lib/layers/domain/models/reading_domain.dart index 7dca4af..b026547 100644 --- a/lib/layers/domain/models/reading_domain.dart +++ b/lib/layers/domain/models/reading_domain.dart @@ -1,9 +1,13 @@ class ReadingDomain { final String bookId; + final String bookName; + final String? bookThumb; final double progress; ReadingDomain({ required this.bookId, + required this.bookName, + this.bookThumb, required this.progress, }); } diff --git a/lib/layers/presentation/screens/bookdetails/book_details_state.dart b/lib/layers/presentation/screens/bookdetails/book_details_state.dart index 841c49f..8e9e4c3 100644 --- a/lib/layers/presentation/screens/bookdetails/book_details_state.dart +++ b/lib/layers/presentation/screens/bookdetails/book_details_state.dart @@ -4,26 +4,14 @@ part 'book_details_state.freezed.dart'; @freezed class BookDetailsState with _$BookDetailsState { - @override - final bool isLoading; - @override - final String? errorMessage; - @override - final BookDetailsUI? bookDetails; - @override - final double bookProgress; + const factory BookDetailsState({ + String? errorMessage, + BookDetailsUI? bookDetails, + @Default(false) bool isLoading, + @Default(0.0) double bookProgress, + }) = _BookDetailsState; - BookDetailsState( - this.errorMessage, - this.bookDetails, { - required this.isLoading, - required this.bookProgress, - }); + const BookDetailsState._(); // allows adding custom getters or methods later - factory BookDetailsState.initial() => BookDetailsState( - null, - null, - isLoading: false, - bookProgress: 0.0, - ); + factory BookDetailsState.initial() => const BookDetailsState(); } diff --git a/lib/layers/presentation/screens/bookdetails/book_details_view_model.dart b/lib/layers/presentation/screens/bookdetails/book_details_view_model.dart index e89c06d..84a25d1 100644 --- a/lib/layers/presentation/screens/bookdetails/book_details_view_model.dart +++ b/lib/layers/presentation/screens/bookdetails/book_details_view_model.dart @@ -66,6 +66,8 @@ class BookDetailsViewModel extends Bloc { await _startReading( reading: ReadingDomain( bookId: bookId!, + bookName: state.bookDetails?.title ?? '', + bookThumb: state.bookDetails?.thumbnail ?? '', progress: progress, ), ); diff --git a/lib/layers/presentation/screens/booksearch/book_search_state.dart b/lib/layers/presentation/screens/booksearch/book_search_state.dart index 0d3cfee..90f63ac 100644 --- a/lib/layers/presentation/screens/booksearch/book_search_state.dart +++ b/lib/layers/presentation/screens/booksearch/book_search_state.dart @@ -5,34 +5,16 @@ part 'book_search_state.freezed.dart'; @freezed class BookSearchState with _$BookSearchState { - @override - final bool isLoading; - @override - final String? errorMessage; - @override - final String searchText; - @override - final List books; - @override - final bool isPageLoading; - @override - final bool canLoadNextPage; + const factory BookSearchState({ + @Default(false) bool isLoading, + String? errorMessage, + @Default('') String searchText, + @Default([]) List books, + @Default(false) bool isPageLoading, + @Default(true) bool canLoadNextPage, + }) = _BookSearchState; - BookSearchState({ - required this.isLoading, - required this.errorMessage, - required this.searchText, - required this.books, - required this.isPageLoading, - required this.canLoadNextPage, - }); + const BookSearchState._(); - factory BookSearchState.initial() => BookSearchState( - isLoading: false, - errorMessage: null, - searchText: '', - books: [], - isPageLoading: false, - canLoadNextPage: true, - ); + factory BookSearchState.initial() => const BookSearchState(); } diff --git a/lib/layers/presentation/screens/startreading/start_reading_event.dart b/lib/layers/presentation/screens/startreading/start_reading_event.dart index 013f7cb..85c314a 100644 --- a/lib/layers/presentation/screens/startreading/start_reading_event.dart +++ b/lib/layers/presentation/screens/startreading/start_reading_event.dart @@ -8,3 +8,5 @@ class DidEditProgressEvent extends StartReadingEvent { class DidClickConfirmEvent extends StartReadingEvent {} class DidClickFinishBookEvent extends StartReadingEvent {} + +class DidClickSavingErrorDismissEvent extends StartReadingEvent {} diff --git a/lib/layers/presentation/screens/startreading/start_reading_page.dart b/lib/layers/presentation/screens/startreading/start_reading_page.dart index e275027..43f4d08 100644 --- a/lib/layers/presentation/screens/startreading/start_reading_page.dart +++ b/lib/layers/presentation/screens/startreading/start_reading_page.dart @@ -7,6 +7,7 @@ import 'package:mibook/core/designsystem/molecules/buttons/secondary_button.dart import 'package:mibook/core/designsystem/molecules/indicators/progress_stepper.dart'; import 'package:mibook/core/designsystem/molecules/inputfields/input_field.dart'; import 'package:mibook/core/designsystem/organisms/app_nav_bar.dart'; +import 'package:mibook/core/designsystem/organisms/show_error_dialog.dart'; import 'package:mibook/core/di/di.dart'; import 'package:mibook/core/utils/strings.dart'; import 'package:mibook/core/utils/strings.dart' as strings; @@ -15,6 +16,9 @@ import 'package:mibook/layers/presentation/screens/startreading/start_reading_ev import 'package:mibook/layers/presentation/screens/startreading/start_reading_state.dart'; import 'package:mibook/layers/presentation/screens/startreading/start_reading_view_model.dart'; +typedef _BlocListener = BlocListener; +typedef _BlocBuilder = BlocBuilder; + @RoutePage() class StartReadingPage extends StatelessWidget { final BookDetailsUI book; @@ -36,34 +40,57 @@ class StartReadingPage extends StatelessWidget { class _StartReadingScaffold extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppNavBar( - titleText: startReading, - onBack: context.router.maybePop, - ), - body: BlocBuilder( - builder: (context, state) { - final viewModel = context.read(); - if (state.shouldNavigateBack) { - context.router.maybePop(); - } - return _StartReadingContent( - book: viewModel.book, - progress: state.progress, - errorMessage: state.inputErrorMessage, - onChangeProgressText: (progress) { - viewModel.add( - DidEditProgressEvent(progress: int.tryParse(progress) ?? 0), - ); - }, - onClickStartReading: () { - viewModel.add(DidClickConfirmEvent()); - }, - onClickFinishBook: () { - viewModel.add(DidClickFinishBookEvent()); + final viewModel = context.read(); + + return _BlocListener( + bloc: viewModel, + listenWhen: (previous, current) => + previous.shouldShowSavingError != current.shouldShowSavingError && + current.shouldShowSavingError, + listener: (context, state) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showErrorDialog( + context, + 'Error', + 'An error occurred while saving your reading progress. Please try again.', + 'OK', + () { + viewModel.add(DidClickSavingErrorDismissEvent()); }, ); - }, + }); + }, + child: Scaffold( + appBar: AppNavBar( + titleText: startReading, + onBack: context.router.maybePop, + ), + body: _BlocBuilder( + builder: (context, state) { + final viewModel = context.read(); + if (state.shouldNavigateBack) { + context.router.maybePop(); + } + return _StartReadingContent( + book: viewModel.book, + progress: state.progress, + errorMessage: state.inputErrorMessage, + onChangeProgressText: (progress) { + viewModel.add( + DidEditProgressEvent( + progress: int.tryParse(progress) ?? 0, + ), + ); + }, + onClickStartReading: () { + viewModel.add(DidClickConfirmEvent()); + }, + onClickFinishBook: () { + viewModel.add(DidClickFinishBookEvent()); + }, + ); + }, + ), ), ); } diff --git a/lib/layers/presentation/screens/startreading/start_reading_state.dart b/lib/layers/presentation/screens/startreading/start_reading_state.dart index 4036d73..255d8c9 100644 --- a/lib/layers/presentation/screens/startreading/start_reading_state.dart +++ b/lib/layers/presentation/screens/startreading/start_reading_state.dart @@ -3,22 +3,12 @@ part 'start_reading_state.freezed.dart'; @freezed class StartReadingState with _$StartReadingState { - @override - final String? inputErrorMessage; - @override - final double progress; - @override - final bool shouldNavigateBack; + const factory StartReadingState({ + String? inputErrorMessage, + @Default(0.0) double progress, + @Default(false) bool shouldNavigateBack, + @Default(false) bool shouldShowSavingError, + }) = _StartReadingState; - StartReadingState({ - required this.inputErrorMessage, - required this.progress, - required this.shouldNavigateBack, - }); - - static StartReadingState get initial => StartReadingState( - inputErrorMessage: null, - progress: 0.0, - shouldNavigateBack: false, - ); + factory StartReadingState.initial() => const StartReadingState(); } diff --git a/lib/layers/presentation/screens/startreading/start_reading_view_model.dart b/lib/layers/presentation/screens/startreading/start_reading_view_model.dart index c8f1d7e..72855af 100644 --- a/lib/layers/presentation/screens/startreading/start_reading_view_model.dart +++ b/lib/layers/presentation/screens/startreading/start_reading_view_model.dart @@ -15,7 +15,7 @@ class StartReadingViewModel extends Bloc { StartReadingViewModel( this._startReading, @factoryParam this.book, - ) : super(StartReadingState.initial) { + ) : super(StartReadingState.initial()) { // Handle DidEditProgress Event on((event, emit) { emit(_didEditProgress(event)); @@ -30,6 +30,10 @@ class StartReadingViewModel extends Bloc { final response = await _didClickFinishBook(); emit(response); }); + on((event, emit) { + final response = _handleDidClickSavingErrorDismiss(); + emit(response); + }); } // Event Handler to DidEditProgressEvent @@ -56,8 +60,21 @@ class StartReadingViewModel extends Bloc { _handleStartReading(1.0); Future _handleStartReading(double progress) async { - final reading = ReadingDomain(bookId: book.id, progress: progress); - await _startReading(reading: reading); - return state.copyWith(shouldNavigateBack: true); + final reading = ReadingDomain( + bookId: book.id, + bookName: book.title, + bookThumb: book.thumbnail, + progress: progress, + ); + try { + await _startReading(reading: reading); + return state.copyWith(shouldNavigateBack: true); + } catch (_) { + print('Error saving reading for bookId: ${book.id}'); + return state.copyWith(shouldShowSavingError: true); + } } + + StartReadingState _handleDidClickSavingErrorDismiss() => + state.copyWith(shouldShowSavingError: false); } diff --git a/lib/main.dart b/lib/main.dart index 6c3f0b5..224f0ec 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:mibook/core/routes/app_router.dart'; import 'package:mibook/core/di/di.dart'; -void main() { +void main() async { WidgetsFlutterBinding.ensureInitialized(); - configureDependencies(); + await configureDependencies(); runApp(CoreApplication()); } diff --git a/pubspec.lock b/pubspec.lock index f970ac6..6a545ad 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "85.0.0" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "7.7.1" + version: "6.4.1" args: dependency: transitive description: @@ -45,18 +45,18 @@ packages: dependency: "direct main" description: name: auto_route - sha256: "1d1bd908a1fec327719326d5d0791edd37f16caff6493c01003689fb03315ad7" + sha256: b83e8ce46da7228cdd019b5a11205454847f0a971bca59a7529b98df9876889b url: "https://pub.dev" source: hosted - version: "9.3.0+1" + version: "9.2.2" auto_route_generator: dependency: "direct main" description: name: auto_route_generator - sha256: c2e359d8932986d4d1bcad7a428143f81384ce10fef8d4aa5bc29e1f83766a46 + sha256: c9086eb07271e51b44071ad5cff34e889f3156710b964a308c2ab590769e79e6 url: "https://pub.dev" source: hosted - version: "9.3.1" + version: "9.0.0" auto_size_text: dependency: "direct main" description: @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: build - sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.4.1" build_config: dependency: transitive description: @@ -109,26 +109,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.4.2" build_runner: - dependency: "direct main" + dependency: "direct dev" description: name: build_runner - sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "9.1.2" + version: "7.3.2" built_collection: dependency: transitive description: @@ -261,26 +261,26 @@ packages: dependency: transitive description: name: dart_style - sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "2.3.6" dio: dependency: "direct main" description: name: dio - sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" url: "https://pub.dev" source: hosted - version: "5.9.0" + version: "5.7.0" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.0.0" encrypt: dependency: transitive description: @@ -377,21 +377,21 @@ packages: source: sdk version: "0.0.0" freezed: - dependency: "direct dev" + dependency: "direct main" description: name: freezed - sha256: "2d399f823b8849663744d2a9ddcce01c49268fb4170d0442a655bf6a2f47be22" + sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "2.5.2" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "2.4.4" frontend_server_client: dependency: transitive description: @@ -404,10 +404,10 @@ packages: dependency: "direct main" description: name: get_it - sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b + sha256: c49895c1ecb0ee2a0ec568d39de882e2c299ba26355aa6744ab1001f98cebd15 url: "https://pub.dev" source: hosted - version: "8.2.0" + version: "8.0.2" glob: dependency: transitive description: @@ -460,18 +460,18 @@ packages: dependency: "direct main" description: name: injectable - sha256: "1b86fab6a98c11a97e5c718afb00e628d47d183c2a2256392e995a4c561141c1" + sha256: "5e1556ea1d374fe44cbe846414d9bab346285d3d8a1da5877c01ad0774006068" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.5.0" injectable_generator: dependency: "direct dev" description: name: injectable_generator - sha256: b04673a4c88b3a848c0c77bf58b8309f9b9e064d9fe1df5450c8ee1675eaea1a + sha256: af403d76c7b18b4217335e0075e950cd0579fd7f8d7bd47ee7c85ada31680ba1 url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.6.2" io: dependency: transitive description: @@ -484,10 +484,10 @@ packages: dependency: transitive description: name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.6.7" json_annotation: dependency: "direct main" description: @@ -500,10 +500,10 @@ packages: dependency: "direct main" description: name: json_serializable - sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b url: "https://pub.dev" source: hosted - version: "6.9.5" + version: "6.8.0" leak_tracker: dependency: transitive description: @@ -588,10 +588,10 @@ packages: dependency: "direct main" description: name: mockito - sha256: "4546eac99e8967ea91bae633d2ca7698181d008e95fa4627330cf903d573277a" + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" url: "https://pub.dev" source: hosted - version: "5.4.6" + version: "5.4.4" nested: dependency: transitive description: @@ -633,7 +633,7 @@ packages: source: hosted version: "1.9.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" @@ -684,10 +684,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "7.0.1" platform: dependency: transitive description: @@ -844,10 +844,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "2.0.1" shimmer: dependency: "direct main" description: @@ -865,18 +865,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.5.0" source_helper: dependency: transitive description: name: source_helper - sha256: "4f81479fe5194a622cdd1713fe1ecb683a6e6c85cd8cec8e2e35ee5ab3fdf2a1" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.6" + version: "1.3.5" source_map_stack_trace: dependency: transitive description: @@ -1113,10 +1113,10 @@ packages: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.6.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3b33d7d..d097a30 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,32 +13,35 @@ dependencies: flutter: sdk: flutter - cupertino_icons: ^1.0.8 - flutter_lints: ^5.0.0 - get_it: ^8.0.2 - injectable: ^2.5.0 - json_serializable: ^6.9.0 - json_annotation: ^4.9.0 - build_runner: ^2.4.13 - auto_route: ^9.2.2 + cupertino_icons: 1.0.8 + flutter_lints: 5.0.0 + get_it: 8.0.2 + injectable: 2.5.0 + json_serializable: 6.8.0 + json_annotation: 4.9.0 + auto_route: 9.2.2 + cached_network_image: 3.4.1 + dio: 5.7.0 + freezed: ^2.5.2 + freezed_annotation: ^2.4.1 + encrypted_shared_preferences: 3.0.1 + flutter_bloc: 8.1.6 + mockito: 5.4.4 + shimmer: 3.0.0 + flutter_html: 3.0.0 + shared_preferences: 2.5.3 + auto_size_text: 3.0.0 + path_provider: ^2.1.5 auto_route_generator: ^9.0.0 - cached_network_image: ^3.4.1 - dio: ^5.7.0 - freezed_annotation: ^3.0.0 - encrypted_shared_preferences: ^3.0.1 - flutter_bloc: ^8.1.6 - mockito: ^5.4.6 - shimmer: ^3.0.0 - flutter_html: ^3.0.0 - shared_preferences: ^2.5.3 - auto_size_text: ^3.0.0 dev_dependencies: flutter_test: sdk: flutter + + build_runner: ^2.4.13 injectable_generator: ^2.6.2 + test: ^1.25.2 - freezed: ^3.0.3 targets: $default: diff --git a/test/layers/data/datasource/reading_data_source_test.dart b/test/layers/data/datasource/reading_data_source_test.dart new file mode 100644 index 0000000..944cfd9 --- /dev/null +++ b/test/layers/data/datasource/reading_data_source_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mibook/layers/data/api/storage_client.dart'; +import 'package:mibook/layers/data/datasource/reading_data_source.dart'; +import 'package:mibook/layers/data/models/reading_data.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; + +@GenerateNiceMocks([MockSpec()]) +import 'reading_data_source_test.mocks.dart'; + +void main() { + late MockIStorageClient storageClient; + late ReadingDataSource sut; + + setUp() { + storageClient = MockIStorageClient(); + sut = ReadingDataSource(storageClient); + } + + group('ReadingDataSource', () { + setUp(); + + test('startReading', () async { + final fakeData = ReadingData('id', 'Harry Potter', 'image', 0.5); + await sut.startReading(readingData: fakeData); + verify(storageClient.saveReading(fakeData)).called(1); + }); + + test('getReadingList', () async { + final fakeData = [ReadingData('id', 'Harry Potter', 'image', 0.5)]; + when(storageClient.getReadingList()).thenAnswer((_) async => fakeData); + final result = await sut.getReadingData(); + verify(storageClient.getReadingList()).called(1); + expect(result, fakeData); + }); + }); +} diff --git a/test/layers/data/repository/reading_repository_test.dart b/test/layers/data/repository/reading_repository_test.dart index 8fc0e0a..32fdea9 100644 --- a/test/layers/data/repository/reading_repository_test.dart +++ b/test/layers/data/repository/reading_repository_test.dart @@ -22,8 +22,17 @@ void main() { setUp(); test('startReading', () async { - final readingDomain = ReadingDomain(bookId: 'id1', progress: 0.5); - final readingData = ReadingData(bookId: 'id1', progress: 0.5); + final readingDomain = ReadingDomain( + bookId: 'id1', + bookName: 'Harry Potter', + progress: 0.5, + ); + final readingData = ReadingData( + 'id1', + 'Harry Potter', + null, + 0.5, + ); await sut.startReading(reading: readingDomain); @@ -44,8 +53,8 @@ void main() { test('getReadings', () async { final fakeData = [ - ReadingData(bookId: 'id1', progress: 0.5), - ReadingData(bookId: 'id2', progress: 0.5), + ReadingData('id1', 'Harry Potter', null, 0.5), + ReadingData('id2', 'Deltora Quest', null, 0.5), ]; when( diff --git a/test/layers/domain/search_books_test.dart b/test/layers/domain/search_books_test.dart index ab033ac..f5c294f 100644 --- a/test/layers/domain/search_books_test.dart +++ b/test/layers/domain/search_books_test.dart @@ -1,14 +1,14 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mibook/layers/domain/repository/search_repository.dart'; import 'package:mibook/layers/domain/usecases/search_books.dart'; +import 'package:mibook/layers/domain/usecases/start_reading.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'fakes/fake_book_list_domain.dart'; - -@GenerateNiceMocks([MockSpec()]) import 'search_books_test.mocks.dart'; +@GenerateNiceMocks([MockSpec(), MockSpec()]) void main() { late MockISearchRepository mockSearchRepository; late SearchBooks searchBooks; @@ -24,8 +24,8 @@ void main() { test('call returns BookListDomain on success', () async { when( mockSearchRepository.searchByTitle( - initTitle: 'initTitle', - startIndex: 0, + initTitle: anyNamed('initTitle'), + startIndex: anyNamed('startIndex'), ), ).thenAnswer((_) async => Future.value(fakeBookListDomain)); @@ -33,29 +33,40 @@ void main() { initTitle: 'initTitle', startIndex: 0, ); + verify( + mockSearchRepository.searchByTitle( + initTitle: anyNamed('initTitle'), + startIndex: anyNamed('startIndex'), + ), + ).called(1); + expect(result, fakeBookListDomain); }); - test('call returns BookListDomain on failure', () async { - when( - mockSearchRepository.searchByTitle( - initTitle: 'initTitle', - startIndex: 0, - ), - ).thenThrow(Exception('Something went wrong')); + test( + 'call throws Exception with correct message when repository fails', + () async { + when( + mockSearchRepository.searchByTitle( + initTitle: 'initTitle', + startIndex: 0, + ), + ).thenThrow(Exception('Something went wrong')); - var error = false; - try { - await searchBooks.call( - initTitle: 'initTitle', - startIndex: 0, + expect( + () => searchBooks.call( + initTitle: 'initTitle', + startIndex: 0, + ), + throwsA( + isA().having( + (e) => e.toString(), + 'message', + contains('Something went wrong'), + ), + ), ); - error = false; - } catch (e) { - error = true; - } - - expect(error, isTrue); - }); + }, + ); }); } diff --git a/test/layers/domain/start_reading_test.dart b/test/layers/domain/start_reading_test.dart index 11c7848..ccf82fc 100644 --- a/test/layers/domain/start_reading_test.dart +++ b/test/layers/domain/start_reading_test.dart @@ -21,7 +21,11 @@ void main() { setUp(); test('start', () async { - final readingDomain = ReadingDomain(bookId: 'id1', progress: 0.5); + final readingDomain = ReadingDomain( + bookId: 'id1', + bookName: 'Harry Potter', + progress: 0.5, + ); // when( // mockIReadingRepository.startReading(reading: readingDomain), // ).thenAnswer((_) async => Future.value);