diff --git a/.github/workflows/analysis.yaml b/.github/workflows/analysis.yaml index 1b8a9b8..2b42166 100644 --- a/.github/workflows/analysis.yaml +++ b/.github/workflows/analysis.yaml @@ -4,11 +4,9 @@ on: pull_request: branches: - master - - dev push: branches: - master - - dev jobs: analysis: diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index bdda7d6..41bede4 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,9 +1,6 @@ name: "Build & Release" on: - pull_request: - branches: - - master push: branches: - master diff --git a/lib/src/core/bloc/pagination_bloc/pagination_extension.dart b/lib/src/core/bloc/pagination_bloc/pagination_extension.dart index 34fff0c..f7ea027 100644 --- a/lib/src/core/bloc/pagination_bloc/pagination_extension.dart +++ b/lib/src/core/bloc/pagination_bloc/pagination_extension.dart @@ -4,23 +4,24 @@ import 'pagination_state.dart'; extension PaginationStateExtension on PaginationState { Widget when({ - required Widget Function(List data, bool isLoading) data, required Widget Function() loading, + required Widget Function(List data) data, required Widget Function(Object error) error, + required Widget Function(List data) loadingWithData, + required Widget Function(List data, dynamic error) errorWithData, }) { return AnimatedSwitcher( duration: Durations.medium3, child: switch (this) { - final PaginationLoadedDataState state => data(state.data, false), - final PaginationLoadingWithDataState state => data( + final PaginationLoadedDataState state => data(state.data), + final PaginationLoadingWithDataState state => loadingWithData( state.data, - true, ), final PaginationErrorState state => error(state.error), final PaginationLoadingState _ => loading(), - final PaginationErrorWithLoadedDataState state => data( + final PaginationErrorWithLoadedDataState state => errorWithData( state.data, - false, + state.error, ), }, ); diff --git a/lib/src/core/di/injectable.config.dart b/lib/src/core/di/injectable.config.dart index 8d4560f..50a9924 100644 --- a/lib/src/core/di/injectable.config.dart +++ b/lib/src/core/di/injectable.config.dart @@ -15,10 +15,10 @@ import 'package:lask_flutter/src/config/router/app_router.dart' as _i915; import 'package:lask_flutter/src/core/bloc/theme_mode_bloc/theme_mode_bloc.dart' as _i988; import 'package:lask_flutter/src/core/di/modules.dart' as _i239; -import 'package:lask_flutter/src/data/data_sources/remote/news/news_api.dart' - as _i105; -import 'package:lask_flutter/src/data/data_sources/remote/news/news_api_impl.dart' - as _i953; +import 'package:lask_flutter/src/data/data_sources/remote/news/news_data_source.dart' + as _i457; +import 'package:lask_flutter/src/data/data_sources/remote/news/news_data_source_impl.dart' + as _i986; import 'package:lask_flutter/src/data/repositories/news_repository_impl.dart' as _i701; import 'package:lask_flutter/src/domain/repositories/news_repository.dart' @@ -49,13 +49,13 @@ extension GetItInjectableX on _i174.GetIt { ); gh.factory<_i988.ThemeModeBloc>(() => _i988.ThemeModeBloc()); gh.lazySingleton<_i915.AppRouter>(() => _i915.AppRouter()); - gh.singleton<_i105.NewsApi>(() => _i953.NewsApiImpl()); + gh.singleton<_i457.NewsDataSource>(() => _i986.NewsDataSourceImpl()); gh.singleton<_i512.NewsRepository>( - () => _i701.NewsRepositoryImpl(gh<_i105.NewsApi>())); - gh.factory<_i775.CategoriesBloc>( - () => _i775.CategoriesBloc(gh<_i512.NewsRepository>())); + () => _i701.NewsRepositoryImpl(gh<_i457.NewsDataSource>())); gh.factory<_i587.GetNewsUseCase>( () => _i587.GetNewsUseCase(gh<_i512.NewsRepository>())); + gh.factory<_i775.CategoriesBloc>( + () => _i775.CategoriesBloc(gh<_i512.NewsRepository>())); gh.factory<_i454.NewsBloc>( () => _i454.NewsBloc(gh<_i587.GetNewsUseCase>())); return this; diff --git a/lib/src/data/data_sources/remote/news/news_api_impl.dart b/lib/src/data/data_sources/remote/news/news_api_impl.dart deleted file mode 100644 index e76f200..0000000 --- a/lib/src/data/data_sources/remote/news/news_api_impl.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'dart:math'; - -import 'package:either_dart/either.dart'; -import 'package:faker/faker.dart'; -import 'package:injectable/injectable.dart'; - -import '../../../../core/utils/exception/exception_handler.dart'; -import '../../../../domain/models/category_model/category_model.dart'; -import '../../../../domain/models/news_model/news_model.dart'; -import '../../../../domain/models/result_model/result_model.dart'; -import 'news_api.dart'; - -@Singleton(as: NewsApi) -class NewsApiImpl implements NewsApi { - @override - Future>> fetchNews({ - required int page, - required int pageSize, - Map? params, - }) async { - const totalNewsItems = 100; - - try { - await Future.delayed(const Duration(milliseconds: 500), () {}); - - final startIndex = page * pageSize; - final endIndex = min(startIndex + pageSize, totalNewsItems); - - if (startIndex >= totalNewsItems) { - return Right( - ResultModel( - data: [], - count: totalNewsItems, - ), - ); - } - - final faker = Faker(); - final newsItems = List.generate( - endIndex - startIndex, - (index) { - final randomCategory = faker.randomGenerator.element([ - 'Technology', - 'Health', - 'Sports', - 'Business', - 'Entertainment', - ]); - - return NewsModel( - title: faker.lorem.sentence(), - imageUrl: faker.image.loremPicsum(), - publishedAt: faker.date.dateTime(), - category: CategoryModel( - name: randomCategory, - id: faker.randomGenerator.integer(1000), - ), - ); - }, - ); - - return Right( - ResultModel( - data: newsItems, - count: totalNewsItems, - ), - ); - } catch (error, stackTrace) { - return Left( - ExceptionHandler.handle( - error, - stackTrace: stackTrace, - ), - ); - } - } - - @override - Future> fetchCategories() async { - try { - await Future.delayed(const Duration(milliseconds: 100), () {}); - - final faker = Faker(); - final categories = List.generate( - 5, - (index) => CategoryModel( - name: faker.randomGenerator.element([ - 'Technology', - 'Health', - 'Sports', - 'Business', - 'Entertainment', - ]), - id: faker.randomGenerator.integer(1000), - ), - ); - - return categories; - } catch (error, stackTrace) { - throw ExceptionHandler.handle( - error, - stackTrace: stackTrace, - ); - } - } -} diff --git a/lib/src/data/data_sources/remote/news/news_api.dart b/lib/src/data/data_sources/remote/news/news_data_source.dart similarity index 63% rename from lib/src/data/data_sources/remote/news/news_api.dart rename to lib/src/data/data_sources/remote/news/news_data_source.dart index 20024b9..9c45cf2 100644 --- a/lib/src/data/data_sources/remote/news/news_api.dart +++ b/lib/src/data/data_sources/remote/news/news_data_source.dart @@ -1,12 +1,9 @@ -import 'package:either_dart/either.dart'; - -import '../../../../core/utils/exception/exception_handler.dart'; import '../../../../domain/models/category_model/category_model.dart'; import '../../../../domain/models/news_model/news_model.dart'; import '../../../../domain/models/result_model/result_model.dart'; -abstract class NewsApi { - Future>> fetchNews({ +abstract class NewsDataSource { + Future> fetchNews({ required int page, required int pageSize, Map? params, diff --git a/lib/src/data/data_sources/remote/news/news_data_source_impl.dart b/lib/src/data/data_sources/remote/news/news_data_source_impl.dart new file mode 100644 index 0000000..bb5d57c --- /dev/null +++ b/lib/src/data/data_sources/remote/news/news_data_source_impl.dart @@ -0,0 +1,93 @@ +import 'dart:math'; + +import 'package:faker/faker.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../../core/utils/exception/exception_handler.dart'; +import '../../../../domain/models/category_model/category_model.dart'; +import '../../../../domain/models/news_model/news_model.dart'; +import '../../../../domain/models/result_model/result_model.dart'; +import 'news_data_source.dart'; + +@Singleton(as: NewsDataSource) +class NewsDataSourceImpl implements NewsDataSource { + final _faker = Faker(); + final _totalNewsItems = 100; + final _categories = [ + 'Technology', + 'Health', + 'Sports', + 'Business', + 'Entertainment', + ]; + + @override + Future> fetchNews({ + required int page, + required int pageSize, + Map? params, + }) async { + try { + await Future.delayed(const Duration(milliseconds: 500), () {}); + + final startIndex = page * pageSize; + final endIndex = min(startIndex + pageSize, _totalNewsItems); + + if (startIndex >= _totalNewsItems) { + return ResultModel( + data: [], + count: _totalNewsItems, + ); + } + + final newsItems = List.generate( + endIndex - startIndex, + (index) { + final randomCategory = _faker.randomGenerator.element(_categories); + + return NewsModel( + title: _faker.lorem.sentence(), + imageUrl: _faker.image.loremPicsum(), + publishedAt: _faker.date.dateTime(), + category: CategoryModel( + name: randomCategory, + id: _faker.randomGenerator.integer(1000), + ), + ); + }, + ); + + return ResultModel( + data: newsItems, + count: _totalNewsItems, + ); + } catch (error, stackTrace) { + throw ExceptionHandler.handle( + error, + stackTrace: stackTrace, + ); + } + } + + @override + Future> fetchCategories() async { + try { + await Future.delayed(const Duration(milliseconds: 100), () {}); + + final categories = List.generate( + _categories.length, + (index) => CategoryModel( + name: _faker.randomGenerator.element(_categories), + id: _faker.randomGenerator.integer(1000), + ), + ); + + return categories; + } catch (error, stackTrace) { + throw ExceptionHandler.handle( + error, + stackTrace: stackTrace, + ); + } + } +} diff --git a/lib/src/data/repositories/news_repository_impl.dart b/lib/src/data/repositories/news_repository_impl.dart index 004046c..f40315b 100644 --- a/lib/src/data/repositories/news_repository_impl.dart +++ b/lib/src/data/repositories/news_repository_impl.dart @@ -6,25 +6,31 @@ import '../../domain/models/category_model/category_model.dart'; import '../../domain/models/news_model/news_model.dart'; import '../../domain/models/result_model/result_model.dart'; import '../../domain/repositories/news_repository.dart'; -import '../data_sources/remote/news/news_api.dart'; +import '../data_sources/remote/news/news_data_source.dart'; @Singleton(as: NewsRepository) class NewsRepositoryImpl implements NewsRepository { NewsRepositoryImpl(this._newsApi); - final NewsApi _newsApi; + final NewsDataSource _newsApi; @override Future>> fetchNews({ required int page, required int pageSize, Map? params, - }) { - return _newsApi.fetchNews( - page: page, - pageSize: pageSize, - params: params, - ); + }) async { + try { + return Right( + await _newsApi.fetchNews( + page: page, + pageSize: pageSize, + params: params, + ), + ); + } on AppException catch (error) { + return Left(error); + } } @override diff --git a/lib/src/presentation/pages/explore_page/explore_page.dart b/lib/src/presentation/pages/explore_page/explore_page.dart index 5cc3f5f..892f33f 100644 --- a/lib/src/presentation/pages/explore_page/explore_page.dart +++ b/lib/src/presentation/pages/explore_page/explore_page.dart @@ -122,23 +122,38 @@ class _BuildNewsListState extends State<_BuildNewsList> @override Widget build(BuildContext context) { super.build(context); + const isFirstLarge = true; return BlocProvider( create: (context) => newsBloc..initialize(widget.category), child: BlocBuilder( builder: (context, state) { return state.when( - data: (data, isLoading) { + data: (data) { return NewsListView( news: data, - isFirstLarge: true, + isFirstLarge: isFirstLarge, onItemBuildIndex: newsBloc.onLoadItemAtIndex, ); }, loading: () => const NewsShimmerListView( - isFirstLarge: true, + isFirstLarge: isFirstLarge, ), error: ErrorWidget.new, + loadingWithData: (data) { + return NewsListView( + news: data, + isFirstLarge: isFirstLarge, + onItemBuildIndex: newsBloc.onLoadItemAtIndex, + ); + }, + errorWithData: (data, error) { + return NewsListView( + news: data, + isFirstLarge: isFirstLarge, + onItemBuildIndex: newsBloc.onLoadItemAtIndex, + ); + }, ); }, ), diff --git a/lib/src/presentation/widgets/sections/news_section.dart b/lib/src/presentation/widgets/sections/news_section.dart index d4615c0..afc9bca 100644 --- a/lib/src/presentation/widgets/sections/news_section.dart +++ b/lib/src/presentation/widgets/sections/news_section.dart @@ -35,7 +35,7 @@ class _NewsSectionState extends State { child: BlocBuilder( builder: (context, state) { return state.when( - data: (data, isLoading) { + data: (data) { return NewsListView.horizontal( news: data, onItemBuildIndex: _newsBloc.onLoadItemAtIndex, @@ -43,6 +43,18 @@ class _NewsSectionState extends State { }, loading: NewsShimmerListView.horizontal, error: ErrorWidget.new, + loadingWithData: (data) { + return NewsListView.horizontal( + news: data, + onItemBuildIndex: _newsBloc.onLoadItemAtIndex, + ); + }, + errorWithData: (data, error) { + return NewsListView.horizontal( + news: data, + onItemBuildIndex: _newsBloc.onLoadItemAtIndex, + ); + }, ); }, ), diff --git a/pubspec.yaml b/pubspec.yaml index a5b652c..826e6ca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+2 +version: 1.0.1+3 environment: sdk: ^3.5.4 @@ -28,45 +28,37 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + cached_network_image: ^3.4.1 + cupertino_icons: ^1.0.8 + dio: ^5.7.0 + easy_localization: ^3.0.7 + either_dart: ^1.0.0 + equatable: ^2.0.7 + faker: ^2.2.0 flutter: sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 - go_router: ^14.6.2 + flutter_bloc: ^8.1.6 flutter_svg: ^2.0.16 - get_it: ^8.0.3 + freezed_annotation: ^2.4.4 gap: ^3.0.1 + get_it: ^8.0.3 + go_router: ^14.6.2 + injectable: ^2.5.0 + json_annotation: ^4.9.0 provider: ^6.1.2 - cached_network_image: ^3.4.1 - shimmer: ^3.0.0 shared_preferences: ^2.3.5 - freezed_annotation: ^2.4.4 - json_annotation: ^4.9.0 - flutter_bloc: ^8.1.6 - equatable: ^2.0.7 - faker: ^2.2.0 - dio: ^5.7.0 - easy_localization: ^3.0.7 - either_dart: ^1.0.0 - injectable: ^2.5.0 + shimmer: ^3.0.0 dev_dependencies: - flutter_test: - sdk: flutter + build_runner: ^2.4.14 - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^4.0.0 - very_good_analysis: ^7.0.0 - build_runner: ^2.4.14 + flutter_test: + sdk: flutter freezed: ^2.5.8 - json_serializable: ^6.9.2 injectable_generator: ^2.7.0 + json_serializable: ^6.9.2 + very_good_analysis: ^7.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec