diff --git a/lib/main.dart b/lib/main.dart index 3b1d629..667f9e4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod_clean_architecture/src/presentation/app.dart'; void main() { - runApp(const App()); + runApp(const ProviderScope(child: App())); } diff --git a/lib/src/data/repository/post/post_repository.dart b/lib/src/data/repository/post/post_repository.dart index 58b6f93..8dc2bda 100644 --- a/lib/src/data/repository/post/post_repository.dart +++ b/lib/src/data/repository/post/post_repository.dart @@ -1,5 +1,7 @@ part of '../repository.dart'; abstract class PostRepository { - Future> getPost({required int start, int limit = 20}); + Future> getPostList({required int start, int limit = 20}); + + Future getPostDetail({required int id}); } diff --git a/lib/src/data/repository/post/post_repository_impl.dart b/lib/src/data/repository/post/post_repository_impl.dart index cb5c2be..7cf3bdf 100644 --- a/lib/src/data/repository/post/post_repository_impl.dart +++ b/lib/src/data/repository/post/post_repository_impl.dart @@ -6,8 +6,12 @@ class PostRepositoryImpl implements PostRepository { final PostDataSource _source; @override - Future> getPost({required int start, int limit = 20}) => - _source.getPost(start: start, limit: limit); + Future> getPostList({required int start, int limit = 20}) => + _source.getPostList(start: start, limit: limit); + + @override + Future getPostDetail({required int id}) => + _source.getPostDetail(id: id); } @riverpod diff --git a/lib/src/data/repository/repository.dart b/lib/src/data/repository/repository.dart index 50d0001..7187357 100644 --- a/lib/src/data/repository/repository.dart +++ b/lib/src/data/repository/repository.dart @@ -6,3 +6,5 @@ part 'repository.g.dart'; part 'post/post_repository.dart'; part 'post/post_repository_impl.dart'; +part 'user/user_repository.dart'; +part 'user/user_repository_impl.dart'; diff --git a/lib/src/data/repository/repository.g.dart b/lib/src/data/repository/repository.g.dart index 1b42428..b64d000 100644 --- a/lib/src/data/repository/repository.g.dart +++ b/lib/src/data/repository/repository.g.dart @@ -21,5 +21,20 @@ final postRepositoryProvider = AutoDisposeProvider.internal( ); typedef PostRepositoryRef = AutoDisposeProviderRef; +String _$userRepositoryHash() => r'6e94e4e3533ecb3c90dc15e03b51e4fb672bc524'; + +/// See also [userRepository]. +@ProviderFor(userRepository) +final userRepositoryProvider = AutoDisposeProvider.internal( + userRepository, + name: r'userRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$userRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef UserRepositoryRef = AutoDisposeProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/data/repository/user/user_repository.dart b/lib/src/data/repository/user/user_repository.dart new file mode 100644 index 0000000..3f63287 --- /dev/null +++ b/lib/src/data/repository/user/user_repository.dart @@ -0,0 +1,5 @@ +part of '../repository.dart'; + +abstract class UserRepository { + Future getUserDetail({required int id}); +} diff --git a/lib/src/data/repository/user/user_repository_impl.dart b/lib/src/data/repository/user/user_repository_impl.dart new file mode 100644 index 0000000..c5c9ed6 --- /dev/null +++ b/lib/src/data/repository/user/user_repository_impl.dart @@ -0,0 +1,17 @@ +part of '../repository.dart'; + +class UserRepositoryImpl implements UserRepository { + UserRepositoryImpl({required UserDataSource source}) : _source = source; + + final UserDataSource _source; + + @override + Future getUserDetail({required int id}) => + _source.getUserDetail(id: id); +} + +@riverpod +UserRepository userRepository(UserRepositoryRef ref) { + final source = ref.watch(userDataSourceProvider); + return UserRepositoryImpl(source: source); +} diff --git a/lib/src/data/source/post/post_data_source.dart b/lib/src/data/source/post/post_data_source.dart index 4f0a6b1..e995186 100644 --- a/lib/src/data/source/post/post_data_source.dart +++ b/lib/src/data/source/post/post_data_source.dart @@ -1,5 +1,7 @@ part of '../source.dart'; abstract class PostDataSource { - Future> getPost({required int start, int limit = 20}); + Future> getPostList({required int start, int limit = 20}); + + Future getPostDetail({required int id}); } diff --git a/lib/src/data/source/post/post_data_source_impl.dart b/lib/src/data/source/post/post_data_source_impl.dart index 7d60484..41073bc 100644 --- a/lib/src/data/source/post/post_data_source_impl.dart +++ b/lib/src/data/source/post/post_data_source_impl.dart @@ -6,8 +6,12 @@ class PostDataSourceImpl implements PostDataSource { final PostService _service; @override - Future> getPost({required int start, int limit = 20}) => - _service.getPost(start: start, limit: limit); + Future> getPostList({required int start, int limit = 20}) => + _service.getPostList(start: start, limit: limit); + + @override + Future getPostDetail({required int id}) => + _service.getPostDetail(id: id); } @riverpod diff --git a/lib/src/data/source/post/post_service.dart b/lib/src/data/source/post/post_service.dart index 2475568..0398105 100644 --- a/lib/src/data/source/post/post_service.dart +++ b/lib/src/data/source/post/post_service.dart @@ -4,9 +4,14 @@ part of '../source.dart'; abstract class PostService { factory PostService(Dio dio, {String baseUrl}) = _PostService; - @GET('post') - Future> getPost({ + @GET('posts') + Future> getPostList({ @Query('_start') required int start, @Query('_limit') required int limit, }); + + @GET('posts/{id}') + Future getPostDetail({ + @Path('id') required int id, + }); } diff --git a/lib/src/data/source/source.dart b/lib/src/data/source/source.dart index 87d0b09..63ea836 100644 --- a/lib/src/data/source/source.dart +++ b/lib/src/data/source/source.dart @@ -9,3 +9,6 @@ part 'source.g.dart'; part 'post/post_data_source.dart'; part 'post/post_data_source_impl.dart'; part 'post/post_service.dart'; +part 'user/user_data_source.dart'; +part 'user/user_data_source_impl.dart'; +part 'user/user_service.dart'; diff --git a/lib/src/data/source/source.g.dart b/lib/src/data/source/source.g.dart index 5d0b182..68673bb 100644 --- a/lib/src/data/source/source.g.dart +++ b/lib/src/data/source/source.g.dart @@ -19,7 +19,7 @@ class _PostService implements PostService { String? baseUrl; @override - Future> getPost({ + Future> getPostList({ required int start, required int limit, }) async { @@ -38,7 +38,7 @@ class _PostService implements PostService { ) .compose( _dio.options, - 'post', + 'posts', queryParameters: queryParameters, data: _data, ) @@ -53,6 +53,103 @@ class _PostService implements PostService { return value; } + @override + Future getPostDetail({required int id}) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final Map? _data = null; + final _result = + await _dio.fetch>(_setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + 'posts/${id}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = Post.fromJson(_result.data!); + return value; + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers + +class _UserService implements UserService { + _UserService( + this._dio, { + this.baseUrl, + }); + + final Dio _dio; + + String? baseUrl; + + @override + Future getUserDetail({required int id}) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final Map? _data = null; + final _result = + await _dio.fetch>(_setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + 'users/${id}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = User.fromJson(_result.data!); + return value; + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || @@ -103,5 +200,20 @@ final postDataSourceProvider = AutoDisposeProvider.internal( ); typedef PostDataSourceRef = AutoDisposeProviderRef; +String _$userDataSourceHash() => r'ab0e920ad00b2dee5ac0a2e56b7f4795bcb9cd44'; + +/// See also [userDataSource]. +@ProviderFor(userDataSource) +final userDataSourceProvider = AutoDisposeProvider.internal( + userDataSource, + name: r'userDataSourceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$userDataSourceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef UserDataSourceRef = AutoDisposeProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/data/source/user/user_data_source.dart b/lib/src/data/source/user/user_data_source.dart new file mode 100644 index 0000000..2ddf1d0 --- /dev/null +++ b/lib/src/data/source/user/user_data_source.dart @@ -0,0 +1,5 @@ +part of '../source.dart'; + +abstract class UserDataSource { + Future getUserDetail({required int id}); +} diff --git a/lib/src/data/source/user/user_data_source_impl.dart b/lib/src/data/source/user/user_data_source_impl.dart new file mode 100644 index 0000000..98374e4 --- /dev/null +++ b/lib/src/data/source/user/user_data_source_impl.dart @@ -0,0 +1,17 @@ +part of '../source.dart'; + +class UserDataSourceImpl implements UserDataSource { + UserDataSourceImpl({required UserService service}) : _service = service; + + final UserService _service; + + @override + Future getUserDetail({required int id}) => + _service.getUserDetail(id: id); +} + +@riverpod +UserDataSource userDataSource(UserDataSourceRef ref) { + final http = ref.watch(httpProvider); + return UserDataSourceImpl(service: UserService(http)); +} diff --git a/lib/src/data/source/user/user_service.dart b/lib/src/data/source/user/user_service.dart new file mode 100644 index 0000000..3d1a63d --- /dev/null +++ b/lib/src/data/source/user/user_service.dart @@ -0,0 +1,11 @@ +part of '../source.dart'; + +@RestApi() +abstract class UserService { + factory UserService(Dio dio, {String baseUrl}) = _UserService; + + @GET('users/{id}') + Future getUserDetail({ + @Path('id') required int id, + }); +} diff --git a/lib/src/model/model.dart b/lib/src/model/model.dart index 4828eb2..de2e64f 100644 --- a/lib/src/model/model.dart +++ b/lib/src/model/model.dart @@ -4,3 +4,4 @@ part 'model.g.dart'; part 'model.freezed.dart'; part 'post/post.dart'; +part 'user/user.dart'; diff --git a/lib/src/model/model.freezed.dart b/lib/src/model/model.freezed.dart index 73cd5ba..7a4f94c 100644 --- a/lib/src/model/model.freezed.dart +++ b/lib/src/model/model.freezed.dart @@ -196,3 +196,830 @@ abstract class _Post implements Post { @JsonKey(ignore: true) _$$_PostCopyWith<_$_Post> get copyWith => throw _privateConstructorUsedError; } + +User _$UserFromJson(Map json) { + return _User.fromJson(json); +} + +/// @nodoc +mixin _$User { + int get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get username => throw _privateConstructorUsedError; + String get email => throw _privateConstructorUsedError; + Address get address => throw _privateConstructorUsedError; + String get phone => throw _privateConstructorUsedError; + String get website => throw _privateConstructorUsedError; + Company get company => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $UserCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserCopyWith<$Res> { + factory $UserCopyWith(User value, $Res Function(User) then) = + _$UserCopyWithImpl<$Res, User>; + @useResult + $Res call( + {int id, + String name, + String username, + String email, + Address address, + String phone, + String website, + Company company}); + + $AddressCopyWith<$Res> get address; + $CompanyCopyWith<$Res> get company; +} + +/// @nodoc +class _$UserCopyWithImpl<$Res, $Val extends User> + implements $UserCopyWith<$Res> { + _$UserCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? username = null, + Object? email = null, + Object? address = null, + Object? phone = null, + Object? website = null, + Object? company = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + username: null == username + ? _value.username + : username // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + address: null == address + ? _value.address + : address // ignore: cast_nullable_to_non_nullable + as Address, + phone: null == phone + ? _value.phone + : phone // ignore: cast_nullable_to_non_nullable + as String, + website: null == website + ? _value.website + : website // ignore: cast_nullable_to_non_nullable + as String, + company: null == company + ? _value.company + : company // ignore: cast_nullable_to_non_nullable + as Company, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $AddressCopyWith<$Res> get address { + return $AddressCopyWith<$Res>(_value.address, (value) { + return _then(_value.copyWith(address: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $CompanyCopyWith<$Res> get company { + return $CompanyCopyWith<$Res>(_value.company, (value) { + return _then(_value.copyWith(company: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_UserCopyWith<$Res> implements $UserCopyWith<$Res> { + factory _$$_UserCopyWith(_$_User value, $Res Function(_$_User) then) = + __$$_UserCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + String name, + String username, + String email, + Address address, + String phone, + String website, + Company company}); + + @override + $AddressCopyWith<$Res> get address; + @override + $CompanyCopyWith<$Res> get company; +} + +/// @nodoc +class __$$_UserCopyWithImpl<$Res> extends _$UserCopyWithImpl<$Res, _$_User> + implements _$$_UserCopyWith<$Res> { + __$$_UserCopyWithImpl(_$_User _value, $Res Function(_$_User) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? username = null, + Object? email = null, + Object? address = null, + Object? phone = null, + Object? website = null, + Object? company = null, + }) { + return _then(_$_User( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + username: null == username + ? _value.username + : username // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + address: null == address + ? _value.address + : address // ignore: cast_nullable_to_non_nullable + as Address, + phone: null == phone + ? _value.phone + : phone // ignore: cast_nullable_to_non_nullable + as String, + website: null == website + ? _value.website + : website // ignore: cast_nullable_to_non_nullable + as String, + company: null == company + ? _value.company + : company // ignore: cast_nullable_to_non_nullable + as Company, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_User implements _User { + _$_User( + {required this.id, + required this.name, + required this.username, + required this.email, + required this.address, + required this.phone, + required this.website, + required this.company}); + + factory _$_User.fromJson(Map json) => _$$_UserFromJson(json); + + @override + final int id; + @override + final String name; + @override + final String username; + @override + final String email; + @override + final Address address; + @override + final String phone; + @override + final String website; + @override + final Company company; + + @override + String toString() { + return 'User(id: $id, name: $name, username: $username, email: $email, address: $address, phone: $phone, website: $website, company: $company)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_User && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.username, username) || + other.username == username) && + (identical(other.email, email) || other.email == email) && + (identical(other.address, address) || other.address == address) && + (identical(other.phone, phone) || other.phone == phone) && + (identical(other.website, website) || other.website == website) && + (identical(other.company, company) || other.company == company)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, id, name, username, email, address, phone, website, company); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_UserCopyWith<_$_User> get copyWith => + __$$_UserCopyWithImpl<_$_User>(this, _$identity); + + @override + Map toJson() { + return _$$_UserToJson( + this, + ); + } +} + +abstract class _User implements User { + factory _User( + {required final int id, + required final String name, + required final String username, + required final String email, + required final Address address, + required final String phone, + required final String website, + required final Company company}) = _$_User; + + factory _User.fromJson(Map json) = _$_User.fromJson; + + @override + int get id; + @override + String get name; + @override + String get username; + @override + String get email; + @override + Address get address; + @override + String get phone; + @override + String get website; + @override + Company get company; + @override + @JsonKey(ignore: true) + _$$_UserCopyWith<_$_User> get copyWith => throw _privateConstructorUsedError; +} + +Address _$AddressFromJson(Map json) { + return _Address.fromJson(json); +} + +/// @nodoc +mixin _$Address { + String get street => throw _privateConstructorUsedError; + String get suite => throw _privateConstructorUsedError; + String get city => throw _privateConstructorUsedError; + String get zipcode => throw _privateConstructorUsedError; + Geo get geo => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $AddressCopyWith
get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AddressCopyWith<$Res> { + factory $AddressCopyWith(Address value, $Res Function(Address) then) = + _$AddressCopyWithImpl<$Res, Address>; + @useResult + $Res call( + {String street, String suite, String city, String zipcode, Geo geo}); + + $GeoCopyWith<$Res> get geo; +} + +/// @nodoc +class _$AddressCopyWithImpl<$Res, $Val extends Address> + implements $AddressCopyWith<$Res> { + _$AddressCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? street = null, + Object? suite = null, + Object? city = null, + Object? zipcode = null, + Object? geo = null, + }) { + return _then(_value.copyWith( + street: null == street + ? _value.street + : street // ignore: cast_nullable_to_non_nullable + as String, + suite: null == suite + ? _value.suite + : suite // ignore: cast_nullable_to_non_nullable + as String, + city: null == city + ? _value.city + : city // ignore: cast_nullable_to_non_nullable + as String, + zipcode: null == zipcode + ? _value.zipcode + : zipcode // ignore: cast_nullable_to_non_nullable + as String, + geo: null == geo + ? _value.geo + : geo // ignore: cast_nullable_to_non_nullable + as Geo, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $GeoCopyWith<$Res> get geo { + return $GeoCopyWith<$Res>(_value.geo, (value) { + return _then(_value.copyWith(geo: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_AddressCopyWith<$Res> implements $AddressCopyWith<$Res> { + factory _$$_AddressCopyWith( + _$_Address value, $Res Function(_$_Address) then) = + __$$_AddressCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String street, String suite, String city, String zipcode, Geo geo}); + + @override + $GeoCopyWith<$Res> get geo; +} + +/// @nodoc +class __$$_AddressCopyWithImpl<$Res> + extends _$AddressCopyWithImpl<$Res, _$_Address> + implements _$$_AddressCopyWith<$Res> { + __$$_AddressCopyWithImpl(_$_Address _value, $Res Function(_$_Address) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? street = null, + Object? suite = null, + Object? city = null, + Object? zipcode = null, + Object? geo = null, + }) { + return _then(_$_Address( + street: null == street + ? _value.street + : street // ignore: cast_nullable_to_non_nullable + as String, + suite: null == suite + ? _value.suite + : suite // ignore: cast_nullable_to_non_nullable + as String, + city: null == city + ? _value.city + : city // ignore: cast_nullable_to_non_nullable + as String, + zipcode: null == zipcode + ? _value.zipcode + : zipcode // ignore: cast_nullable_to_non_nullable + as String, + geo: null == geo + ? _value.geo + : geo // ignore: cast_nullable_to_non_nullable + as Geo, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_Address implements _Address { + _$_Address( + {required this.street, + required this.suite, + required this.city, + required this.zipcode, + required this.geo}); + + factory _$_Address.fromJson(Map json) => + _$$_AddressFromJson(json); + + @override + final String street; + @override + final String suite; + @override + final String city; + @override + final String zipcode; + @override + final Geo geo; + + @override + String toString() { + return 'Address(street: $street, suite: $suite, city: $city, zipcode: $zipcode, geo: $geo)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_Address && + (identical(other.street, street) || other.street == street) && + (identical(other.suite, suite) || other.suite == suite) && + (identical(other.city, city) || other.city == city) && + (identical(other.zipcode, zipcode) || other.zipcode == zipcode) && + (identical(other.geo, geo) || other.geo == geo)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, street, suite, city, zipcode, geo); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_AddressCopyWith<_$_Address> get copyWith => + __$$_AddressCopyWithImpl<_$_Address>(this, _$identity); + + @override + Map toJson() { + return _$$_AddressToJson( + this, + ); + } +} + +abstract class _Address implements Address { + factory _Address( + {required final String street, + required final String suite, + required final String city, + required final String zipcode, + required final Geo geo}) = _$_Address; + + factory _Address.fromJson(Map json) = _$_Address.fromJson; + + @override + String get street; + @override + String get suite; + @override + String get city; + @override + String get zipcode; + @override + Geo get geo; + @override + @JsonKey(ignore: true) + _$$_AddressCopyWith<_$_Address> get copyWith => + throw _privateConstructorUsedError; +} + +Geo _$GeoFromJson(Map json) { + return _Geo.fromJson(json); +} + +/// @nodoc +mixin _$Geo { + String get lat => throw _privateConstructorUsedError; + String get lng => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $GeoCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GeoCopyWith<$Res> { + factory $GeoCopyWith(Geo value, $Res Function(Geo) then) = + _$GeoCopyWithImpl<$Res, Geo>; + @useResult + $Res call({String lat, String lng}); +} + +/// @nodoc +class _$GeoCopyWithImpl<$Res, $Val extends Geo> implements $GeoCopyWith<$Res> { + _$GeoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? lat = null, + Object? lng = null, + }) { + return _then(_value.copyWith( + lat: null == lat + ? _value.lat + : lat // ignore: cast_nullable_to_non_nullable + as String, + lng: null == lng + ? _value.lng + : lng // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_GeoCopyWith<$Res> implements $GeoCopyWith<$Res> { + factory _$$_GeoCopyWith(_$_Geo value, $Res Function(_$_Geo) then) = + __$$_GeoCopyWithImpl<$Res>; + @override + @useResult + $Res call({String lat, String lng}); +} + +/// @nodoc +class __$$_GeoCopyWithImpl<$Res> extends _$GeoCopyWithImpl<$Res, _$_Geo> + implements _$$_GeoCopyWith<$Res> { + __$$_GeoCopyWithImpl(_$_Geo _value, $Res Function(_$_Geo) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? lat = null, + Object? lng = null, + }) { + return _then(_$_Geo( + lat: null == lat + ? _value.lat + : lat // ignore: cast_nullable_to_non_nullable + as String, + lng: null == lng + ? _value.lng + : lng // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_Geo implements _Geo { + _$_Geo({required this.lat, required this.lng}); + + factory _$_Geo.fromJson(Map json) => _$$_GeoFromJson(json); + + @override + final String lat; + @override + final String lng; + + @override + String toString() { + return 'Geo(lat: $lat, lng: $lng)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_Geo && + (identical(other.lat, lat) || other.lat == lat) && + (identical(other.lng, lng) || other.lng == lng)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, lat, lng); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_GeoCopyWith<_$_Geo> get copyWith => + __$$_GeoCopyWithImpl<_$_Geo>(this, _$identity); + + @override + Map toJson() { + return _$$_GeoToJson( + this, + ); + } +} + +abstract class _Geo implements Geo { + factory _Geo({required final String lat, required final String lng}) = _$_Geo; + + factory _Geo.fromJson(Map json) = _$_Geo.fromJson; + + @override + String get lat; + @override + String get lng; + @override + @JsonKey(ignore: true) + _$$_GeoCopyWith<_$_Geo> get copyWith => throw _privateConstructorUsedError; +} + +Company _$CompanyFromJson(Map json) { + return _Company.fromJson(json); +} + +/// @nodoc +mixin _$Company { + String get name => throw _privateConstructorUsedError; + String get catchPhrase => throw _privateConstructorUsedError; + String get bs => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $CompanyCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CompanyCopyWith<$Res> { + factory $CompanyCopyWith(Company value, $Res Function(Company) then) = + _$CompanyCopyWithImpl<$Res, Company>; + @useResult + $Res call({String name, String catchPhrase, String bs}); +} + +/// @nodoc +class _$CompanyCopyWithImpl<$Res, $Val extends Company> + implements $CompanyCopyWith<$Res> { + _$CompanyCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? catchPhrase = null, + Object? bs = null, + }) { + return _then(_value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + catchPhrase: null == catchPhrase + ? _value.catchPhrase + : catchPhrase // ignore: cast_nullable_to_non_nullable + as String, + bs: null == bs + ? _value.bs + : bs // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_CompanyCopyWith<$Res> implements $CompanyCopyWith<$Res> { + factory _$$_CompanyCopyWith( + _$_Company value, $Res Function(_$_Company) then) = + __$$_CompanyCopyWithImpl<$Res>; + @override + @useResult + $Res call({String name, String catchPhrase, String bs}); +} + +/// @nodoc +class __$$_CompanyCopyWithImpl<$Res> + extends _$CompanyCopyWithImpl<$Res, _$_Company> + implements _$$_CompanyCopyWith<$Res> { + __$$_CompanyCopyWithImpl(_$_Company _value, $Res Function(_$_Company) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? catchPhrase = null, + Object? bs = null, + }) { + return _then(_$_Company( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + catchPhrase: null == catchPhrase + ? _value.catchPhrase + : catchPhrase // ignore: cast_nullable_to_non_nullable + as String, + bs: null == bs + ? _value.bs + : bs // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_Company implements _Company { + _$_Company({required this.name, required this.catchPhrase, required this.bs}); + + factory _$_Company.fromJson(Map json) => + _$$_CompanyFromJson(json); + + @override + final String name; + @override + final String catchPhrase; + @override + final String bs; + + @override + String toString() { + return 'Company(name: $name, catchPhrase: $catchPhrase, bs: $bs)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_Company && + (identical(other.name, name) || other.name == name) && + (identical(other.catchPhrase, catchPhrase) || + other.catchPhrase == catchPhrase) && + (identical(other.bs, bs) || other.bs == bs)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, name, catchPhrase, bs); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_CompanyCopyWith<_$_Company> get copyWith => + __$$_CompanyCopyWithImpl<_$_Company>(this, _$identity); + + @override + Map toJson() { + return _$$_CompanyToJson( + this, + ); + } +} + +abstract class _Company implements Company { + factory _Company( + {required final String name, + required final String catchPhrase, + required final String bs}) = _$_Company; + + factory _Company.fromJson(Map json) = _$_Company.fromJson; + + @override + String get name; + @override + String get catchPhrase; + @override + String get bs; + @override + @JsonKey(ignore: true) + _$$_CompanyCopyWith<_$_Company> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/src/model/model.g.dart b/lib/src/model/model.g.dart index 27ddd87..9a0a631 100644 --- a/lib/src/model/model.g.dart +++ b/lib/src/model/model.g.dart @@ -19,3 +19,65 @@ Map _$$_PostToJson(_$_Post instance) => { 'title': instance.title, 'body': instance.body, }; + +_$_User _$$_UserFromJson(Map json) => _$_User( + id: json['id'] as int, + name: json['name'] as String, + username: json['username'] as String, + email: json['email'] as String, + address: Address.fromJson(json['address'] as Map), + phone: json['phone'] as String, + website: json['website'] as String, + company: Company.fromJson(json['company'] as Map), + ); + +Map _$$_UserToJson(_$_User instance) => { + 'id': instance.id, + 'name': instance.name, + 'username': instance.username, + 'email': instance.email, + 'address': instance.address, + 'phone': instance.phone, + 'website': instance.website, + 'company': instance.company, + }; + +_$_Address _$$_AddressFromJson(Map json) => _$_Address( + street: json['street'] as String, + suite: json['suite'] as String, + city: json['city'] as String, + zipcode: json['zipcode'] as String, + geo: Geo.fromJson(json['geo'] as Map), + ); + +Map _$$_AddressToJson(_$_Address instance) => + { + 'street': instance.street, + 'suite': instance.suite, + 'city': instance.city, + 'zipcode': instance.zipcode, + 'geo': instance.geo, + }; + +_$_Geo _$$_GeoFromJson(Map json) => _$_Geo( + lat: json['lat'] as String, + lng: json['lng'] as String, + ); + +Map _$$_GeoToJson(_$_Geo instance) => { + 'lat': instance.lat, + 'lng': instance.lng, + }; + +_$_Company _$$_CompanyFromJson(Map json) => _$_Company( + name: json['name'] as String, + catchPhrase: json['catchPhrase'] as String, + bs: json['bs'] as String, + ); + +Map _$$_CompanyToJson(_$_Company instance) => + { + 'name': instance.name, + 'catchPhrase': instance.catchPhrase, + 'bs': instance.bs, + }; diff --git a/lib/src/model/user/user.dart b/lib/src/model/user/user.dart new file mode 100644 index 0000000..8c8fbf6 --- /dev/null +++ b/lib/src/model/user/user.dart @@ -0,0 +1,53 @@ +part of '../model.dart'; + +@freezed +class User with _$User { + factory User({ + required int id, + required String name, + required String username, + required String email, + required Address address, + required String phone, + required String website, + required Company company, + }) = _User; + + factory User.fromJson(Map json) => _$UserFromJson(json); +} + +@freezed +class Address with _$Address { + factory Address({ + required String street, + required String suite, + required String city, + required String zipcode, + required Geo geo, + }) = _Address; + + factory Address.fromJson(Map json) => + _$AddressFromJson(json); +} + +@freezed +class Geo with _$Geo { + factory Geo({ + required String lat, + required String lng, + }) = _Geo; + + factory Geo.fromJson(Map json) => _$GeoFromJson(json); +} + +@freezed +class Company with _$Company { + factory Company({ + required String name, + required String catchPhrase, + required String bs, + }) = _Company; + + factory Company.fromJson(Map json) => + _$CompanyFromJson(json); +} diff --git a/lib/src/presentation/app.dart b/lib/src/presentation/app.dart index df5153d..e4fffe3 100644 --- a/lib/src/presentation/app.dart +++ b/lib/src/presentation/app.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod_clean_architecture/src/presentation/src/page/home_page.dart'; class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { - return const MaterialApp(); + return MaterialApp( + home: HomePage(), + ); } } diff --git a/lib/src/presentation/src/controller/controller.dart b/lib/src/presentation/src/controller/controller.dart new file mode 100644 index 0000000..2f042ca --- /dev/null +++ b/lib/src/presentation/src/controller/controller.dart @@ -0,0 +1,12 @@ +import 'package:flutter_riverpod_clean_architecture/src/data/repository/repository.dart'; +import 'package:flutter_riverpod_clean_architecture/src/model/model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'controller.g.dart'; +part 'controller.freezed.dart'; + +part 'detail/detail_controller.dart'; +part 'detail/detail_state.dart'; +part 'post/post_controller.dart'; +part 'post/post_state.dart'; diff --git a/lib/src/presentation/src/controller/controller.freezed.dart b/lib/src/presentation/src/controller/controller.freezed.dart new file mode 100644 index 0000000..da12ad0 --- /dev/null +++ b/lib/src/presentation/src/controller/controller.freezed.dart @@ -0,0 +1,389 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'controller.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$DetailState { + User get user => throw _privateConstructorUsedError; + Post get post => throw _privateConstructorUsedError; + List get comments => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $DetailStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DetailStateCopyWith<$Res> { + factory $DetailStateCopyWith( + DetailState value, $Res Function(DetailState) then) = + _$DetailStateCopyWithImpl<$Res, DetailState>; + @useResult + $Res call({User user, Post post, List comments}); + + $UserCopyWith<$Res> get user; + $PostCopyWith<$Res> get post; +} + +/// @nodoc +class _$DetailStateCopyWithImpl<$Res, $Val extends DetailState> + implements $DetailStateCopyWith<$Res> { + _$DetailStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? user = null, + Object? post = null, + Object? comments = null, + }) { + return _then(_value.copyWith( + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as User, + post: null == post + ? _value.post + : post // ignore: cast_nullable_to_non_nullable + as Post, + comments: null == comments + ? _value.comments + : comments // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $UserCopyWith<$Res> get user { + return $UserCopyWith<$Res>(_value.user, (value) { + return _then(_value.copyWith(user: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $PostCopyWith<$Res> get post { + return $PostCopyWith<$Res>(_value.post, (value) { + return _then(_value.copyWith(post: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_DetailStateCopyWith<$Res> + implements $DetailStateCopyWith<$Res> { + factory _$$_DetailStateCopyWith( + _$_DetailState value, $Res Function(_$_DetailState) then) = + __$$_DetailStateCopyWithImpl<$Res>; + @override + @useResult + $Res call({User user, Post post, List comments}); + + @override + $UserCopyWith<$Res> get user; + @override + $PostCopyWith<$Res> get post; +} + +/// @nodoc +class __$$_DetailStateCopyWithImpl<$Res> + extends _$DetailStateCopyWithImpl<$Res, _$_DetailState> + implements _$$_DetailStateCopyWith<$Res> { + __$$_DetailStateCopyWithImpl( + _$_DetailState _value, $Res Function(_$_DetailState) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? user = null, + Object? post = null, + Object? comments = null, + }) { + return _then(_$_DetailState( + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as User, + post: null == post + ? _value.post + : post // ignore: cast_nullable_to_non_nullable + as Post, + comments: null == comments + ? _value._comments + : comments // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$_DetailState implements _DetailState { + _$_DetailState( + {required this.user, + required this.post, + required final List comments}) + : _comments = comments; + + @override + final User user; + @override + final Post post; + final List _comments; + @override + List get comments { + if (_comments is EqualUnmodifiableListView) return _comments; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_comments); + } + + @override + String toString() { + return 'DetailState(user: $user, post: $post, comments: $comments)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_DetailState && + (identical(other.user, user) || other.user == user) && + (identical(other.post, post) || other.post == post) && + const DeepCollectionEquality().equals(other._comments, _comments)); + } + + @override + int get hashCode => Object.hash( + runtimeType, user, post, const DeepCollectionEquality().hash(_comments)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_DetailStateCopyWith<_$_DetailState> get copyWith => + __$$_DetailStateCopyWithImpl<_$_DetailState>(this, _$identity); +} + +abstract class _DetailState implements DetailState { + factory _DetailState( + {required final User user, + required final Post post, + required final List comments}) = _$_DetailState; + + @override + User get user; + @override + Post get post; + @override + List get comments; + @override + @JsonKey(ignore: true) + _$$_DetailStateCopyWith<_$_DetailState> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$PostState { + bool get isLoading => throw _privateConstructorUsedError; + int get currentIndex => throw _privateConstructorUsedError; + List get posts => throw _privateConstructorUsedError; + bool get hasReachEnd => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $PostStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PostStateCopyWith<$Res> { + factory $PostStateCopyWith(PostState value, $Res Function(PostState) then) = + _$PostStateCopyWithImpl<$Res, PostState>; + @useResult + $Res call( + {bool isLoading, int currentIndex, List posts, bool hasReachEnd}); +} + +/// @nodoc +class _$PostStateCopyWithImpl<$Res, $Val extends PostState> + implements $PostStateCopyWith<$Res> { + _$PostStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isLoading = null, + Object? currentIndex = null, + Object? posts = null, + Object? hasReachEnd = null, + }) { + return _then(_value.copyWith( + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + currentIndex: null == currentIndex + ? _value.currentIndex + : currentIndex // ignore: cast_nullable_to_non_nullable + as int, + posts: null == posts + ? _value.posts + : posts // ignore: cast_nullable_to_non_nullable + as List, + hasReachEnd: null == hasReachEnd + ? _value.hasReachEnd + : hasReachEnd // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$_PostStateCopyWith<$Res> implements $PostStateCopyWith<$Res> { + factory _$$_PostStateCopyWith( + _$_PostState value, $Res Function(_$_PostState) then) = + __$$_PostStateCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool isLoading, int currentIndex, List posts, bool hasReachEnd}); +} + +/// @nodoc +class __$$_PostStateCopyWithImpl<$Res> + extends _$PostStateCopyWithImpl<$Res, _$_PostState> + implements _$$_PostStateCopyWith<$Res> { + __$$_PostStateCopyWithImpl( + _$_PostState _value, $Res Function(_$_PostState) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isLoading = null, + Object? currentIndex = null, + Object? posts = null, + Object? hasReachEnd = null, + }) { + return _then(_$_PostState( + isLoading: null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + currentIndex: null == currentIndex + ? _value.currentIndex + : currentIndex // ignore: cast_nullable_to_non_nullable + as int, + posts: null == posts + ? _value._posts + : posts // ignore: cast_nullable_to_non_nullable + as List, + hasReachEnd: null == hasReachEnd + ? _value.hasReachEnd + : hasReachEnd // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$_PostState implements _PostState { + _$_PostState( + {this.isLoading = false, + this.currentIndex = 0, + final List posts = const [], + this.hasReachEnd = false}) + : _posts = posts; + + @override + @JsonKey() + final bool isLoading; + @override + @JsonKey() + final int currentIndex; + final List _posts; + @override + @JsonKey() + List get posts { + if (_posts is EqualUnmodifiableListView) return _posts; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_posts); + } + + @override + @JsonKey() + final bool hasReachEnd; + + @override + String toString() { + return 'PostState(isLoading: $isLoading, currentIndex: $currentIndex, posts: $posts, hasReachEnd: $hasReachEnd)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_PostState && + (identical(other.isLoading, isLoading) || + other.isLoading == isLoading) && + (identical(other.currentIndex, currentIndex) || + other.currentIndex == currentIndex) && + const DeepCollectionEquality().equals(other._posts, _posts) && + (identical(other.hasReachEnd, hasReachEnd) || + other.hasReachEnd == hasReachEnd)); + } + + @override + int get hashCode => Object.hash(runtimeType, isLoading, currentIndex, + const DeepCollectionEquality().hash(_posts), hasReachEnd); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_PostStateCopyWith<_$_PostState> get copyWith => + __$$_PostStateCopyWithImpl<_$_PostState>(this, _$identity); +} + +abstract class _PostState implements PostState { + factory _PostState( + {final bool isLoading, + final int currentIndex, + final List posts, + final bool hasReachEnd}) = _$_PostState; + + @override + bool get isLoading; + @override + int get currentIndex; + @override + List get posts; + @override + bool get hasReachEnd; + @override + @JsonKey(ignore: true) + _$$_PostStateCopyWith<_$_PostState> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/src/presentation/src/controller/controller.g.dart b/lib/src/presentation/src/controller/controller.g.dart new file mode 100644 index 0000000..4267ada --- /dev/null +++ b/lib/src/presentation/src/controller/controller.g.dart @@ -0,0 +1,191 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$detailControllerHash() => r'bb97c3a3a096243c8dd9ac114f45b5d7163501cd'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$DetailController + extends BuildlessAutoDisposeAsyncNotifier { + late final int id; + + FutureOr build( + int id, + ); +} + +/// See also [DetailController]. +@ProviderFor(DetailController) +const detailControllerProvider = DetailControllerFamily(); + +/// See also [DetailController]. +class DetailControllerFamily extends Family> { + /// See also [DetailController]. + const DetailControllerFamily(); + + /// See also [DetailController]. + DetailControllerProvider call( + int id, + ) { + return DetailControllerProvider( + id, + ); + } + + @override + DetailControllerProvider getProviderOverride( + covariant DetailControllerProvider provider, + ) { + return call( + provider.id, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'detailControllerProvider'; +} + +/// See also [DetailController]. +class DetailControllerProvider extends AutoDisposeAsyncNotifierProviderImpl< + DetailController, DetailState> { + /// See also [DetailController]. + DetailControllerProvider( + int id, + ) : this._internal( + () => DetailController()..id = id, + from: detailControllerProvider, + name: r'detailControllerProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$detailControllerHash, + dependencies: DetailControllerFamily._dependencies, + allTransitiveDependencies: + DetailControllerFamily._allTransitiveDependencies, + id: id, + ); + + DetailControllerProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.id, + }) : super.internal(); + + final int id; + + @override + FutureOr runNotifierBuild( + covariant DetailController notifier, + ) { + return notifier.build( + id, + ); + } + + @override + Override overrideWith(DetailController Function() create) { + return ProviderOverride( + origin: this, + override: DetailControllerProvider._internal( + () => create()..id = id, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + id: id, + ), + ); + } + + @override + AutoDisposeAsyncNotifierProviderElement + createElement() { + return _DetailControllerProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is DetailControllerProvider && other.id == id; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, id.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin DetailControllerRef on AutoDisposeAsyncNotifierProviderRef { + /// The parameter `id` of this provider. + int get id; +} + +class _DetailControllerProviderElement + extends AutoDisposeAsyncNotifierProviderElement with DetailControllerRef { + _DetailControllerProviderElement(super.provider); + + @override + int get id => (origin as DetailControllerProvider).id; +} + +String _$postControllerHash() => r'c2704bdcd27e67b9534efdfba6ec8a307823118f'; + +/// See also [PostController]. +@ProviderFor(PostController) +final postControllerProvider = + AutoDisposeAsyncNotifierProvider.internal( + PostController.new, + name: r'postControllerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$postControllerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$PostController = AutoDisposeAsyncNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/src/presentation/src/controller/detail/detail_controller.dart b/lib/src/presentation/src/controller/detail/detail_controller.dart new file mode 100644 index 0000000..521357f --- /dev/null +++ b/lib/src/presentation/src/controller/detail/detail_controller.dart @@ -0,0 +1,16 @@ +part of '../controller.dart'; + +@riverpod +class DetailController extends _$DetailController { + @override + FutureOr build(int id) { + return _fetchData(id: id); + } + + Future _fetchData({required int id}) async { + final post = await ref.watch(postRepositoryProvider).getPostDetail(id: id); + final user = + await ref.watch(userRepositoryProvider).getUserDetail(id: post.userId); + return DetailState(post: post, user: user, comments: []); + } +} diff --git a/lib/src/presentation/src/controller/detail/detail_state.dart b/lib/src/presentation/src/controller/detail/detail_state.dart new file mode 100644 index 0000000..9339687 --- /dev/null +++ b/lib/src/presentation/src/controller/detail/detail_state.dart @@ -0,0 +1,10 @@ +part of '../controller.dart'; + +@freezed +class DetailState with _$DetailState { + factory DetailState({ + required User user, + required Post post, + required List comments, + }) = _DetailState; +} diff --git a/lib/src/presentation/src/controller/post/post_controller.dart b/lib/src/presentation/src/controller/post/post_controller.dart new file mode 100644 index 0000000..b8a30c9 --- /dev/null +++ b/lib/src/presentation/src/controller/post/post_controller.dart @@ -0,0 +1,37 @@ +part of '../controller.dart'; + +@riverpod +class PostController extends _$PostController { + @override + FutureOr build() async { + return _fetchData(); + } + + Future _fetchData() async { + final posts = await ref.watch(postRepositoryProvider).getPostList(start: 0); + return PostState(currentIndex: posts.last.id, posts: posts); + } + + Future loadMore() async { + final value = state.valueOrNull; + + if (value != null) { + if (value.hasReachEnd) return; + if (!value.isLoading) { + state = AsyncValue.data(value.copyWith(isLoading: true)); + + state = await AsyncValue.guard(() async { + final posts = await ref + .watch(postRepositoryProvider) + .getPostList(start: value.currentIndex); + + return value.copyWith( + isLoading: false, + hasReachEnd: posts.isEmpty, + currentIndex: posts.isEmpty ? value.posts.last.id : posts.last.id, + posts: [...value.posts, ...posts]); + }); + } + } + } +} diff --git a/lib/src/presentation/src/controller/post/post_state.dart b/lib/src/presentation/src/controller/post/post_state.dart new file mode 100644 index 0000000..43cf116 --- /dev/null +++ b/lib/src/presentation/src/controller/post/post_state.dart @@ -0,0 +1,11 @@ +part of '../controller.dart'; + +@freezed +sealed class PostState with _$PostState { + factory PostState({ + @Default(false) bool isLoading, + @Default(0) int currentIndex, + @Default([]) List posts, + @Default(false) bool hasReachEnd, + }) = _PostState; +} diff --git a/lib/src/presentation/src/page/detail_page.dart b/lib/src/presentation/src/page/detail_page.dart new file mode 100644 index 0000000..4ce3f9a --- /dev/null +++ b/lib/src/presentation/src/page/detail_page.dart @@ -0,0 +1,78 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_riverpod_clean_architecture/src/presentation/src/controller/controller.dart'; + +class DetailPage extends ConsumerWidget { + const DetailPage({super.key, required this.id}); + + final int id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(detailControllerProvider(id)); + return Scaffold( + appBar: AppBar( + title: const Text('Detail'), + ), + body: switch (state) { + AsyncData(:final value) => _buildDetail(value), + AsyncError(:final error) => Text('Error: $error'), + _ => const Center(child: CircularProgressIndicator()), + }); + } + + Widget _buildDetail(DetailState state) { + return Consumer( + builder: (context, ref, child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + state.post.title, + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20.0, + ), + ), + const SizedBox(height: 12.0), + Text(state.post.body), + const SizedBox(height: 20.0), + Divider(height: 1.0), + ], + ), + ), + ListTile( + onTap: () {}, + leading: CircleAvatar( + backgroundImage: NetworkImage( + 'https://source.unsplash.com/random/100x100/?img=${state.user.id}'), + ), + title: Text( + state.user.username, + style: TextStyle( + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text(state.user.email), + ), + Padding( + padding: + const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 16.0), + child: Column( + children: [ + Divider(height: 1.0), + const SizedBox(height: 20.0), + ], + ), + ) + ], + ), + ); + } +} diff --git a/lib/src/presentation/src/page/home_page.dart b/lib/src/presentation/src/page/home_page.dart new file mode 100644 index 0000000..235c24c --- /dev/null +++ b/lib/src/presentation/src/page/home_page.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_riverpod_clean_architecture/src/presentation/src/controller/controller.dart'; +import 'package:flutter_riverpod_clean_architecture/src/presentation/src/page/detail_page.dart'; + +class HomePage extends ConsumerWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(postControllerProvider); + return Scaffold( + appBar: AppBar( + title: const Text('Home'), + ), + body: switch (state) { + AsyncData(:final value) => _buildList(value), + AsyncError(:final error) => Text('Error: $error'), + _ => const Center(child: CircularProgressIndicator()), + }); + } + + _buildList(PostState state) { + return Consumer( + builder: (context, ref, child) => + NotificationListener( + child: ListView.builder( + padding: const EdgeInsets.all(16.0), + itemCount: + state.hasReachEnd ? state.posts.length : state.posts.length + 1, + itemBuilder: (context, index) => index != state.posts.length + ? Card( + child: ListTile( + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + DetailPage(id: state.posts[index].id), + ), + ), + leading: Stack( + alignment: Alignment.center, + children: [ + Container( + width: 32.0, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.blue, + ), + ), + Text( + '${index + 1}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16.0, + color: Colors.white, + ), + ), + ], + ), + title: Text( + state.posts[index].title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + state.posts[index].body, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ) + : const Padding( + padding: EdgeInsets.only(top: 20.0, bottom: 32.0), + child: Center( + child: CircularProgressIndicator(), + ), + ), + ), + onNotification: (notification) { + if (notification.metrics.extentBefore == + notification.metrics.maxScrollExtent) { + ref.read(postControllerProvider.notifier).loadMore(); + } + return false; + }, + ), + ); + } + + _loading() { + return const Center(child: CircularProgressIndicator()); + } + + _error() { + return const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.warning_amber, + size: 60.0, + ), + Text('Error occured!'), + ], + ), + ); + } +} diff --git a/lib/src/shared/provider/http/http_provider.dart b/lib/src/shared/provider/http/http_provider.dart index f04bc10..e9262de 100644 --- a/lib/src/shared/provider/http/http_provider.dart +++ b/lib/src/shared/provider/http/http_provider.dart @@ -8,7 +8,10 @@ Dio http(HttpRef ref) { connectTimeout: const Duration(milliseconds: 3000), receiveTimeout: const Duration(milliseconds: 3000), ); - return Dio(options)..interceptors.addAll([]); + return Dio(options) + ..interceptors.addAll([ + ref.watch(dummyInterceptorProvider), + ]); } @riverpod diff --git a/lib/src/shared/provider/provider.g.dart b/lib/src/shared/provider/provider.g.dart index d9990a5..a4a9fb3 100644 --- a/lib/src/shared/provider/provider.g.dart +++ b/lib/src/shared/provider/provider.g.dart @@ -6,7 +6,7 @@ part of 'provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$httpHash() => r'e87fa6ed82a215b1a33ba24fe8c1173db7444df8'; +String _$httpHash() => r'2b3b469736a05d4318f385e3777de9a349f470d0'; /// See also [http]. @ProviderFor(http) diff --git a/pubspec.lock b/pubspec.lock index 93dcb20..73e6068 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -262,6 +262,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: "6ae13b1145c589112cbd5c4fda6c65908993a9cb18d4f82042e9c28dd9fbf611" + url: "https://pub.dev" + source: hosted + version: "0.20.1" flutter_lints: dependency: "direct dev" description: @@ -323,6 +331,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + hooks_riverpod: + dependency: "direct main" + description: + name: hooks_riverpod + sha256: ad7b877c3687e38764633d221a1f65491bc7a540e724101e9a404a84db2a4276 + url: "https://pub.dev" + source: hosted + version: "2.4.0" hotreloader: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a609e92..6f8fa2e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ environment: dependencies: flutter: sdk: flutter + flutter_riverpod: ^2.4.0 riverpod_annotation: ^2.1.5 freezed_annotation: ^2.4.1 @@ -41,11 +42,13 @@ dependencies: logger: ^2.0.2 # widget cupertino_icons: ^1.0.2 + flutter_hooks: ^0.20.1 + hooks_riverpod: ^2.4.0 dev_dependencies: flutter_test: sdk: flutter - + flutter_lints: ^2.0.0 riverpod_generator: ^2.3.2 build_runner: ^2.4.6