diff --git a/README.md b/README.md index 5b90e68..718e2d8 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,39 @@ # mc_gallery ## Dart docs explanation +The first line of docs are like normal Dart docs. The second line is less of a 'further' + explanation, and more of an active commentary for the viewer. ## Emulation +### Main API +The unsplash_images_api contains code for fetching data from an external website. Since I have used 'source.unsplash.com', there is no actual daa +data (in the form of protected URLs or raw JSON like Facebook API) that is sent back. So this base URL can directly be used to cache the images.
+ +But I wanted to give a more realistic procedure. So, I manually serialize the same data about the URL to JSON and then deserialize it, just a step +further on needlessly. In addition to it, constant fetch delays have added in as well. But in theory, the first serialization can be replaced by actual data-fetch, +while the rest works as expected + +### Search API +Since websites also have a server-side search call that is available, functionality had been included to make use of that too. But again, this is mocked +by the steps mentioned above. ## Maintaining scope -It's an 'assignment' - -## Model vs. DTO +Since it's just a demo assignment, I have tried to keep the scope of the functionalities small. By that, I mean I have not fully optimized performance-critical sections, +or fixed some rare-case bugs. But instead I have left either a 'todo' for them, or mentioned that in the docs themselves ## Extra quirks -Just because I had those assets lying around \ No newline at end of file +A lot of the empty directories and unused asset files are left in, as I used my project templated from my other templates. I decided to leave them in, in any case since they +may give a better indication about how I code, should you choose to look into them. A lot of the functionality that I used, is something that I have already made for my previous project(s), +so it was not as much work as it seems. One of my recent ones, (luckily) literally had a Gallery feature as well, for congregating social media posts into one place. So a lot of assets were reused from there. + +Some of the extra quirks that I added in are: +1. Dark mode support. +2. Live search -> both for locally (cached) image files, and Web (using a website's search API). +3. Clearing image cache on app going to background, and then restarting the program (way easier on an emulator using hot restart). + + I found it easier to directly use the change in app life cycle, than implement a pull-down-refresh. +4. Logging. +5. Local storage of favourites state -> on restart, the favourites stay. + + Not very useful on a real device as app restart can only be triggered with app life cycle state change, which would clear the download cache, + which would be an invalidate them -> so clearing them away with the image cache for now. +6. Web API unit test +7. Project UML diagram using `dcdg` and a graphviz pipeline in `docs\` directory. \ No newline at end of file diff --git a/doc/Class UML.svg b/doc/Class UML.svg new file mode 100644 index 0000000..e9be746 --- /dev/null +++ b/doc/Class UML.svg @@ -0,0 +1,983 @@ +mc_gallery::locator.dartmc_gallery::features::home::abstracts::images_api.dartmc_gallery::features::home::api::unsplash_images_api.dartmc_gallery::features::core::services::logging_service.dartdart::mathmc_gallery::features::home::data::enums::search_option.dartdart::coremc_gallery::features::home::data::models::image_model.dartmc_gallery::features::home::data::dtos::image_model_dto.dartmc_gallery::features::home::views::gallery::gallery_view_model.dartmc_gallery::features::home::services::images_service.dartmc_gallery::features::core::services::navigation_service.dartmc_gallery::features::home::services::image_cache_manager_service.dartflutter::src::foundation::change_notifier.dartmc_gallery::features::core::abstracts::base_view_model.dartmc_gallery::features::home::views::gallery::gallery_view.dartflutter::src::widgets::framework.dartmc_gallery::features::home::views::image_carousel::image_carousel_view_model.dartmc_gallery::features::home::views::image_carousel::image_carousel_view.dartmc_gallery::features::core::services::local_storage_service.dartdart::collectionmc_gallery::features::core::utils::mutex.dartdart::asyncmc_gallery::features::core::services::app_lifecycle_service.dartflutter_cache_manager::src::cache_managers::default_cache_manager.dartmc_gallery::features::home::widgets::custom_wrap.dartnullmc_gallery::features::core::abstracts::app_setup.dartmc_gallery::features::core::abstracts::router::routes.dartmc_gallery::features::core::abstracts::router::app_router.dartgo_router::src::router.dartmc_gallery::features::core::data::constants::const_text.dartflutter::src::painting::text_style.dartmc_gallery::features::core::data::constants::const_sorters.dartmc_gallery::features::core::data::constants::const_media.dartmc_gallery::features::core::data::constants::const_colors.dartdart::uiflutter::src::material::colors.dartflutter::src::material::theme_data.dartflutter::src::cupertino::theme.dartmc_gallery::features::core::data::constants::const_values.dartmc_gallery::features::core::data::constants::const_durations.dartmc_gallery::features::core::data::enums::view_model_state.dartmc_gallery::features::core::views::error_page_view.darthive::hive.dartmc_gallery::features::core::services::overlay_service.dartflutter::src::widgets::binding.dartmc_gallery::features::core::services::connections_service.dartinternet_connection_checker::internet_connection_checker.dartconnectivity_plus::connectivity_plus.darttalker::src::talker.dartmc_gallery::features::core::widgets::animated_column.dartflutter::src::rendering::flex.dartflutter::src::painting::basic_types.dartmc_gallery::features::core::widgets::gap.dartflutter::src::rendering::box.dartflutter::src::animation::curves.dartflutter::src::animation::animation_controller.dartflutter::src::widgets::ticker_provider.dartflutter::src::rendering::sliver.dartmc_gallery::features::core::widgets::state::multi_value_listenable_builder.dartmc_gallery::features::core::widgets::state::view_model_builder.dartmc_gallery::features::core::widgets::mcg_scaffold.dartflutter::src::material::app_bar.dartmc_gallery::app.dartLocatorGetIt instance()T locate()Future<void> setup()void _registerAPIs()void _registerViewModels()FutureOr<void> _registerServices()FutureOr<void> _registerRepos()void _registerSingletons()ImagesApiFutureOr<Iterable<ImageModelDTO>> fetchImageUri()FutureOr<Iterable<ImageModelDTO>> searchImages()UnsplashImagesApiLoggingService _loggingServiceRandom randomUnsplashImagesApi locateFutureOr<Iterable<ImageModelDTO>> fetchImageUri()FutureOr<Iterable<ImageModelDTO>> searchImages()Uri _imageUrlGenerator()LoggingServiceTalker _talkerLoggingService locatevoid Function(dynamic, [Object?, StackTrace?]) finevoid Function(dynamic, [Object?, StackTrace?]) goodvoid Function(dynamic, [Object?, StackTrace?]) infovoid Function(dynamic, [Object, StackTrace]) warningvoid Function(dynamic, [Object?, StackTrace?]) errorvoid Function(Object, [StackTrace, dynamic]) handlevoid Function(Error, [StackTrace, dynamic]) handleErrorvoid Function(Exception, [StackTrace?, dynamic]) handleExceptionvoid successfulInit()void successfulDispose()void addLoggingInterceptor()RandomSearchOptionint indexList<SearchOption> valuesSearchOption localSearchOption webEnumImageModelUri uriint imageIndexString imageNamebool isFavouriteImageModel copyWith()ImageModelDTOUri uriint imageIndexString imageNameMap<String, dynamic> toJson()GalleryViewModelImagesService _imagesServiceNavigationService _navigationServiceImageCacheManagerService _imageCacheManagerServiceLoggingService _loggingServiceValueNotifier<bool> _isDisplayingPressingPromptValueNotifier<bool> _isSearchingNotifierValueNotifier<SearchOption> _searchOptionNotifierValueNotifier<List<ImageModel>> _imageSearchResultsNotifierValueNotifier<bool> _isViewingFavouriteNotifierValueListenable<bool> isDisplayingPressingPromptValueListenable<bool> isSearchingListenableValueListenable<SearchOption> searchOptionListenableValueListenable<List<ImageModel>> imageSearchResultsListenableValueListenable<bool> isViewingFavouriteListenableFuture<void> lastQueryResultDoneIterable<ImageModel> favouriteImageModelsIterable<ImageModel> imageModelsFuture<void> initImageFetchIsDoneGalleryViewModel locateFuture<void> initialise()Future<void> dispose()Future<void> onSearchTermUpdate()void searchPressed()void onSearchOptionChanged()void onFavouriteViewChange()void updateImageFavouriteStatus()void onPromptPressed()double? downloadProgressValue()void pushImageCarouselView()ImagesServiceImagesApi _imagesApiLocalStorageService _localStorageServiceLoggingService _loggingServiceLinkedHashMap<String, ImageModel> _imageModelsMutex _searchMutexCompleter<dynamic> _initAwaiterIterable<ImageModel> imageModelsFuture<dynamic> initAwaiterint firstAvailableImageIndexint lastAvailableImageIndexint numberOfImagesFuture<void> lastQueryIsCompletedImagesService locateFuture<void> _init()ImageModel imageModelAt()Future<List<ImageModel>> searchImages()void updateImageFavouriteStatus()NavigationServiceMcgRouter _mcgRouterNavigationService locatevoid pushImageCarouselView()ImageCacheManagerServiceAppLifecycleService _appLifecycleServiceLocalStorageService _localStorageServiceLoggingService _loggingServiceDefaultCacheManager _cacheManagerImageCacheManagerService locateFuture<void> emptyCache()Future<void> _init()ValueNotifier<bool>ValueNotifier<SearchOption>ValueNotifier<List<ImageModel>>ValueListenable<bool>ValueListenable<SearchOption>ValueListenable<List<ImageModel>>ValueNotifier<ImageModel>ValueListenable<ImageModel>ValueNotifier<ViewModelState>ValueListenable<ViewModelState>ChangeNotifierValueNotifier<InternetConnectionStatus>ValueNotifier<ConnectivityResult>ValueListenable<InternetConnectionStatus>ValueListenable<ConnectivityResult>BaseViewModelValueNotifier<bool> _isInitialisedValueNotifier<bool> _isBusyValueNotifier<bool> _hasErrorValueNotifier<ViewModelState> _stateLoggingService _loggingServiceString? _errorMessagedynamic stringsValueListenable<bool> isInitialisedValueListenable<bool> isBusyValueListenable<bool> hasErrorValueListenable<ViewModelState> stateString errorMessagebool Function() _mountedvoid initialise()void setBusy()void setError()void dispose()void ifMounted()double width()double height()GalleryViewWidget build()_SearchBoxGalleryViewModel galleryViewModelWidget build()_DownloadedGalleryViewGalleryViewModel galleryViewModelWidget build()_StarrableImageGalleryViewModel galleryViewModelImageModel imageModelState<_StarrableImage> createState()_StarrableImageStatebool isMarkedFavouritevoid initState()Widget build()_SearchGalleryViewGalleryViewModel galleryViewModelWidget build()StatelessWidgetStatefulWidgetStateLeafRenderObjectWidgetWidgetImageCarouselViewModelImagesService _imagesServiceLoggingService _loggingServiceValueNotifier<ImageModel> _currentImageModelNotifierValueListenable<ImageModel> currentImageModelListenableString currentImageUrlString currentImageKeyString currentImageNameint currentImageIndexint numberOfImagesbool hasPreviousImagebool hasNextImageImageCarouselViewModel locateFuture<void> initialise()Future<void> dispose()void swipedTo()double? downloadProgressValue()ImageCarouselViewArgumentsint imageIndexKeyImageCarouselViewImageCarouselViewArguments imageCarouselViewArgumentsWidget build()LocalStorageServiceLoggingService _loggingServiceBox<bool> _userBoxString _userBoxKeyIterable<bool> storedFavouritesStatesLocalStorageService locateFuture<void> _init()void initNewFavourites()void updateFavourite()void resetFavourites()LinkedHashMap<String, ImageModel>Queue<Completer<dynamic>>MutexQueue<Completer<dynamic>> _completerQueueFuture<void> lastOperationCompletionAwaiterFutureOr<T> lockAndRun()Completer<dynamic>StreamController<AppLifecycleState>AppLifecycleServiceLoggingService _loggingServiceStreamController<AppLifecycleState> _lifecycleStateStreamControllerMap<String, StreamSubscription<dynamic>> _appLifecycleSubscriptionsAppLifecycleState? _appLifeCycleStateAppLifecycleState? appLifeCycleStateAppLifecycleService locateFuture<void> dispose()void didChangeAppLifecycleState()void addListener()Future<void> removeListener()DefaultCacheManagerCustomWrapList<Widget> childrenWidget build()bool Function()void Function(Object, StackTrace)void Function(dynamic, [Object, StackTrace])void Function(Object, [StackTrace, dynamic])void Function(Error, [StackTrace, dynamic])void Function(Exception, [StackTrace, dynamic])Widget Function(BuildContext, List<dynamic>, Widget)Widget Function(BuildContext, T)T Function()dynamic Function()AppSetupList<Locale> supportedLocalesvoid Function(Object, StackTrace) onUncaughtExceptionFuture<void> initialise()Locale resolveLocale()Future<void> _setupStrings()RoutesInfoString routePathString routeNameRoutesint indexList<Routes> valuesRoutes homeMcgRouterMcgRouter _mcgRouterGoRouter routerMcgRouter locateGoRouterConstTextTextStyle _imageOverlayTextStyleTextStyle imageOverlayTextStyle()TextStyleConstSortersint stringsSimilarityTarget()ConstMediaString favStarFilledString favStarOutlineSvgPicture buildIcon()ConstColoursColor galleryBackgroundColourMaterialColor redColor whiteColor blackColor transparentConstThemesThemeData materialLightThemeThemeData materialDarkThemeCupertinoThemeData cupertinoLightThemeCupertinoThemeData cupertinoDarkThemeThemeData cupertinoThemeLightHackThemeData cupertinoThemeDarkHackColorAppLifecycleStateTextDirectionTextBaselineMaterialColorThemeDataCupertinoThemeDataConstValuesString httpsSchemeString imagesHostServerList<String> imagesHostUrlPathSegmentsint numberOfImagesint minImageSizeint maxImageSizeint defaultEmulatedLatencyMillisConstDurationsDuration tripleDefaultAnimationDurationDuration doubleDefaultAnimationDurationDuration oneAndHalfDefaultAnimationDurationDuration defaultAnimationDurationDuration halfDefaultAnimationDurationDuration quarterDefaultAnimationDurationDuration zeroViewModelStateint indexList<ViewModelState> valuesViewModelState isInitialisingViewModelState isInitialisedViewModelState isBusyViewModelState hasErrorErrorPageViewException? errorWidget build()Box<bool>OverlayServiceLoggingService _loggingServiceMap<int, OverlayEntry> _overlayEntryMapOverlayService locatevoid insertOverlayEntry()void removeOverlayEntry()void dispose()WidgetsBindingObserverConnectionsServiceInternetConnectionChecker _internetConnectionCheckerConnectivity _connectivityLoggingService _loggingServiceValueNotifier<InternetConnectionStatus> _internetConnectionStatusNotifierValueNotifier<ConnectivityResult> _connectivityResultNotifierValueListenable<InternetConnectionStatus> internetConnectionStatusListenableValueListenable<ConnectivityResult> connectivityResultListenableConnectionsService locateFuture<void> _init()Future<void> dispose()InternetConnectionCheckerConnectivityTalkerAnimatedColumnDuration durationint maxAnimatingChildrenMainAxisAlignment mainAxisAlignmentMainAxisSize mainAxisSizeCrossAxisAlignment crossAxisAlignmentTextDirection? textDirectionVerticalDirection verticalDirectionTextBaseline? textBaselineList<Widget> childrenWidget build()MainAxisAlignmentMainAxisSizeCrossAxisAlignmentVerticalDirectionGapdouble sizeGap size4Gap size8Gap size16Gap size24Gap size32Gap size64RenderGap createRenderObject()void updateRenderObject()RenderGapdouble _gapdouble gapvoid performLayout()AnimatedGapDuration durationdouble gapCurve curveState<AnimatedGap> createState()_AnimatedGapStateAnimationController _controllervoid didUpdateWidget()Widget build()AnimatedSliverGapDuration durationdouble gapCurve curveState<AnimatedSliverGap> createState()_AnimatedSliverGapStateAnimationController _controllervoid didUpdateWidget()Widget build()SliverGapdouble gapSliverGap size4SliverGap size8SliverGap size16SliverGap size24SliverGap size32SliverGap size64RenderSliverGap createRenderObject()void updateRenderObject()RenderSliverGapdouble _gapdouble gapvoid performLayout()RenderBoxCurveAnimationControllerSingleTickerProviderStateMixinRenderSliverMultiValueListenableBuilderList<ValueListenable<dynamic>> valueListenablesWidget? childWidget Function(BuildContext, List<dynamic>, Widget?) builderWidget build()ViewModelBuilderWidget Function(BuildContext, T) _builderT Function() _viewModelBuilderdynamic Function()? _argumentBuilder_ViewModelBuilderState<T> createState()_ViewModelBuilderStateT _viewModelvoid initState()void dispose()Widget build()McgScaffoldAppBar? appBarValueListenable<bool>? bodyBuilderWaiterWidget? bodyWidget? waitingWidgetbool forceInternetCheckConnectionsService _connectionsServiceOverlayService _overlayServiceWidget build()void _handleOverlayDisplay()AppBarMcgAppWidget build() \ No newline at end of file diff --git a/lib/features/core/data/constants/const_values.dart b/lib/features/core/data/constants/const_values.dart index c8a9a7d..5aea8af 100644 --- a/lib/features/core/data/constants/const_values.dart +++ b/lib/features/core/data/constants/const_values.dart @@ -1,7 +1,7 @@ abstract class ConstValues { static const String httpsScheme = 'https'; - static const String backendHost = 'source.unsplash.com'; - static const List backendUrlPathSegments = ['user', 'c_v_r']; + static const String imagesHostServer = 'source.unsplash.com'; + static const List imagesHostUrlPathSegments = ['user', 'c_v_r']; static const int numberOfImages = 25; static const int minImageSize = 50; diff --git a/lib/features/home/api/unsplash_images_api.dart b/lib/features/home/api/unsplash_images_api.dart index 283fcab..67c29ea 100644 --- a/lib/features/home/api/unsplash_images_api.dart +++ b/lib/features/home/api/unsplash_images_api.dart @@ -93,8 +93,8 @@ class UnsplashImagesApi implements ImagesApi { Uri _imageUrlGenerator({required int imageSide}) => Uri( scheme: ConstValues.httpsScheme, - host: ConstValues.backendHost, - pathSegments: [...ConstValues.backendUrlPathSegments, '${imageSide}x$imageSide'], + host: ConstValues.imagesHostServer, + pathSegments: [...ConstValues.imagesHostUrlPathSegments, '${imageSide}x$imageSide'], ); static UnsplashImagesApi get locate => Locator.locate(); diff --git a/lib/features/home/views/image_carousel/image_carousel_view.dart b/lib/features/home/views/image_carousel/image_carousel_view.dart index 235f2e9..185e7d1 100644 --- a/lib/features/home/views/image_carousel/image_carousel_view.dart +++ b/lib/features/home/views/image_carousel/image_carousel_view.dart @@ -105,6 +105,8 @@ class ImageCarouselView extends StatelessWidget { const Gap(24), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), + // Assuming that this data is coming from an external CRM, if it is coming with the + // image itself, then add it to the DTO and the Model as well, and access it here. child: MarkdownBody(data: model.strings.imageDetails), ), const Gap(16), diff --git a/test/api/images_api_test.dart b/test/api/images_api_test.dart new file mode 100644 index 0000000..e69de29 diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 0bafc25..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mc_gallery/app.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const McgApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -}