From b318ed250d1e8142b2ef4185da2c2a57782198c2 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Thu, 2 Oct 2025 19:08:34 -0300 Subject: [PATCH 01/11] [feat](pnalvarez): added hive package --- pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 2 files changed, 9 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index f970ac6..2ba834a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -424,6 +424,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" html: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3b33d7d..9e1c769 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: flutter_html: ^3.0.0 shared_preferences: ^2.5.3 auto_size_text: ^3.0.0 + hive: ^2.2.3 dev_dependencies: flutter_test: From 1c826679a0367276502609bad20519d3d65b8cff Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Tue, 14 Oct 2025 22:15:43 -0300 Subject: [PATCH 02/11] [feat](pedro-alvarez): save reading and hive migration --- lib/core/di/di.dart | 6 +- lib/layers/data/api/storage_client.dart | 48 +++++++++ .../data/datasource/reading_data_source.dart | 13 ++- lib/layers/data/models/reading_data.dart | 38 ++++--- lib/layers/data/models/reading_list_data.dart | 18 ++++ .../data/repository/reading_repository.dart | 12 ++- lib/layers/domain/models/reading_domain.dart | 4 + .../domain/repository/reading_repository.dart | 2 +- .../bookdetails/book_details_view_model.dart | 2 + .../start_reading_view_model.dart | 7 +- lib/main.dart | 4 +- pubspec.lock | 102 ++++++++---------- pubspec.yaml | 11 +- .../datasource/reading_data_source_test.dart | 37 +++++++ .../repository/reading_repository_test.dart | 19 +++- test/layers/domain/search_books_test.dart | 4 +- test/layers/domain/start_reading_test.dart | 6 +- 17 files changed, 235 insertions(+), 98 deletions(-) create mode 100644 lib/layers/data/api/storage_client.dart create mode 100644 lib/layers/data/models/reading_list_data.dart create mode 100644 test/layers/data/datasource/reading_data_source_test.dart diff --git a/lib/core/di/di.dart b/lib/core/di/di.dart index 4ed5c76..013e808 100644 --- a/lib/core/di/di.dart +++ b/lib/core/di/di.dart @@ -1,10 +1,14 @@ import 'package:get_it/get_it.dart'; +import 'package:hive_flutter/hive_flutter.dart'; import 'package:injectable/injectable.dart'; import 'di.config.dart'; final getIt = GetIt.instance; @InjectableInit() -void configureDependencies() { +Future configureDependencies() async { getIt.init(environment: 'prod'); + + final appBox = await Hive.openBox('AppBox'); + getIt.registerSingleton(appBox); } diff --git a/lib/layers/data/api/storage_client.dart b/lib/layers/data/api/storage_client.dart new file mode 100644 index 0000000..d851435 --- /dev/null +++ b/lib/layers/data/api/storage_client.dart @@ -0,0 +1,48 @@ +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:injectable/injectable.dart'; +import 'package:mibook/layers/data/models/reading_data.dart'; + +const _appBoxName = 'AppBox'; +const _readingListInfo = 'ReadingList'; + +abstract class IStorageClient { + Future initLocalDataSource(); + List getReadingList(); + Future saveReading(ReadingData reading); + Future get(String key); + Future clearDB(); +} + +@Injectable(as: IStorageClient) +class StorageClient implements IStorageClient { + late Box _appBox; + + StorageClient(this._appBox); + + Future _getAppBox() async => await Hive.openBox(_appBoxName); + + @override + Future initLocalDataSource() async { + await Hive.initFlutter(); + Hive.registerAdapter(ReadingDataAdapter()); + _appBox = await _getAppBox(); + } + + @override + List getReadingList() => + _appBox.get(_readingListInfo, defaultValue: []); + + @override + Future saveReading(ReadingData reading) { + var hiveList = getReadingList(); + hiveList.add(reading); + + return _appBox.put(_readingListInfo, hiveList); + } + + @override + Future clearDB() => _appBox.clear(); + + @override + Future get(String key) => _appBox.get(key); +} diff --git a/lib/layers/data/datasource/reading_data_source.dart b/lib/layers/data/datasource/reading_data_source.dart index 0067fb0..b31752e 100644 --- a/lib/layers/data/datasource/reading_data_source.dart +++ b/lib/layers/data/datasource/reading_data_source.dart @@ -1,22 +1,25 @@ 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 { Future startReading({ required ReadingData readingData, }); - Future> getReadingData(); + List getReadingData(); } @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 => []; + List getReadingData() => _storageClient.getReadingList(); } diff --git a/lib/layers/data/models/reading_data.dart b/lib/layers/data/models/reading_data.dart index 6ad64ce..27416e4 100644 --- a/lib/layers/data/models/reading_data.dart +++ b/lib/layers/data/models/reading_data.dart @@ -1,29 +1,43 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hive/hive.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:mibook/layers/domain/models/reading_domain.dart'; part 'reading_data.g.dart'; +@HiveType(typeId: 0) @JsonSerializable() -class ReadingData { - final String bookId; - final double progress; +class ReadingData extends HiveObject { + @HiveField(0) + String bookId; + @HiveField(1) + String bookName; + @HiveField(2) + String? bookThumb; + @HiveField(3) + 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, ); } diff --git a/lib/layers/data/models/reading_list_data.dart b/lib/layers/data/models/reading_list_data.dart new file mode 100644 index 0000000..720f61a --- /dev/null +++ b/lib/layers/data/models/reading_list_data.dart @@ -0,0 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hive/hive.dart'; +import 'package:mibook/layers/data/models/reading_data.dart'; + +part 'reading_list_data.g.dart'; + +@HiveType(typeId: 1) +@JsonSerializable() +class ReadingListData extends HiveObject { + @HiveField(0) + List list; + + ReadingListData(this.list); + + factory ReadingListData.fromJson(Map json) => + _$ReadingListDataFromJson(json); + Map toJson() => _$ReadingListDataToJson(this); +} diff --git a/lib/layers/data/repository/reading_repository.dart b/lib/layers/data/repository/reading_repository.dart index a225bbe..a7ee6a0 100644 --- a/lib/layers/data/repository/reading_repository.dart +++ b/lib/layers/data/repository/reading_repository.dart @@ -15,15 +15,17 @@ 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); } @override - Future> getReadings() async { - final data = await _dataSource.getReadingData(); - return data.map((e) => e.toDomain()).toList(); + List getReadings() { + final data = _dataSource.getReadingData(); + 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/domain/repository/reading_repository.dart b/lib/layers/domain/repository/reading_repository.dart index 899ae66..8c10a3b 100644 --- a/lib/layers/domain/repository/reading_repository.dart +++ b/lib/layers/domain/repository/reading_repository.dart @@ -4,5 +4,5 @@ abstract class IReadingRepository { Future startReading({ required ReadingDomain reading, }); - Future> getReadings(); + List getReadings(); } 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/startreading/start_reading_view_model.dart b/lib/layers/presentation/screens/startreading/start_reading_view_model.dart index c8f1d7e..d5e54c8 100644 --- a/lib/layers/presentation/screens/startreading/start_reading_view_model.dart +++ b/lib/layers/presentation/screens/startreading/start_reading_view_model.dart @@ -56,7 +56,12 @@ class StartReadingViewModel extends Bloc { _handleStartReading(1.0); Future _handleStartReading(double progress) async { - final reading = ReadingDomain(bookId: book.id, progress: progress); + final reading = ReadingDomain( + bookId: book.id, + bookName: book.title, + bookThumb: book.thumbnail, + progress: progress, + ); await _startReading(reading: reading); return state.copyWith(shouldNavigateBack: true); } diff --git a/lib/main.dart b/lib/main.dart index 6c3f0b5..deb15b3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:mibook/core/routes/app_router.dart'; import 'package:mibook/core/di/di.dart'; +import 'package:mibook/layers/data/api/storage_client.dart'; -void main() { +void main() async { WidgetsFlutterBinding.ensureInitialized(); configureDependencies(); + await getIt.get().initLocalDataSource(); runApp(CoreApplication()); } diff --git a/pubspec.lock b/pubspec.lock index 2ba834a..f4b0299 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: @@ -49,14 +49,6 @@ packages: url: "https://pub.dev" source: hosted version: "9.3.0+1" - auto_route_generator: - dependency: "direct main" - description: - name: auto_route_generator - sha256: c2e359d8932986d4d1bcad7a428143f81384ce10fef8d4aa5bc29e1f83766a46 - url: "https://pub.dev" - source: hosted - version: "9.3.1" auto_size_text: dependency: "direct main" description: @@ -85,10 +77,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 +101,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,10 +253,10 @@ 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: @@ -376,14 +368,6 @@ packages: description: flutter source: sdk version: "0.0.0" - freezed: - dependency: "direct dev" - description: - name: freezed - sha256: "2d399f823b8849663744d2a9ddcce01c49268fb4170d0442a655bf6a2f47be22" - url: "https://pub.dev" - source: hosted - version: "3.1.0" freezed_annotation: dependency: "direct main" description: @@ -432,6 +416,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct main" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.dev" + source: hosted + version: "2.0.1" html: dependency: transitive description: @@ -476,10 +476,10 @@ packages: 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: @@ -508,10 +508,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: @@ -596,10 +596,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: @@ -688,14 +688,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" - url: "https://pub.dev" - source: hosted - version: "6.1.0" platform: dependency: transitive description: @@ -852,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: @@ -873,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: @@ -1117,14 +1109,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" - xml: - dependency: transitive - description: - name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.dev" - source: hosted - version: "6.5.0" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9e1c769..ba22f51 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,29 +17,30 @@ dependencies: flutter_lints: ^5.0.0 get_it: ^8.0.2 injectable: ^2.5.0 - json_serializable: ^6.9.0 + json_serializable: ^6.8.0 json_annotation: ^4.9.0 - build_runner: ^2.4.13 auto_route: ^9.2.2 - 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 + mockito: ^5.4.4 # ✅ compatible with hive_generator shimmer: ^3.0.0 flutter_html: ^3.0.0 shared_preferences: ^2.5.3 auto_size_text: ^3.0.0 hive: ^2.2.3 + hive_flutter: ^1.1.0 + hive_generator: ^2.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..2ac8fc1 --- /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', () { + final fakeData = [ReadingData('id', 'Harry Potter', 'image', 0.5)]; + when(storageClient.getReadingList()).thenReturn(fakeData); + final result = 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..4baf53f 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,13 +53,13 @@ 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( mockIReadingDataSource.getReadingData(), - ).thenAnswer((_) async => fakeData); + ).thenReturn(fakeData); final response = await sut.getReadings(); diff --git a/test/layers/domain/search_books_test.dart b/test/layers/domain/search_books_test.dart index ab033ac..f99cc89 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; 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); From 249f2915f0c86e90f5d2f6ab7d786f8872aed7f1 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Tue, 21 Oct 2025 10:00:47 -0300 Subject: [PATCH 03/11] created box builder --- lib/core/di/box_builder.dart | 13 +++++++++++++ lib/core/di/di.dart | 8 +++++--- lib/layers/data/api/storage_client.dart | 17 ++++++++++------- .../data/repository/reading_repository.dart | 2 +- 4 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 lib/core/di/box_builder.dart diff --git a/lib/core/di/box_builder.dart b/lib/core/di/box_builder.dart new file mode 100644 index 0000000..c8f127b --- /dev/null +++ b/lib/core/di/box_builder.dart @@ -0,0 +1,13 @@ +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:injectable/injectable.dart'; +import 'package:mibook/layers/data/models/reading_data.dart'; + +@module +abstract class BoxBuilder { + @preResolve + Future get appBox async { + await Hive.initFlutter(); + Hive.registerAdapter(ReadingDataAdapter()); + return await Hive.openBox('AppBox'); + } +} diff --git a/lib/core/di/di.dart b/lib/core/di/di.dart index 013e808..213aaf6 100644 --- a/lib/core/di/di.dart +++ b/lib/core/di/di.dart @@ -1,14 +1,16 @@ import 'package:get_it/get_it.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:injectable/injectable.dart'; +import 'package:mibook/layers/data/api/storage_client.dart'; import 'di.config.dart'; final getIt = GetIt.instance; @InjectableInit() -Future configureDependencies() async { +void configureDependencies() async { getIt.init(environment: 'prod'); - final appBox = await Hive.openBox('AppBox'); - getIt.registerSingleton(appBox); + // final appBox = await Hive.openBox('AppBox'); + // final storageClient = StorageClient(appBox); + // getIt.registerSingleton(storageClient); } diff --git a/lib/layers/data/api/storage_client.dart b/lib/layers/data/api/storage_client.dart index d851435..26e82f9 100644 --- a/lib/layers/data/api/storage_client.dart +++ b/lib/layers/data/api/storage_client.dart @@ -1,31 +1,33 @@ import 'package:hive_flutter/hive_flutter.dart'; import 'package:injectable/injectable.dart'; +import 'package:mibook/core/di/box_builder.dart'; import 'package:mibook/layers/data/models/reading_data.dart'; const _appBoxName = 'AppBox'; const _readingListInfo = 'ReadingList'; abstract class IStorageClient { - Future initLocalDataSource(); + Future initLocalDataSource(); List getReadingList(); Future saveReading(ReadingData reading); Future get(String key); Future clearDB(); } -@Injectable(as: IStorageClient) +@LazySingleton(as: IStorageClient) class StorageClient implements IStorageClient { + final BoxBuilder _boxBuilder; late Box _appBox; - StorageClient(this._appBox); - - Future _getAppBox() async => await Hive.openBox(_appBoxName); + StorageClient(this._boxBuilder) { + _boxBuilder.appBox.then((value) => _appBox = value); + } @override - Future initLocalDataSource() async { + Future initLocalDataSource() async { + print('Init Datasource'); await Hive.initFlutter(); Hive.registerAdapter(ReadingDataAdapter()); - _appBox = await _getAppBox(); } @override @@ -37,6 +39,7 @@ class StorageClient implements IStorageClient { var hiveList = getReadingList(); hiveList.add(reading); + print('Reading saved: ${reading.bookName}'); return _appBox.put(_readingListInfo, hiveList); } diff --git a/lib/layers/data/repository/reading_repository.dart b/lib/layers/data/repository/reading_repository.dart index a7ee6a0..f244ee2 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; From ecf41cf091d0394d98266884e151a78dc212dc70 Mon Sep 17 00:00:00 2001 From: Gabriel Moro Date: Tue, 21 Oct 2025 10:15:41 -0300 Subject: [PATCH 04/11] Solve getIt issues to start the hive lazily --- lib/core/di/di.dart | 8 +------- lib/layers/data/api/storage_client.dart | 17 ++++++----------- lib/main.dart | 4 +--- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/lib/core/di/di.dart b/lib/core/di/di.dart index 213aaf6..d93cbb9 100644 --- a/lib/core/di/di.dart +++ b/lib/core/di/di.dart @@ -1,16 +1,10 @@ import 'package:get_it/get_it.dart'; -import 'package:hive_flutter/hive_flutter.dart'; import 'package:injectable/injectable.dart'; -import 'package:mibook/layers/data/api/storage_client.dart'; import 'di.config.dart'; final getIt = GetIt.instance; @InjectableInit() -void configureDependencies() async { +Future configureDependencies() async { getIt.init(environment: 'prod'); - - // final appBox = await Hive.openBox('AppBox'); - // final storageClient = StorageClient(appBox); - // getIt.registerSingleton(storageClient); } diff --git a/lib/layers/data/api/storage_client.dart b/lib/layers/data/api/storage_client.dart index 26e82f9..7363e48 100644 --- a/lib/layers/data/api/storage_client.dart +++ b/lib/layers/data/api/storage_client.dart @@ -1,9 +1,7 @@ import 'package:hive_flutter/hive_flutter.dart'; import 'package:injectable/injectable.dart'; -import 'package:mibook/core/di/box_builder.dart'; import 'package:mibook/layers/data/models/reading_data.dart'; -const _appBoxName = 'AppBox'; const _readingListInfo = 'ReadingList'; abstract class IStorageClient { @@ -16,12 +14,9 @@ abstract class IStorageClient { @LazySingleton(as: IStorageClient) class StorageClient implements IStorageClient { - final BoxBuilder _boxBuilder; - late Box _appBox; + final Box appBox; - StorageClient(this._boxBuilder) { - _boxBuilder.appBox.then((value) => _appBox = value); - } + StorageClient(this.appBox); @override Future initLocalDataSource() async { @@ -32,7 +27,7 @@ class StorageClient implements IStorageClient { @override List getReadingList() => - _appBox.get(_readingListInfo, defaultValue: []); + appBox.get(_readingListInfo, defaultValue: []); @override Future saveReading(ReadingData reading) { @@ -40,12 +35,12 @@ class StorageClient implements IStorageClient { hiveList.add(reading); print('Reading saved: ${reading.bookName}'); - return _appBox.put(_readingListInfo, hiveList); + return appBox.put(_readingListInfo, hiveList); } @override - Future clearDB() => _appBox.clear(); + Future clearDB() => appBox.clear(); @override - Future get(String key) => _appBox.get(key); + Future get(String key) => appBox.get(key); } diff --git a/lib/main.dart b/lib/main.dart index deb15b3..224f0ec 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,10 @@ import 'package:flutter/material.dart'; import 'package:mibook/core/routes/app_router.dart'; import 'package:mibook/core/di/di.dart'; -import 'package:mibook/layers/data/api/storage_client.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - configureDependencies(); - await getIt.get().initLocalDataSource(); + await configureDependencies(); runApp(CoreApplication()); } From d805fb6f3e65b366cf4c5019eda71be9b8879597 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Fri, 24 Oct 2025 09:09:46 -0300 Subject: [PATCH 05/11] [feat](pnalvarez): saving reading data on hive --- .../organisms/show_error_dialog.dart | 32 +++++++++++ lib/core/di/box_builder.dart | 2 + lib/layers/data/api/custom_errors.dart | 4 ++ lib/layers/data/api/storage_client.dart | 26 ++++----- test/layers/domain/search_books_test.dart | 53 +++++++++++-------- 5 files changed, 84 insertions(+), 33 deletions(-) create mode 100644 lib/core/designsystem/organisms/show_error_dialog.dart 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..511c6ca --- /dev/null +++ b/lib/core/designsystem/organisms/show_error_dialog.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +import '../molecules/buttons/primary_button.dart'; + +Future showErrorDialog( + BuildContext context, + String title, + String content, + String ctaText, +) { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text( + title, + textAlign: TextAlign.center, + ), + content: Text( + content, + textAlign: TextAlign.center, + ), + actions: [ + Center( + child: PrimaryButton( + title: title, + onPressed: () => Navigator.of(context).pop(), + ), + ), + ], + ), + ); +} diff --git a/lib/core/di/box_builder.dart b/lib/core/di/box_builder.dart index c8f127b..735730f 100644 --- a/lib/core/di/box_builder.dart +++ b/lib/core/di/box_builder.dart @@ -1,6 +1,7 @@ import 'package:hive_flutter/hive_flutter.dart'; import 'package:injectable/injectable.dart'; import 'package:mibook/layers/data/models/reading_data.dart'; +import 'package:mibook/layers/data/models/reading_list_data.dart'; @module abstract class BoxBuilder { @@ -8,6 +9,7 @@ abstract class BoxBuilder { Future get appBox async { await Hive.initFlutter(); Hive.registerAdapter(ReadingDataAdapter()); + Hive.registerAdapter(ReadingListDataAdapter()); return await Hive.openBox('AppBox'); } } 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 index 7363e48..3fdfe7a 100644 --- a/lib/layers/data/api/storage_client.dart +++ b/lib/layers/data/api/storage_client.dart @@ -1,11 +1,12 @@ import 'package:hive_flutter/hive_flutter.dart'; 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:mibook/layers/data/models/reading_list_data.dart'; const _readingListInfo = 'ReadingList'; abstract class IStorageClient { - Future initLocalDataSource(); List getReadingList(); Future saveReading(ReadingData reading); Future get(String key); @@ -19,23 +20,24 @@ class StorageClient implements IStorageClient { StorageClient(this.appBox); @override - Future initLocalDataSource() async { - print('Init Datasource'); - await Hive.initFlutter(); - Hive.registerAdapter(ReadingDataAdapter()); - } + List getReadingList() { + ReadingListData readingListData = appBox.get( + _readingListInfo, + defaultValue: ReadingListData([]), + ); - @override - List getReadingList() => - appBox.get(_readingListInfo, defaultValue: []); + return readingListData.list; + } @override Future saveReading(ReadingData reading) { var hiveList = getReadingList(); + if (hiveList.any((element) => element.bookId == reading.bookId)) { + throw DuplicatedReadingError(); + } hiveList.add(reading); - - print('Reading saved: ${reading.bookName}'); - return appBox.put(_readingListInfo, hiveList); + final readingListData = ReadingListData(hiveList); + return appBox.put(_readingListInfo, readingListData); } @override diff --git a/test/layers/domain/search_books_test.dart b/test/layers/domain/search_books_test.dart index f99cc89..f5c294f 100644 --- a/test/layers/domain/search_books_test.dart +++ b/test/layers/domain/search_books_test.dart @@ -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); - }); + }, + ); }); } From 07ad590aab87fccb5a744dd368258353914345ff Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Mon, 27 Oct 2025 22:22:47 -0300 Subject: [PATCH 06/11] [test](pnalvarez): unit tests for storage client --- lib/layers/data/models/reading_data.dart | 19 +++- lib/layers/data/models/reading_list_data.dart | 10 ++ test/layers/data/api/storage_client_test.dart | 103 ++++++++++++++++++ 3 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 test/layers/data/api/storage_client_test.dart diff --git a/lib/layers/data/models/reading_data.dart b/lib/layers/data/models/reading_data.dart index 27416e4..38ab564 100644 --- a/lib/layers/data/models/reading_data.dart +++ b/lib/layers/data/models/reading_data.dart @@ -1,10 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hive/hive.dart'; -import 'package:json_annotation/json_annotation.dart'; import 'package:mibook/layers/domain/models/reading_domain.dart'; part 'reading_data.g.dart'; @HiveType(typeId: 0) @JsonSerializable() +@freezed class ReadingData extends HiveObject { @HiveField(0) String bookId; @@ -40,4 +41,20 @@ class ReadingData extends HiveObject { 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/models/reading_list_data.dart b/lib/layers/data/models/reading_list_data.dart index 720f61a..ded7421 100644 --- a/lib/layers/data/models/reading_list_data.dart +++ b/lib/layers/data/models/reading_list_data.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hive/hive.dart'; import 'package:mibook/layers/data/models/reading_data.dart'; @@ -6,6 +7,7 @@ part 'reading_list_data.g.dart'; @HiveType(typeId: 1) @JsonSerializable() +@freezed class ReadingListData extends HiveObject { @HiveField(0) List list; @@ -15,4 +17,12 @@ class ReadingListData extends HiveObject { factory ReadingListData.fromJson(Map json) => _$ReadingListDataFromJson(json); Map toJson() => _$ReadingListDataToJson(this); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ReadingListData && listEquals(list, other.list); + + @override + int get hashCode => list.hashCode; } diff --git a/test/layers/data/api/storage_client_test.dart b/test/layers/data/api/storage_client_test.dart new file mode 100644 index 0000000..f5be88a --- /dev/null +++ b/test/layers/data/api/storage_client_test.dart @@ -0,0 +1,103 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:mibook/layers/data/api/custom_errors.dart'; +import 'package:mibook/layers/data/api/storage_client.dart'; +import 'package:mibook/layers/data/models/reading_data.dart'; +import 'package:mibook/layers/data/models/reading_list_data.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; + +@GenerateNiceMocks([MockSpec()]) +import 'storage_client_test.mocks.dart'; + +void main() { + late MockBox appBox; + late StorageClient sut; + + setUp() { + appBox = MockBox(); + sut = StorageClient(appBox); + } + + group('StorageClient', () { + setUp(); + + test( + 'given box returns reading list, when getReadingList, should return list', + () { + when( + appBox.get('ReadingList', defaultValue: anyNamed('defaultValue')), + ).thenReturn( + ReadingListData( + [ + ReadingData('bookId', 'bookName', 'bookThumb', 0.5), + ], + ), + ); + + final result = sut.getReadingList(); + + verify( + appBox.get('ReadingList', defaultValue: anyNamed('defaultValue')), + ).called(1); + + expect( + result, + [ + ReadingData('bookId', 'bookName', 'bookThumb', 0.5), + ], + ); + }, + ); + + test( + 'given appBox contains reading, when saveReading, should throw duplicated reading error', + () async { + when( + appBox.get(any, defaultValue: anyNamed('defaultValue')), + ).thenReturn( + ReadingListData([ + ReadingData('bookId', 'bookName', 'bookThumb', 0.5), + ]), + ); + + expectLater( + () => sut.saveReading( + ReadingData('bookId', 'bookName', 'bookThumb', 0.5), + ), + throwsA(isA()), + ); + }, + ); + + test( + 'given appBox does not contain reading, when saveReading, should save reading', + () async { + when( + appBox.get(any, defaultValue: anyNamed('defaultValue')), + ).thenReturn( + ReadingListData([]), + ); + + await sut.saveReading( + ReadingData('bookId', 'bookName', 'bookThumb', 0.5), + ); + + verify( + appBox.put( + 'ReadingList', + ReadingListData([ + ReadingData('bookId', 'bookName', 'bookThumb', 0.5), + ]), + ), + ).called(1); + }, + ); + + test('clearDB', () async { + await sut.clearDB(); + + verify(appBox.clear()).called(1); + }); + }); +} From ebdfc08cf463253bec96fecc902719d5969abf2c Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Mon, 27 Oct 2025 22:59:40 -0300 Subject: [PATCH 07/11] [fix](pnalvarez): test --- test/layers/domain/start_reading_test.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/layers/domain/start_reading_test.dart b/test/layers/domain/start_reading_test.dart index ccf82fc..3cbc425 100644 --- a/test/layers/domain/start_reading_test.dart +++ b/test/layers/domain/start_reading_test.dart @@ -26,9 +26,6 @@ void main() { bookName: 'Harry Potter', progress: 0.5, ); - // when( - // mockIReadingRepository.startReading(reading: readingDomain), - // ).thenAnswer((_) async => Future.value); await sut(reading: readingDomain); verify( mockIReadingRepository.startReading( From 0e4d96c25109fda750b0073c699e8637e446a3c6 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Mon, 27 Oct 2025 23:08:04 -0300 Subject: [PATCH 08/11] [fix](pnalvarez): test --- test/layers/domain/start_reading_test.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/layers/domain/start_reading_test.dart b/test/layers/domain/start_reading_test.dart index 3cbc425..ccf82fc 100644 --- a/test/layers/domain/start_reading_test.dart +++ b/test/layers/domain/start_reading_test.dart @@ -26,6 +26,9 @@ void main() { bookName: 'Harry Potter', progress: 0.5, ); + // when( + // mockIReadingRepository.startReading(reading: readingDomain), + // ).thenAnswer((_) async => Future.value); await sut(reading: readingDomain); verify( mockIReadingRepository.startReading( From ee6d7b2a4661fa00d9d92dbab5bd807ec8423d9c Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:55:52 -0300 Subject: [PATCH 09/11] [feat](pnalvarez): removed objectbox and saving data as json files --- lib/core/di/box_builder.dart | 15 --- lib/layers/data/api/storage_client.dart | 63 ++++++----- .../data/datasource/reading_data_source.dart | 5 +- lib/layers/data/models/reading_data.dart | 16 +-- lib/layers/data/models/reading_list_data.dart | 5 +- .../data/repository/reading_repository.dart | 4 +- .../domain/repository/reading_repository.dart | 2 +- pubspec.lock | 54 +++------ pubspec.yaml | 39 ++++--- test/layers/data/api/storage_client_test.dart | 103 ------------------ .../datasource/reading_data_source_test.dart | 6 +- .../repository/reading_repository_test.dart | 2 +- 12 files changed, 83 insertions(+), 231 deletions(-) delete mode 100644 lib/core/di/box_builder.dart delete mode 100644 test/layers/data/api/storage_client_test.dart diff --git a/lib/core/di/box_builder.dart b/lib/core/di/box_builder.dart deleted file mode 100644 index 735730f..0000000 --- a/lib/core/di/box_builder.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:injectable/injectable.dart'; -import 'package:mibook/layers/data/models/reading_data.dart'; -import 'package:mibook/layers/data/models/reading_list_data.dart'; - -@module -abstract class BoxBuilder { - @preResolve - Future get appBox async { - await Hive.initFlutter(); - Hive.registerAdapter(ReadingDataAdapter()); - Hive.registerAdapter(ReadingListDataAdapter()); - return await Hive.openBox('AppBox'); - } -} diff --git a/lib/layers/data/api/storage_client.dart b/lib/layers/data/api/storage_client.dart index 3fdfe7a..86c0857 100644 --- a/lib/layers/data/api/storage_client.dart +++ b/lib/layers/data/api/storage_client.dart @@ -1,48 +1,51 @@ -import 'package:hive_flutter/hive_flutter.dart'; +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:mibook/layers/data/models/reading_list_data.dart'; - -const _readingListInfo = 'ReadingList'; +import 'package:path_provider/path_provider.dart'; abstract class IStorageClient { - List getReadingList(); - Future saveReading(ReadingData reading); - Future get(String key); - Future clearDB(); + Future saveReading(ReadingData readingData); + Future> getReadingList(); } -@LazySingleton(as: IStorageClient) +@Singleton(as: IStorageClient) class StorageClient implements IStorageClient { - final Box appBox; - - StorageClient(this.appBox); - - @override - List getReadingList() { - ReadingListData readingListData = appBox.get( - _readingListInfo, - defaultValue: ReadingListData([]), - ); - - return readingListData.list; + Future _getLocalFile(String fileName) async { + final directory = await getApplicationDocumentsDirectory(); + return File('${directory.path}/$fileName.json'); } @override - Future saveReading(ReadingData reading) { - var hiveList = getReadingList(); - if (hiveList.any((element) => element.bookId == reading.bookId)) { + 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(); } - hiveList.add(reading); - final readingListData = ReadingListData(hiveList); - return appBox.put(_readingListInfo, readingListData); + currentList.add(readingData); + final jsonString = jsonEncode(currentList.map((e) => e.toJson()).toList()); + await file.writeAsString(jsonString); } @override - Future clearDB() => appBox.clear(); + Future> getReadingList() async { + final file = await _getLocalFile('reading_list'); - @override - Future get(String key) => appBox.get(key); + 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 b31752e..b38b211 100644 --- a/lib/layers/data/datasource/reading_data_source.dart +++ b/lib/layers/data/datasource/reading_data_source.dart @@ -6,7 +6,7 @@ abstract class IReadingDataSource { Future startReading({ required ReadingData readingData, }); - List getReadingData(); + Future> getReadingData(); } @Injectable(as: IReadingDataSource) @@ -21,5 +21,6 @@ class ReadingDataSource implements IReadingDataSource { }) async => _storageClient.saveReading(readingData); @override - List getReadingData() => _storageClient.getReadingList(); + 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 38ab564..1e0f9e3 100644 --- a/lib/layers/data/models/reading_data.dart +++ b/lib/layers/data/models/reading_data.dart @@ -1,20 +1,14 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hive/hive.dart'; import 'package:mibook/layers/domain/models/reading_domain.dart'; part 'reading_data.g.dart'; -@HiveType(typeId: 0) @JsonSerializable() @freezed -class ReadingData extends HiveObject { - @HiveField(0) - String bookId; - @HiveField(1) - String bookName; - @HiveField(2) - String? bookThumb; - @HiveField(3) - double progress; +class ReadingData { + final String bookId; + final String bookName; + final String? bookThumb; + final double progress; ReadingData( this.bookId, diff --git a/lib/layers/data/models/reading_list_data.dart b/lib/layers/data/models/reading_list_data.dart index ded7421..3318127 100644 --- a/lib/layers/data/models/reading_list_data.dart +++ b/lib/layers/data/models/reading_list_data.dart @@ -1,15 +1,12 @@ import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hive/hive.dart'; import 'package:mibook/layers/data/models/reading_data.dart'; part 'reading_list_data.g.dart'; -@HiveType(typeId: 1) @JsonSerializable() @freezed -class ReadingListData extends HiveObject { - @HiveField(0) +class ReadingListData { List list; ReadingListData(this.list); diff --git a/lib/layers/data/repository/reading_repository.dart b/lib/layers/data/repository/reading_repository.dart index f244ee2..aa59180 100644 --- a/lib/layers/data/repository/reading_repository.dart +++ b/lib/layers/data/repository/reading_repository.dart @@ -24,8 +24,8 @@ class ReadingRepository implements IReadingRepository { } @override - List getReadings() { - final data = _dataSource.getReadingData(); + Future> getReadings() async { + final data = await _dataSource.getReadingData(); return data.map((e) => e.toDomainModel()).toList(); } } diff --git a/lib/layers/domain/repository/reading_repository.dart b/lib/layers/domain/repository/reading_repository.dart index 8c10a3b..899ae66 100644 --- a/lib/layers/domain/repository/reading_repository.dart +++ b/lib/layers/domain/repository/reading_repository.dart @@ -4,5 +4,5 @@ abstract class IReadingRepository { Future startReading({ required ReadingDomain reading, }); - List getReadings(); + Future> getReadings(); } diff --git a/pubspec.lock b/pubspec.lock index f4b0299..53fa62c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ 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_size_text: dependency: "direct main" description: @@ -261,18 +261,18 @@ packages: 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: @@ -372,10 +372,10 @@ packages: dependency: "direct main" description: name: freezed_annotation - sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.0.0" frontend_server_client: dependency: transitive description: @@ -388,10 +388,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: @@ -408,30 +408,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - hive: - dependency: "direct main" - description: - name: hive - sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" - url: "https://pub.dev" - source: hosted - version: "2.2.3" - hive_flutter: - dependency: "direct main" - description: - name: hive_flutter - sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc - url: "https://pub.dev" - source: hosted - version: "1.1.0" - hive_generator: - dependency: "direct main" - description: - name: hive_generator - sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" - url: "https://pub.dev" - source: hosted - version: "2.0.1" html: dependency: transitive description: @@ -468,10 +444,10 @@ 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: @@ -492,10 +468,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: @@ -641,7 +617,7 @@ packages: source: hosted version: "1.9.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" diff --git a/pubspec.yaml b/pubspec.yaml index ba22f51..43c92de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,26 +13,24 @@ 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.8.0 - json_annotation: ^4.9.0 - auto_route: ^9.2.2 - 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.4 # ✅ compatible with hive_generator - shimmer: ^3.0.0 - flutter_html: ^3.0.0 - shared_preferences: ^2.5.3 - auto_size_text: ^3.0.0 - hive: ^2.2.3 - hive_flutter: ^1.1.0 - hive_generator: ^2.0.0 + 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_annotation: 3.0.0 + 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 dev_dependencies: flutter_test: @@ -40,6 +38,7 @@ dev_dependencies: build_runner: ^2.4.13 injectable_generator: ^2.6.2 + test: ^1.25.2 targets: diff --git a/test/layers/data/api/storage_client_test.dart b/test/layers/data/api/storage_client_test.dart deleted file mode 100644 index f5be88a..0000000 --- a/test/layers/data/api/storage_client_test.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:mibook/layers/data/api/custom_errors.dart'; -import 'package:mibook/layers/data/api/storage_client.dart'; -import 'package:mibook/layers/data/models/reading_data.dart'; -import 'package:mibook/layers/data/models/reading_list_data.dart'; -import 'package:mockito/mockito.dart'; -import 'package:mockito/annotations.dart'; - -@GenerateNiceMocks([MockSpec()]) -import 'storage_client_test.mocks.dart'; - -void main() { - late MockBox appBox; - late StorageClient sut; - - setUp() { - appBox = MockBox(); - sut = StorageClient(appBox); - } - - group('StorageClient', () { - setUp(); - - test( - 'given box returns reading list, when getReadingList, should return list', - () { - when( - appBox.get('ReadingList', defaultValue: anyNamed('defaultValue')), - ).thenReturn( - ReadingListData( - [ - ReadingData('bookId', 'bookName', 'bookThumb', 0.5), - ], - ), - ); - - final result = sut.getReadingList(); - - verify( - appBox.get('ReadingList', defaultValue: anyNamed('defaultValue')), - ).called(1); - - expect( - result, - [ - ReadingData('bookId', 'bookName', 'bookThumb', 0.5), - ], - ); - }, - ); - - test( - 'given appBox contains reading, when saveReading, should throw duplicated reading error', - () async { - when( - appBox.get(any, defaultValue: anyNamed('defaultValue')), - ).thenReturn( - ReadingListData([ - ReadingData('bookId', 'bookName', 'bookThumb', 0.5), - ]), - ); - - expectLater( - () => sut.saveReading( - ReadingData('bookId', 'bookName', 'bookThumb', 0.5), - ), - throwsA(isA()), - ); - }, - ); - - test( - 'given appBox does not contain reading, when saveReading, should save reading', - () async { - when( - appBox.get(any, defaultValue: anyNamed('defaultValue')), - ).thenReturn( - ReadingListData([]), - ); - - await sut.saveReading( - ReadingData('bookId', 'bookName', 'bookThumb', 0.5), - ); - - verify( - appBox.put( - 'ReadingList', - ReadingListData([ - ReadingData('bookId', 'bookName', 'bookThumb', 0.5), - ]), - ), - ).called(1); - }, - ); - - test('clearDB', () async { - await sut.clearDB(); - - verify(appBox.clear()).called(1); - }); - }); -} diff --git a/test/layers/data/datasource/reading_data_source_test.dart b/test/layers/data/datasource/reading_data_source_test.dart index 2ac8fc1..944cfd9 100644 --- a/test/layers/data/datasource/reading_data_source_test.dart +++ b/test/layers/data/datasource/reading_data_source_test.dart @@ -26,10 +26,10 @@ void main() { verify(storageClient.saveReading(fakeData)).called(1); }); - test('getReadingList', () { + test('getReadingList', () async { final fakeData = [ReadingData('id', 'Harry Potter', 'image', 0.5)]; - when(storageClient.getReadingList()).thenReturn(fakeData); - final result = sut.getReadingData(); + 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 4baf53f..32fdea9 100644 --- a/test/layers/data/repository/reading_repository_test.dart +++ b/test/layers/data/repository/reading_repository_test.dart @@ -59,7 +59,7 @@ void main() { when( mockIReadingDataSource.getReadingData(), - ).thenReturn(fakeData); + ).thenAnswer((_) async => fakeData); final response = await sut.getReadings(); From d37dc3c77adaa39e904662ca1c6a6245dde203b1 Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:37:45 -0300 Subject: [PATCH 10/11] [fix](pnalvarez): fixed tests --- lib/layers/data/models/reading_data.dart | 1 - lib/layers/data/models/reading_list_data.dart | 25 ------------ .../bookdetails/book_details_state.dart | 28 ++++---------- .../screens/booksearch/book_search_state.dart | 38 +++++-------------- .../startreading/start_reading_state.dart | 24 ++++-------- .../start_reading_view_model.dart | 2 +- pubspec.lock | 12 +++++- pubspec.yaml | 3 +- 8 files changed, 38 insertions(+), 95 deletions(-) delete mode 100644 lib/layers/data/models/reading_list_data.dart diff --git a/lib/layers/data/models/reading_data.dart b/lib/layers/data/models/reading_data.dart index 1e0f9e3..c7805f1 100644 --- a/lib/layers/data/models/reading_data.dart +++ b/lib/layers/data/models/reading_data.dart @@ -3,7 +3,6 @@ import 'package:mibook/layers/domain/models/reading_domain.dart'; part 'reading_data.g.dart'; @JsonSerializable() -@freezed class ReadingData { final String bookId; final String bookName; diff --git a/lib/layers/data/models/reading_list_data.dart b/lib/layers/data/models/reading_list_data.dart deleted file mode 100644 index 3318127..0000000 --- a/lib/layers/data/models/reading_list_data.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:mibook/layers/data/models/reading_data.dart'; - -part 'reading_list_data.g.dart'; - -@JsonSerializable() -@freezed -class ReadingListData { - List list; - - ReadingListData(this.list); - - factory ReadingListData.fromJson(Map json) => - _$ReadingListDataFromJson(json); - Map toJson() => _$ReadingListDataToJson(this); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ReadingListData && listEquals(list, other.list); - - @override - int get hashCode => list.hashCode; -} 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/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_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 d5e54c8..989f622 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)); diff --git a/pubspec.lock b/pubspec.lock index 53fa62c..d6c8665 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -368,14 +368,22 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed: + dependency: "direct main" + description: + name: freezed + sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 + url: "https://pub.dev" + source: hosted + version: "2.5.2" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "2.4.4" frontend_server_client: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 43c92de..123090a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,8 @@ dependencies: auto_route: 9.2.2 cached_network_image: 3.4.1 dio: 5.7.0 - freezed_annotation: 3.0.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 From bc7ff68cd0d54c7c93705155796ff037066d332a Mon Sep 17 00:00:00 2001 From: Pedro Alvarez <14023675+pnalvarez@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:47:14 -0300 Subject: [PATCH 11/11] [fix](pnalvarez): using block listener to answer to storage saving errors --- .../organisms/show_error_dialog.dart | 8 +- .../startreading/start_reading_event.dart | 2 + .../startreading/start_reading_page.dart | 79 +++++++++++++------ .../start_reading_view_model.dart | 16 +++- pubspec.lock | 24 ++++++ pubspec.yaml | 1 + 6 files changed, 100 insertions(+), 30 deletions(-) diff --git a/lib/core/designsystem/organisms/show_error_dialog.dart b/lib/core/designsystem/organisms/show_error_dialog.dart index 511c6ca..8a91983 100644 --- a/lib/core/designsystem/organisms/show_error_dialog.dart +++ b/lib/core/designsystem/organisms/show_error_dialog.dart @@ -7,6 +7,7 @@ Future showErrorDialog( String title, String content, String ctaText, + Function()? onCtaPressed, ) { return showDialog( context: context, @@ -22,8 +23,11 @@ Future showErrorDialog( actions: [ Center( child: PrimaryButton( - title: title, - onPressed: () => Navigator.of(context).pop(), + title: ctaText, + onPressed: () { + onCtaPressed?.call(); + Navigator.of(context).pop(); + }, ), ), ], 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_view_model.dart b/lib/layers/presentation/screens/startreading/start_reading_view_model.dart index 989f622..72855af 100644 --- a/lib/layers/presentation/screens/startreading/start_reading_view_model.dart +++ b/lib/layers/presentation/screens/startreading/start_reading_view_model.dart @@ -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 @@ -62,7 +66,15 @@ class StartReadingViewModel extends Bloc { bookThumb: book.thumbnail, progress: progress, ); - await _startReading(reading: reading); - return state.copyWith(shouldNavigateBack: true); + 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/pubspec.lock b/pubspec.lock index d6c8665..6a545ad 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.2.2" + auto_route_generator: + dependency: "direct main" + description: + name: auto_route_generator + sha256: c9086eb07271e51b44071ad5cff34e889f3156710b964a308c2ab590769e79e6 + url: "https://pub.dev" + source: hosted + version: "9.0.0" auto_size_text: dependency: "direct main" description: @@ -672,6 +680,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" platform: dependency: transitive description: @@ -1093,6 +1109,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 123090a..d097a30 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: shared_preferences: 2.5.3 auto_size_text: 3.0.0 path_provider: ^2.1.5 + auto_route_generator: ^9.0.0 dev_dependencies: flutter_test: