diff --git a/lib/configs/factories.dart b/lib/configs/factories.dart index 4ad3849b..534a474b 100644 --- a/lib/configs/factories.dart +++ b/lib/configs/factories.dart @@ -4,4 +4,10 @@ final Map _factories = { UserInfo: () => UserInfo(), AuthTokenInfo: () => AuthTokenInfo(), SurveyInfo: () => SurveyInfo(), + ApiRawResponse: () => ApiRawResponse(), + ApiRawObject: () => ApiRawObject(), + DetailedSurveyInfo: () => DetailedSurveyInfo(), + SurveyQuestionInfo: () => SurveyQuestionInfo(), + SurveyQuestionDisplayType: (v) => SurveyQuestionDisplayType(v as String), + SurveyQuestionPickType: (v) => SurveyQuestionPickType(v as String), }; diff --git a/lib/core/viper/interactor.dart b/lib/core/viper/interactor.dart index ec1af059..ae65971d 100644 --- a/lib/core/viper/interactor.dart +++ b/lib/core/viper/interactor.dart @@ -3,3 +3,8 @@ part of 'module.dart'; class Interactor { D? delegate; } + +abstract class ArgumentsInteractor + extends Interactor { + A? arguments; +} diff --git a/lib/core/viper/module.dart b/lib/core/viper/module.dart index b3249799..a63d6c51 100644 --- a/lib/core/viper/module.dart +++ b/lib/core/viper/module.dart @@ -4,9 +4,13 @@ import 'package:survey/services/locator/locator_service.dart'; export 'package:survey/core/extensions/build_context.dart'; export 'package:survey/core/extensions/stream.dart'; + part 'presenter.dart'; + part 'interactor.dart'; + part 'router.dart'; + part 'view.dart'; abstract class Module extends Module { + void setArguments(Object? arguments) { + interactor.arguments = arguments as A?; + } +} + +abstract class ModuleArguments {} diff --git a/lib/core/viper/view.dart b/lib/core/viper/view.dart index b4b0b2af..b98995ea 100644 --- a/lib/core/viper/view.dart +++ b/lib/core/viper/view.dart @@ -8,6 +8,7 @@ abstract class View { abstract class ViewState extends State implements View { + @visibleForTesting static Module? overriddenModule; Module? _disposedModule; @override @@ -17,12 +18,19 @@ abstract class ViewState void initState() { super.initState(); - _getModule()?.assembly(this); + final module = _getModule(); + module?.assembly(this); } @override void didChangeDependencies() { - _disposedModule = _getModule(); + final module = _getModule(); + + if (module is ArgumentsModule) { + module.setArguments(ModalRoute.of(context)?.settings.arguments); + } + + _disposedModule = module; super.didChangeDependencies(); } diff --git a/lib/gen/configs.gen.dart b/lib/gen/configs.gen.dart index df6f316b..6b5aa214 100644 --- a/lib/gen/configs.gen.dart +++ b/lib/gen/configs.gen.dart @@ -1,7 +1,9 @@ import 'package:survey/gen/flavors.gen.dart'; import 'package:flutter/widgets.dart'; import 'package:survey/models/auth_token_info.dart'; +import 'package:survey/models/detailed_survey_info.dart'; import 'package:survey/models/survey_info.dart'; +import 'package:survey/models/survey_question_info.dart'; import 'package:survey/models/user_info.dart'; import 'package:survey/modules/forgot_password/forgot_password_module.dart'; import 'package:survey/modules/home/home_module.dart'; @@ -9,6 +11,7 @@ import 'package:survey/modules/landing/landing_module.dart'; import 'package:survey/modules/login/login_module.dart'; import 'package:survey/modules/side_menu/side_menu_module.dart'; import 'package:survey/modules/survey_detail/survey_detail_module.dart'; +import 'package:survey/services/api/api_service.dart'; part 'package:survey/configs/app.dart'; part 'package:survey/configs/factories.dart'; diff --git a/lib/models/auth_token_info.dart b/lib/models/auth_token_info.dart index 3073aa3b..2baeb6f8 100644 --- a/lib/models/auth_token_info.dart +++ b/lib/models/auth_token_info.dart @@ -8,11 +8,13 @@ class AuthTokenInfo with Mappable { @override void mapping(Mapper map) { - map("access_token", accessToken, (v) => accessToken = v as String); - map("token_type", tokenType, (v) => tokenType = v as String); - map("expires_in", expiresIn, (v) => expiresIn = v as DateTime, - DateTransform()); + map("attributes.access_token", accessToken, + (v) => accessToken = v as String); map( - "refresh_token", refreshToken, (v) => refreshToken = v as String); + "attributes.token_type", tokenType, (v) => tokenType = v as String); + map("attributes.expires_in", expiresIn, + (v) => expiresIn = v as DateTime, const DateTransform()); + map("attributes.refresh_token", refreshToken, + (v) => refreshToken = v as String); } } diff --git a/lib/models/detailed_survey_info.dart b/lib/models/detailed_survey_info.dart new file mode 100644 index 00000000..c48e0bbc --- /dev/null +++ b/lib/models/detailed_survey_info.dart @@ -0,0 +1,6 @@ +import 'package:survey/models/survey_info.dart'; +import 'package:survey/models/survey_question_info.dart'; + +class DetailedSurveyInfo extends SurveyInfo { + late List questions; +} diff --git a/lib/models/survey_info.dart b/lib/models/survey_info.dart index 4e1f495f..0495a3db 100644 --- a/lib/models/survey_info.dart +++ b/lib/models/survey_info.dart @@ -3,15 +3,17 @@ import 'package:equatable/equatable.dart'; // ignore: must_be_immutable class SurveyInfo extends Equatable with Mappable { + String? id; String? title; String? description; String? coverImageUrl; @override void mapping(Mapper map) { - map("title", title, (v) => title = v as String?); - map("description", description, (v) => description = v as String?); - map("cover_image_url", coverImageUrl, (v) { + map("id", id, (v) => id = v as String?); + map("attributes.title", title, (v) => title = v as String?); + map("attributes.description", description, (v) => description = v as String?); + map("attributes.cover_image_url", coverImageUrl, (v) { final url = v as String?; coverImageUrl = url != null ? "${url}l" : null; }); diff --git a/lib/models/survey_question_info.dart b/lib/models/survey_question_info.dart new file mode 100644 index 00000000..338b77d0 --- /dev/null +++ b/lib/models/survey_question_info.dart @@ -0,0 +1,87 @@ +import 'package:object_mapper/object_mapper.dart'; + +class SurveyQuestionInfo with Mappable { + String? id; + String? content; + int? displayOrder; + SurveyQuestionPickType? pickType; + SurveyQuestionDisplayType? displayType; + bool? isMandatory; + String? coverImageUrl; + double? coverImageOpacity; + + @override + void mapping(Mapper map) { + map( + "id", + id, + (v) => id = v as String, + ); + map( + "attributes.text", + content, + (v) => content = v as String, + ); + map( + "attributes.display_order", + displayOrder, + (v) => displayOrder = v as int, + ); + map( + "attributes.pick", + pickType, + (v) => pickType = v as SurveyQuestionPickType?, + const EnumTransform(), + ); + map( + "attributes.display_type", + displayType, + (v) => displayType = v as SurveyQuestionDisplayType?, + const EnumTransform(), + ); + map( + "attributes.is_mandatory", + isMandatory, + (v) => isMandatory = v as bool, + ); + map( + "attributes.cover_image_url", + coverImageUrl, + (v) => coverImageUrl = v as String?, + ); + map( + "attributes.cover_image_opacity", + coverImageOpacity, + (v) => coverImageOpacity = v as double?, + ); + } +} + +// ignore: avoid_implementing_value_types +class SurveyQuestionDisplayType extends Enumerable { + const SurveyQuestionDisplayType(this.rawValue); + + @override + final String rawValue; + + static const intro = SurveyQuestionDisplayType("intro"); + static const star = SurveyQuestionDisplayType("star"); + static const heart = SurveyQuestionDisplayType("heart"); + static const smiley = SurveyQuestionDisplayType("smiley"); + static const choice = SurveyQuestionDisplayType("choice"); + static const nps = SurveyQuestionDisplayType("NPS"); + static const textarea = SurveyQuestionDisplayType("textarea"); + static const textField = SurveyQuestionDisplayType("textfield"); + static const outro = SurveyQuestionDisplayType("outro"); +} + +class SurveyQuestionPickType extends Enumerable { + const SurveyQuestionPickType(this.rawValue); + + @override + final String rawValue; + + static const none = SurveyQuestionDisplayType("none"); + static const one = SurveyQuestionDisplayType("one"); + static const any = SurveyQuestionDisplayType("any"); +} diff --git a/lib/models/user_info.dart b/lib/models/user_info.dart index 61f2d443..9b271e79 100644 --- a/lib/models/user_info.dart +++ b/lib/models/user_info.dart @@ -6,7 +6,7 @@ class UserInfo with Mappable { @override void mapping(Mapper map) { - map("email", email, (v) => email = v as String); - map("avatar_url", avatarUrl, (v) => avatarUrl = v as String); + map("attributes.email", email, (v) => email = v as String); + map("attributes.avatar_url", avatarUrl, (v) => avatarUrl = v as String); } } diff --git a/lib/modules/home/components/body.dart b/lib/modules/home/components/body.dart index 8f78b7f0..ad32a8de 100644 --- a/lib/modules/home/components/body.dart +++ b/lib/modules/home/components/body.dart @@ -41,6 +41,7 @@ class Body extends StatelessWidget { isShown: isLoading, baseColor: Colors.white.withOpacity(0.12), highlightColor: Colors.white.withOpacity(0.5), + transition: BoneTransition.none, child: child!, ), child: Stack( diff --git a/lib/modules/home/home_router.dart b/lib/modules/home/home_router.dart index 406a037c..87ea44cb 100644 --- a/lib/modules/home/home_router.dart +++ b/lib/modules/home/home_router.dart @@ -9,6 +9,9 @@ class HomeRouterImpl extends HomeRouter { @override void pushToSurveyDetail( {required BuildContext context, required SurveyInfo survey}) { - context.navigator.pushNamed(SurveyDetailModule.routePath); + context.navigator.pushNamed( + SurveyDetailModule.routePath, + arguments: SurveyDetailArguments(survey: survey), + ); } } diff --git a/lib/modules/survey_detail/components/content.dart b/lib/modules/survey_detail/components/content.dart index 75462d3a..4654d933 100644 --- a/lib/modules/survey_detail/components/content.dart +++ b/lib/modules/survey_detail/components/content.dart @@ -5,62 +5,85 @@ class Content extends StatelessWidget { @override Widget build(BuildContext context) { - return Screen( - body: Stack( - fit: StackFit.expand, - children: [ - Image( - image: Assets.images.mainBackgroundDimmed, - fit: BoxFit.fill, - ), - SafeArea( - child: Column( - children: [ - NavigationBar(), - Expanded( - child: Container( - margin: const EdgeInsets.fromLTRB(20, 0, 20, 0), - child: Column( - children: [ - const Text( - "Working from home Check-In", - style: TextStyle( - color: Colors.white, - fontSize: 34, + final state = + context.findAncestorStateOfType<_SurveyDetailViewImplState>()!; + + return StreamsSelector0.value( + stream: state.isProgressHUDShown, + builder: (_, isShown, child) => ProgressHUD( + isShow: isShown, + child: child!, + ), + child: Screen( + body: Stack( + fit: StackFit.expand, + children: [ + StreamsSelector0.value( + stream: state._survey, + builder: (_, survey, __) => Image( + image: NetworkImage(survey.coverImageUrl!), + fit: BoxFit.fill, + ), + ), + SafeArea( + child: Column( + children: [ + NavigationBar(), + Expanded( + child: Container( + margin: const EdgeInsets.fromLTRB(20, 0, 20, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StreamsSelector0.value( + stream: state._survey, + builder: (_, survey, __) => Text( + survey.title!, + style: const TextStyle( + color: Colors.white, + fontSize: 34, + ), + ), ), - ), - const SizedBox( - height: 17, - ), - Text( - "We would like to know how you feel about our work from home (WFH) experience.", - style: TextStyle( - color: Colors.white.withOpacity(0.7), - fontSize: 17, + const SizedBox( + height: 17, ), - ), - Expanded(child: Container()), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ConstrainedBox( - constraints: const BoxConstraints( - minWidth: 140, - ), - child: Button( - title: AppLocalizations.of(context)! - .surveyDetailScreenStartSurveyButtonTitle, - ), + StreamsSelector0.value( + stream: state._survey, + builder: (_, survey, __) => Text( + survey.description!, + style: TextStyle( + color: Colors.white.withOpacity(0.7), + fontSize: 17, ), - ]), - ], + ), + ), + Expanded(child: Container()), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 140, + ), + child: Button( + onPressed: () => state + .delegate?.startSurveyButtonDidTap + .add(null), + title: AppLocalizations.of(context)! + .surveyDetailScreenStartSurveyButtonTitle, + ), + ), + ]), + ], + ), ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/modules/survey_detail/survey_detail_interactor.dart b/lib/modules/survey_detail/survey_detail_interactor.dart index 62097065..690763ad 100644 --- a/lib/modules/survey_detail/survey_detail_interactor.dart +++ b/lib/modules/survey_detail/survey_detail_interactor.dart @@ -1,8 +1,30 @@ part of 'survey_detail_module.dart'; -abstract class SurveyDetailInteractorDelegate {} +abstract class SurveyDetailInteractorDelegate { + BehaviorSubject get detailedSurveyDidFetch; -abstract class SurveyDetailInteractor - extends Interactor {} + BehaviorSubject get detailedSurveyDidFailToFetch; +} -class SurveyDetailInteractorImpl extends SurveyDetailInteractor {} +abstract class SurveyDetailInteractor extends ArgumentsInteractor< + SurveyDetailInteractorDelegate, SurveyDetailArguments> { + SurveyInfo get survey; + + void fetchDetailedSurvey(); +} + +class SurveyDetailInteractorImpl extends SurveyDetailInteractor { + final SurveyRepository _surveyRepository = locator.get(); + + @override + SurveyInfo get survey => arguments!.survey; + + @override + void fetchDetailedSurvey() { + _surveyRepository + .fetchDetailedSurvey(survey.id!) + .then((value) => delegate?.detailedSurveyDidFetch.add(value)) + .onError((exception, _) => + delegate?.detailedSurveyDidFailToFetch.add(exception)); + } +} diff --git a/lib/modules/survey_detail/survey_detail_module.dart b/lib/modules/survey_detail/survey_detail_module.dart index bfc60933..201efe2b 100644 --- a/lib/modules/survey_detail/survey_detail_module.dart +++ b/lib/modules/survey_detail/survey_detail_module.dart @@ -1,11 +1,17 @@ import 'package:flutter/material.dart' hide Router; import 'package:flutter/widgets.dart' hide Router; +import 'package:streams_provider/streams_provider.dart'; +import 'package:survey/components/alert/alert.dart'; import 'package:survey/components/button/button.dart'; +import 'package:survey/components/common/progress_hud.dart'; import 'package:survey/components/navigation_bar/navigation_bar.dart'; import 'package:survey/core/viper/module.dart'; -import 'package:survey/gen/assets.gen.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:survey/models/detailed_survey_info.dart'; +import 'package:survey/models/survey_info.dart'; import 'package:survey/modules/screen.dart'; +import 'package:survey/repositories/survey_repository.dart'; +import 'package:survey/services/locator/locator_service.dart'; part 'survey_detail_view.dart'; @@ -17,8 +23,12 @@ part 'survey_detail_router.dart'; part 'components/content.dart'; -class SurveyDetailModule extends Module { +class SurveyDetailModule extends ArgumentsModule< + SurveyDetailView, + SurveyDetailInteractor, + SurveyDetailPresenter, + SurveyDetailRouter, + SurveyDetailArguments> { static const routePath = "/survey/detail"; @override @@ -26,3 +36,9 @@ class SurveyDetailModule extends Module {} class SurveyDetailPresenterImpl extends SurveyDetailPresenter - implements SurveyDetailViewDelegate, SurveyDetailInteractorDelegate {} + implements SurveyDetailViewDelegate, SurveyDetailInteractorDelegate { + SurveyDetailPresenterImpl() { + stateDidInit.voidListen(_stateDidInit).addTo(disposeBag); + startSurveyButtonDidTap + .voidListen(_startSurveyButtonDidTap) + .addTo(disposeBag); + + detailedSurveyDidFetch.listen(_detailedSurveyDidFetch).addTo(disposeBag); + detailedSurveyDidFailToFetch + .listen(_detailedSurveyDidFailToFetch) + .addTo(disposeBag); + } + + @override + final detailedSurveyDidFailToFetch = BehaviorSubject(); + + @override + final detailedSurveyDidFetch = BehaviorSubject(); + + @override + final alertDialogDidClose = BehaviorSubject(); + + @override + final stateDidInit = BehaviorSubject(); + + @override + final startSurveyButtonDidTap = BehaviorSubject(); + + void _stateDidInit() { + view.setSurvey(interactor.survey); + } + + void _startSurveyButtonDidTap() { + view.showProgressHUD(); + interactor.fetchDetailedSurvey(); + } + + void _detailedSurveyDidFetch(DetailedSurveyInfo survey) { + view.dismissProgressHUD(); + router.pushToSurveyQuestionsScreen(view.context, survey: survey); + } + + void _detailedSurveyDidFailToFetch(Exception exception) { + view.dismissProgressHUD(); + view.alert(exception); + } +} diff --git a/lib/modules/survey_detail/survey_detail_router.dart b/lib/modules/survey_detail/survey_detail_router.dart index 1a5259e8..67b8b080 100644 --- a/lib/modules/survey_detail/survey_detail_router.dart +++ b/lib/modules/survey_detail/survey_detail_router.dart @@ -1,5 +1,12 @@ part of 'survey_detail_module.dart'; -abstract class SurveyDetailRouter extends Router {} +abstract class SurveyDetailRouter extends Router { + void pushToSurveyQuestionsScreen(BuildContext context, + {required DetailedSurveyInfo survey}); +} -class SurveyDetailRouterImpl extends SurveyDetailRouter {} +class SurveyDetailRouterImpl extends SurveyDetailRouter { + @override + void pushToSurveyQuestionsScreen(BuildContext context, + {required DetailedSurveyInfo survey}) {} +} diff --git a/lib/modules/survey_detail/survey_detail_view.dart b/lib/modules/survey_detail/survey_detail_view.dart index d77600c5..b070fd69 100644 --- a/lib/modules/survey_detail/survey_detail_view.dart +++ b/lib/modules/survey_detail/survey_detail_view.dart @@ -1,8 +1,15 @@ part of 'survey_detail_module.dart'; -abstract class SurveyDetailViewDelegate {} +abstract class SurveyDetailViewDelegate implements AlertViewMixinDelegate { + BehaviorSubject get stateDidInit; -abstract class SurveyDetailView extends View {} + BehaviorSubject get startSurveyButtonDidTap; +} + +abstract class SurveyDetailView extends View + with ProgressHUDViewMixin, AlertViewMixin { + void setSurvey(SurveyInfo survey); +} class SurveyDetailViewImpl extends StatefulWidget { const SurveyDetailViewImpl({Key? key}) : super(key: key); @@ -12,9 +19,29 @@ class SurveyDetailViewImpl extends StatefulWidget { } class _SurveyDetailViewImplState extends ViewState implements SurveyDetailView { + SurveyDetailModule, SurveyDetailViewDelegate> + with ProgressHUDViewMixin, AlertViewMixin + implements SurveyDetailView { + late final _survey = BehaviorSubject(); + + @override + void initState() { + super.initState(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + delegate?.stateDidInit.add(null); + } + @override Widget build(BuildContext context) { return const Content(); } + + @override + void setSurvey(SurveyInfo survey) { + _survey.add(survey); + } } diff --git a/lib/repositories/survey_repository.dart b/lib/repositories/survey_repository.dart index c6b7bb0c..0423d9de 100644 --- a/lib/repositories/survey_repository.dart +++ b/lib/repositories/survey_repository.dart @@ -1,3 +1,4 @@ +import 'package:survey/models/detailed_survey_info.dart'; import 'package:survey/models/survey_info.dart'; import 'package:survey/services/api/survey/survey_api_service.dart'; import 'package:survey/services/local_storage/local_storage_service.dart'; @@ -11,6 +12,8 @@ abstract class SurveyRepository { Future> fetchSurveysFromCached(); Future> fetchSurveysFromRemote(); + + Future fetchDetailedSurvey(String id); } class SurveyRepositoryImpl implements SurveyRepository { @@ -45,4 +48,10 @@ class SurveyRepositoryImpl implements SurveyRepository { return list.items; } + + @override + Future fetchDetailedSurvey(String id) { + final params = SurveyInfoParams(id: id); + return _surveyApiService.info(params: params); + } } diff --git a/lib/services/api/api_raw_response.dart b/lib/services/api/api_raw_response.dart new file mode 100644 index 00000000..9f19cf18 --- /dev/null +++ b/lib/services/api/api_raw_response.dart @@ -0,0 +1,30 @@ +part of 'api_service.dart'; + +class ApiRawResponse with Mappable { + ApiRawObject? data; + Map? meta; + List? included; + + @override + void mapping(Mapper map) { + map("data", data, (v) => data = v as ApiRawObject?); + map>( + "meta", meta, (v) => meta = v as Map?); + map( + "included", included, (v) => included = v as List?); + } +} + +class ApiRawObject with Mappable { + String? id; + String? type; + Map? attributes; + + @override + void mapping(Mapper map) { + map("id", id, (v) => id = v as String); + map("type", type, (v) => type = v as String); + map>("attributes", attributes, + (v) => attributes = v as Map); + } +} diff --git a/lib/services/api/api_service.dart b/lib/services/api/api_service.dart index ca369a24..dfb0cec3 100644 --- a/lib/services/api/api_service.dart +++ b/lib/services/api/api_service.dart @@ -12,26 +12,14 @@ part 'api_service_register.dart'; part 'api_list_object.dart'; +part 'api_raw_response.dart'; + abstract class ApiService { - Future call({ - required HttpMethod method, - String? baseUrl, - required String endpoint, - ApiParams? params, - bool requiresAuthentication = true, - String? token, - String? tokenType, - }); - - Future> callForList({ - required HttpMethod method, - String? baseUrl, - required String endpoint, - ApiParams? params, - bool requiresAuthentication = true, - String? token, - String? tokenType, - }); + Future call( + {required HttpMethod method, String? baseUrl, required String endpoint, ApiParams? params, bool requiresAuthentication = true, String? token, String? tokenType,}); + + Future> callForList( + {required HttpMethod method, String? baseUrl, required String endpoint, ApiParams? params, bool requiresAuthentication = true, String? token, String? tokenType,}); void configureGlobalBaseUrl(String? baseUrl); @@ -45,15 +33,8 @@ class ApiServiceImpl implements ApiService { final HttpService _httpService = locator.get(); @override - Future call({ - required HttpMethod method, - String? baseUrl, - required String endpoint, - ApiParams? params, - bool requiresAuthentication = true, - String? token, - String? tokenType, - }) async { + Future call( + {required HttpMethod method, String? baseUrl, required String endpoint, ApiParams? params, bool requiresAuthentication = true, String? token, String? tokenType,}) async { final response = await _request( method: method, baseUrl: baseUrl, @@ -61,22 +42,14 @@ class ApiServiceImpl implements ApiService { params: params, requiresAuthentication: requiresAuthentication, token: token, - tokenType: tokenType, - ); + tokenType: tokenType,); return _convertResponseToObject(response); } @override - Future> callForList({ - required HttpMethod method, - String? baseUrl, - required String endpoint, - ApiParams? params, - bool requiresAuthentication = true, - String? token, - String? tokenType, - }) async { + Future> callForList( + {required HttpMethod method, String? baseUrl, required String endpoint, ApiParams? params, bool requiresAuthentication = true, String? token, String? tokenType,}) async { final response = await _request( method: method, baseUrl: baseUrl, @@ -84,8 +57,7 @@ class ApiServiceImpl implements ApiService { params: params, requiresAuthentication: requiresAuthentication, token: token, - tokenType: tokenType, - ); + tokenType: tokenType,); return _convertResponseToListObject(response); } @@ -111,15 +83,8 @@ class ApiServiceImpl implements ApiService { } } - Future> _request({ - required HttpMethod method, - String? baseUrl, - required String endpoint, - ApiParams? params, - required bool requiresAuthentication, - String? token, - String? tokenType, - }) async { + Future> _request( + {required HttpMethod method, String? baseUrl, required String endpoint, ApiParams? params, required bool requiresAuthentication, String? token, String? tokenType,}) async { final String? finalBaseUrl = baseUrl ?? _baseUrl; assert(finalBaseUrl != null); @@ -135,42 +100,40 @@ class ApiServiceImpl implements ApiService { final url = finalBaseUrl! + endpoint; try { return await _httpService.request( - method: method, - data: params?.toJson(), - url: url, - headers: headers, - ) as Map; - } on HttpException catch (e) { + method: method, data: params?.toJson(), url: url, headers: headers,) as Map; + } + on HttpException catch (e) { throw ApiException.fromHttpException(e) ?? e; } } T _convertResponseToObject(Map response) { + if (T == ApiRawResponse) { + return Mapper.fromJson(response).toObject()!; + } + if (response["data"] == null && T.toString() == "void") { return null as T; } - if (response["data"] is! Map || - response["data"]["attributes"] is! Map) { + if (response["data"] is! Map) { throw ApiException.invalidResponseStructure; } - return Mapper.fromJson( - response["data"]["attributes"] as Map) - .toObject()!; + return Mapper.fromJson(response["data"] as Map).toObject< + T>()!; } - ApiListObject _convertResponseToListObject( - Map response) { + ApiListObject _convertResponseToListObject(Map response) { if (response["data"] is! List) { throw ApiException.invalidResponseStructure; } final items = (response["data"] as List) - .where((e) => e["attributes"] is Map) - .map((e) => Mapper.fromJson(e["attributes"] as Map) - .toObject()!) - .toList(); + .map((e) => Mapper.fromJson(e as Map) + .toObject()!) + .toList(); return ApiListObject(items: items); } diff --git a/lib/services/api/survey/survey_api_service.dart b/lib/services/api/survey/survey_api_service.dart index 155a7250..e3fbe91f 100644 --- a/lib/services/api/survey/survey_api_service.dart +++ b/lib/services/api/survey/survey_api_service.dart @@ -1,5 +1,7 @@ import 'package:object_mapper/object_mapper.dart'; +import 'package:survey/models/detailed_survey_info.dart'; import 'package:survey/models/survey_info.dart'; +import 'package:survey/models/survey_question_info.dart'; import 'package:survey/services/api/api_service.dart'; import 'package:survey/services/http/http_service.dart'; import 'package:survey/services/locator/locator_service.dart'; @@ -11,7 +13,8 @@ part 'params/survey_info_params.dart'; abstract class SurveyApiService { static const endpoint = "/surveys"; - Future info({required SurveyInfoParams params}); + Future info({required SurveyInfoParams params}); + Future> list({required SurveyListParams params}); } @@ -19,11 +22,26 @@ class SurveyApiServiceImpl implements SurveyApiService { final ApiService _apiService = locator.get(); @override - Future info({required SurveyInfoParams params}) { - return _apiService.call( + Future info({required SurveyInfoParams params}) async { + final rawResponse = await _apiService.call( method: HttpMethod.get, endpoint: "${SurveyApiService.endpoint}/${params.id}", ); + + final info = Mapper.fromJson(rawResponse.data!.toJson()) + .toObject()!; + + final questions = List.empty(growable: true); + final included = (rawResponse.included ?? []) + .where((element) => element.type == "question"); + + for (final ApiRawObject rawObject in included) { + questions.add( + Mapper.fromJson(rawObject.toJson()).toObject()!); + } + info.questions = questions; + + return info; } @override diff --git a/pubspec.lock b/pubspec.lock index 706ceeaf..fac0361e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -475,7 +475,7 @@ packages: name: object_mapper url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" package_config: dependency: transitive description: @@ -685,7 +685,7 @@ packages: name: skeletor url: "https://pub.dartlang.org" source: hosted - version: "0.0.2" + version: "0.0.3" sky_engine: dependency: transitive description: flutter @@ -732,7 +732,7 @@ packages: name: streams_provider url: "https://pub.dartlang.org" source: hosted - version: "0.1.0" + version: "0.1.1" string_scanner: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 325e9aa0..7c070808 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,14 +35,14 @@ dependencies: get_it: ^7.0.0 json_annotation: ^4.0.1 enumerated_class: ^0.0.1+1 - object_mapper: ^1.1.0 - streams_provider: ^0.1.0 + object_mapper: ^1.2.0 + streams_provider: ^0.1.1 shared_preferences: ^2.0.5 modal_progress_hud_nsn: ^0.1.0-nullsafety-1 adaptive_dialog: ^0.10.0+5 async: ^2.5.0 carousel_slider: ^4.0.0-nullsafety.0 - skeletor: ^0.0.2 + skeletor: ^0.0.3 simple_gesture_detector: ^0.2.0 equatable: ^2.0.2 tuple: ^2.0.0 diff --git a/test/modules/home/home_router_test.dart b/test/modules/home/home_router_test.dart index 357b2eeb..8633621d 100644 --- a/test/modules/home/home_router_test.dart +++ b/test/modules/home/home_router_test.dart @@ -26,12 +26,15 @@ void main() { beforeEach(() { when(buildContext.findAncestorStateOfType()) .thenReturn(navigatorState); - when(navigatorState.pushNamed(any)).thenAnswer((_) => Future.value()); - router.pushToSurveyDetail(context: buildContext, survey: SurveyInfo()); + when(navigatorState.pushNamed(any, arguments: anyNamed("arguments"))) + .thenAnswer((_) => Future.value()); + router.pushToSurveyDetail( + context: buildContext, survey: SurveyInfo()); }); it("triggers navigator to push to Survey Detail screen", () { - final routePath = verify(navigatorState.pushNamed(captureAny)) + final routePath = verify(navigatorState.pushNamed(captureAny, + arguments: anyNamed("arguments"))) .captured .single as String; expect(routePath, SurveyDetailModule.routePath);