From e4aa36f880efb9e7d02d712da1a863f9bd7f42be Mon Sep 17 00:00:00 2001 From: Simone Stasi <62812903+sstasi95@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:55:40 +0100 Subject: [PATCH] Add native ads in list pages (#55) * add native ads in list pages * load new ads in each page This is done to avoid 'ad already in the widget tree' error. * add ads_mixin * refresh ads when filtering lists * fix hasSavedQuery * don't load native ads if ads are removed * fix rebase errors * format files * refactoring: move show ad conditions in AdsMixin --- lib/src/mixins/ads_mixin.dart | 17 ++ .../board_detail/base_board_detail.dart | 5 +- .../board_detail/controller_board_detail.dart | 12 +- lib/src/screens/commits/base_commits.dart | 7 +- .../screens/commits/controller_commits.dart | 14 +- lib/src/screens/commits/screen_commits.dart | 28 +- lib/src/screens/pipelines/base_pipelines.dart | 7 +- .../pipelines/controller_pipelines.dart | 22 +- .../screens/pipelines/screen_pipelines.dart | 48 ++-- lib/src/screens/profile/base_profile.dart | 6 +- .../screens/profile/controller_profile.dart | 7 +- lib/src/screens/profile/screen_profile.dart | 264 +++++++++--------- .../pull_requests/base_pull_requests.dart | 7 +- .../controller_pull_requests.dart | 18 +- .../pull_requests/screen_pull_requests.dart | 28 +- .../screens/work_items/base_work_items.dart | 7 +- .../work_items/controller_work_items.dart | 28 +- .../screens/work_items/screen_work_items.dart | 25 +- lib/src/services/ads_service.dart | 81 +++++- lib/src/services/overlay_service.dart | 7 +- lib/src/widgets/ad_widget.dart | 24 ++ test/api_service_mock.dart | 6 + test/commits_test.dart | 22 +- test/pipelines_test.dart | 19 +- test/pull_requests_test.dart | 22 +- test/work_items_test.dart | 13 +- 26 files changed, 509 insertions(+), 235 deletions(-) create mode 100644 lib/src/mixins/ads_mixin.dart create mode 100644 lib/src/widgets/ad_widget.dart diff --git a/lib/src/mixins/ads_mixin.dart b/lib/src/mixins/ads_mixin.dart new file mode 100644 index 00000000..b9e43ce3 --- /dev/null +++ b/lib/src/mixins/ads_mixin.dart @@ -0,0 +1,17 @@ +import 'package:azure_devops/src/services/ads_service.dart'; +import 'package:azure_devops/src/widgets/ad_widget.dart'; +import 'package:flutter/widgets.dart'; + +mixin AdsMixin { + List ads = []; + + /// Load new native ads and map them to [AdWithKey] objects with a new global key to force refresh the UI. + Future getNewNativeAds(AdsService adsService) async { + final newAds = await adsService.getNewNativeAds(); + ads = newAds.map((ad) => (ad: ad, key: GlobalKey())).toList(); + } + + /// Whether to show a native ad at the given [index] inside [items] list. + bool shouldShowNativeAd(List items, T item, int index) => + items.indexOf(item) % 5 == 4 && item != items.first && index < ads.length; +} diff --git a/lib/src/screens/board_detail/base_board_detail.dart b/lib/src/screens/board_detail/base_board_detail.dart index a7cfe63b..cf8b831f 100644 --- a/lib/src/screens/board_detail/base_board_detail.dart +++ b/lib/src/screens/board_detail/base_board_detail.dart @@ -1,9 +1,11 @@ library board_detail; +import 'package:azure_devops/src/extensions/context_extension.dart'; import 'package:azure_devops/src/mixins/api_error_mixin.dart'; import 'package:azure_devops/src/models/board.dart'; import 'package:azure_devops/src/models/work_items.dart'; import 'package:azure_devops/src/router/router.dart'; +import 'package:azure_devops/src/services/ads_service.dart'; import 'package:azure_devops/src/services/azure_api_service.dart'; import 'package:azure_devops/src/services/overlay_service.dart'; import 'package:azure_devops/src/theme/dev_ops_icons_icons.dart'; @@ -31,8 +33,9 @@ class BoardDetailPage extends StatelessWidget { Widget build(BuildContext context) { final api = AzureApiServiceInherited.of(context).apiService; final args = AppRouter.getBoardDetailArgs(context); + final ads = context.adsService; return AppBasePage( - initState: () => _BoardDetailController._(api, args), + initState: () => _BoardDetailController._(api, args, ads), smartphone: (ctrl) => _BoardDetailScreen(ctrl, _smartphoneParameters), tablet: (ctrl) => _BoardDetailScreen(ctrl, _tabletParameters), ); diff --git a/lib/src/screens/board_detail/controller_board_detail.dart b/lib/src/screens/board_detail/controller_board_detail.dart index 7a6d61b0..998f5050 100644 --- a/lib/src/screens/board_detail/controller_board_detail.dart +++ b/lib/src/screens/board_detail/controller_board_detail.dart @@ -1,9 +1,10 @@ part of board_detail; class _BoardDetailController with ApiErrorHelper { - _BoardDetailController._(this.api, this.args); + _BoardDetailController._(this.api, this.args, this.ads); final AzureApiService api; + final AdsService ads; final BoardDetailArgs args; final boardWithItems = ValueNotifier?>(null); @@ -101,7 +102,14 @@ class _BoardDetailController with ApiErrorHelper { return OverlayService.error('Error', description: 'Item not updated.\n${errorMessage.msg}'); } - OverlayService.snackbar('Item successfully moved to column $column'); + await _showInterstitialAd( + onDismiss: () => OverlayService.snackbar('Item successfully moved to column $column'), + ); + await init(); } + + Future _showInterstitialAd({VoidCallback? onDismiss}) async { + await ads.showInterstitialAd(onDismiss: onDismiss); + } } diff --git a/lib/src/screens/commits/base_commits.dart b/lib/src/screens/commits/base_commits.dart index ba3f9103..2124741d 100644 --- a/lib/src/screens/commits/base_commits.dart +++ b/lib/src/screens/commits/base_commits.dart @@ -1,6 +1,8 @@ library commits; import 'package:azure_devops/src/extensions/commit_extension.dart'; +import 'package:azure_devops/src/extensions/context_extension.dart'; +import 'package:azure_devops/src/mixins/ads_mixin.dart'; import 'package:azure_devops/src/mixins/api_error_mixin.dart'; import 'package:azure_devops/src/mixins/filter_mixin.dart'; import 'package:azure_devops/src/models/commit.dart'; @@ -8,10 +10,12 @@ import 'package:azure_devops/src/models/commits_tags.dart'; import 'package:azure_devops/src/models/project.dart'; import 'package:azure_devops/src/models/user.dart'; import 'package:azure_devops/src/router/router.dart'; +import 'package:azure_devops/src/services/ads_service.dart'; import 'package:azure_devops/src/services/azure_api_service.dart'; import 'package:azure_devops/src/services/filters_service.dart'; import 'package:azure_devops/src/services/overlay_service.dart'; import 'package:azure_devops/src/services/storage_service.dart'; +import 'package:azure_devops/src/widgets/ad_widget.dart'; import 'package:azure_devops/src/widgets/app_base_page.dart'; import 'package:azure_devops/src/widgets/app_page.dart'; import 'package:azure_devops/src/widgets/commit_list_tile.dart'; @@ -36,9 +40,10 @@ class CommitsPage extends StatelessWidget { Widget build(BuildContext context) { final apiService = AzureApiServiceInherited.of(context).apiService; final storageService = StorageServiceInherited.of(context).storageService; + final ads = context.adsService; final args = AppRouter.getCommitsArgs(context); return AppBasePage( - initState: () => _CommitsController._(apiService, storageService, args), + initState: () => _CommitsController._(apiService, storageService, args, ads), smartphone: (ctrl) => _CommitsScreen(ctrl, _smartphoneParameters), tablet: (ctrl) => _CommitsScreen(ctrl, _tabletParameters), ); diff --git a/lib/src/screens/commits/controller_commits.dart b/lib/src/screens/commits/controller_commits.dart index 0387bb34..36b46092 100644 --- a/lib/src/screens/commits/controller_commits.dart +++ b/lib/src/screens/commits/controller_commits.dart @@ -1,13 +1,14 @@ part of commits; -class _CommitsController with FilterMixin, ApiErrorHelper { - _CommitsController._(this.apiService, this.storageService, this.args) { +class _CommitsController with FilterMixin, ApiErrorHelper, AdsMixin { + _CommitsController._(this.apiService, this.storageService, this.args, this.adsService) { if (args?.project != null) projectsFilter = {args!.project!}; if (args?.author != null) usersFilter = {args!.author!}; } final AzureApiService apiService; final StorageService storageService; + final AdsService adsService; final CommitsArgs? args; final recentCommits = ValueNotifier?>?>(null); @@ -29,6 +30,11 @@ class _CommitsController with FilterMixin, ApiErrorHelper { _fillShortcutFilters(); } + await _getDataAndAds(); + } + + Future _getDataAndAds() async { + await getNewNativeAds(adsService); await _getData(); } @@ -102,7 +108,7 @@ class _CommitsController with FilterMixin, ApiErrorHelper { recentCommits.value = null; projectsFilter = projects; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.saveCommitsProjectsFilter(projects.map((p) => p.name!).toSet()); @@ -114,7 +120,7 @@ class _CommitsController with FilterMixin, ApiErrorHelper { recentCommits.value = null; usersFilter = users; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.saveCommitsAuthorsFilter(users.map((p) => p.mailAddress!).toSet()); diff --git a/lib/src/screens/commits/screen_commits.dart b/lib/src/screens/commits/screen_commits.dart index 0ff0e7de..23e272f6 100644 --- a/lib/src/screens/commits/screen_commits.dart +++ b/lib/src/screens/commits/screen_commits.dart @@ -48,18 +48,28 @@ class _CommitsScreen extends StatelessWidget { ), ], ), - builder: (commits) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: commits! - .map( - (c) => CommitListTile( + builder: (commits) { + var adsIndex = 0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: commits!.expand( + (c) sync* { + yield CommitListTile( commit: c, onTap: () => ctrl.goToCommitDetail(c), isLast: c == commits.last, - ), - ) - .toList(), - ), + ); + + if (ctrl.shouldShowNativeAd(commits, c, adsIndex)) { + yield NativeAdWidget( + ad: ctrl.ads[adsIndex++], + ); + } + }, + ).toList(), + ); + }, ); } } diff --git a/lib/src/screens/pipelines/base_pipelines.dart b/lib/src/screens/pipelines/base_pipelines.dart index 73f9825d..44a55743 100644 --- a/lib/src/screens/pipelines/base_pipelines.dart +++ b/lib/src/screens/pipelines/base_pipelines.dart @@ -2,17 +2,21 @@ library pipelines; import 'dart:async'; +import 'package:azure_devops/src/extensions/context_extension.dart'; import 'package:azure_devops/src/extensions/pipeline_result_extension.dart'; +import 'package:azure_devops/src/mixins/ads_mixin.dart'; import 'package:azure_devops/src/mixins/api_error_mixin.dart'; import 'package:azure_devops/src/mixins/filter_mixin.dart'; import 'package:azure_devops/src/models/pipeline.dart'; import 'package:azure_devops/src/models/project.dart'; import 'package:azure_devops/src/models/user.dart'; import 'package:azure_devops/src/router/router.dart'; +import 'package:azure_devops/src/services/ads_service.dart'; import 'package:azure_devops/src/services/azure_api_service.dart'; import 'package:azure_devops/src/services/filters_service.dart'; import 'package:azure_devops/src/services/overlay_service.dart'; import 'package:azure_devops/src/services/storage_service.dart'; +import 'package:azure_devops/src/widgets/ad_widget.dart'; import 'package:azure_devops/src/widgets/app_base_page.dart'; import 'package:azure_devops/src/widgets/app_page.dart'; import 'package:azure_devops/src/widgets/filter_menu.dart'; @@ -38,9 +42,10 @@ class PipelinesPage extends StatelessWidget { Widget build(BuildContext context) { final apiService = AzureApiServiceInherited.of(context).apiService; final storageService = StorageServiceInherited.of(context).storageService; + final ads = context.adsService; final args = AppRouter.getPipelinesArgs(context); return AppBasePage( - initState: () => _PipelinesController._(apiService, storageService, args), + initState: () => _PipelinesController._(apiService, storageService, args, ads), smartphone: (ctrl) => _PipelinesScreen(ctrl, _smartphoneParameters), tablet: (ctrl) => _PipelinesScreen(ctrl, _tabletParameters), ); diff --git a/lib/src/screens/pipelines/controller_pipelines.dart b/lib/src/screens/pipelines/controller_pipelines.dart index 52b72cb8..b335e53a 100644 --- a/lib/src/screens/pipelines/controller_pipelines.dart +++ b/lib/src/screens/pipelines/controller_pipelines.dart @@ -1,12 +1,13 @@ part of pipelines; -class _PipelinesController with FilterMixin, ApiErrorHelper { - _PipelinesController._(this.apiService, this.storageService, this.args) { +class _PipelinesController with FilterMixin, ApiErrorHelper, AdsMixin { + _PipelinesController._(this.apiService, this.storageService, this.args, this.adsService) { if (args?.project != null) projectsFilter = {args!.project!}; } final AzureApiService apiService; final StorageService storageService; + final AdsService adsService; final PipelinesArgs? args; final pipelines = ValueNotifier?>?>(null); @@ -54,7 +55,7 @@ class _PipelinesController with FilterMixin, ApiErrorHelper { _fillShortcutFilters(); } - await _getData(); + await _getDataAndAds(); if (pipelines.value != null) { final shouldRefresh = inProgressPipelines > 0 || queuedPipelines > 0 || cancellingPipelines > 0; @@ -72,6 +73,11 @@ class _PipelinesController with FilterMixin, ApiErrorHelper { } } + Future _getDataAndAds() async { + await getNewNativeAds(adsService); + await _getData(); + } + void _fillSavedFilters() { final savedFilters = filtersService.getPipelinesSavedFilters(); _fillFilters(savedFilters); @@ -152,7 +158,7 @@ class _PipelinesController with FilterMixin, ApiErrorHelper { pipelines.value = null; projectsFilter = projects; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.savePipelinesProjectsFilter(projects.map((p) => p.name!).toSet()); @@ -164,7 +170,7 @@ class _PipelinesController with FilterMixin, ApiErrorHelper { pipelines.value = null; resultFilter = result; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.savePipelinesResultFilter(result.stringValue); @@ -176,7 +182,7 @@ class _PipelinesController with FilterMixin, ApiErrorHelper { pipelines.value = null; statusFilter = status; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.savePipelinesStatusFilter(status.stringValue); @@ -188,7 +194,7 @@ class _PipelinesController with FilterMixin, ApiErrorHelper { pipelines.value = null; usersFilter = users; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.savePipelinesTriggeredByFilter(users.map((p) => p.mailAddress!).toSet()); @@ -200,7 +206,7 @@ class _PipelinesController with FilterMixin, ApiErrorHelper { pipelines.value = null; pipelineNamesFilter = names; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.savePipelinesNamesFilter(names); diff --git a/lib/src/screens/pipelines/screen_pipelines.dart b/lib/src/screens/pipelines/screen_pipelines.dart index 5853b053..61dff330 100644 --- a/lib/src/screens/pipelines/screen_pipelines.dart +++ b/lib/src/screens/pipelines/screen_pipelines.dart @@ -82,27 +82,39 @@ class _PipelinesScreen extends StatelessWidget { ), ], ), - builder: (pipelines) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 16, - ), - if (ctrl.inProgressPipelines > 0) Text('Running pipelines: ${ctrl.inProgressPipelines}'), - if (ctrl.queuedPipelines > 0) Text('Queued pipelines: ${ctrl.queuedPipelines}'), - if (ctrl.inProgressPipelines > 0 || ctrl.queuedPipelines > 0) + builder: (pipelines) { + var adsIndex = 0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ const SizedBox( - height: 24, + height: 16, ), - ...pipelines!.map( - (p) => PipelineListTile( - pipe: p, - onTap: () => ctrl.goToPipelineDetail(p), - isLast: p == pipelines.last, + if (ctrl.inProgressPipelines > 0) Text('Running pipelines: ${ctrl.inProgressPipelines}'), + if (ctrl.queuedPipelines > 0) Text('Queued pipelines: ${ctrl.queuedPipelines}'), + if (ctrl.inProgressPipelines > 0 || ctrl.queuedPipelines > 0) + const SizedBox( + height: 24, + ), + ...pipelines!.expand( + (p) sync* { + yield PipelineListTile( + pipe: p, + onTap: () => ctrl.goToPipelineDetail(p), + isLast: p == pipelines.last, + ); + + if (ctrl.shouldShowNativeAd(pipelines, p, adsIndex)) { + yield NativeAdWidget( + ad: ctrl.ads[adsIndex++], + ); + } + }, ), - ), - ], - ), + ], + ); + }, ), ); } diff --git a/lib/src/screens/profile/base_profile.dart b/lib/src/screens/profile/base_profile.dart index c65a77dd..5e8e63a5 100644 --- a/lib/src/screens/profile/base_profile.dart +++ b/lib/src/screens/profile/base_profile.dart @@ -5,15 +5,18 @@ import 'dart:async'; import 'package:azure_devops/src/extensions/commit_extension.dart'; import 'package:azure_devops/src/extensions/context_extension.dart'; import 'package:azure_devops/src/extensions/datetime_extension.dart'; +import 'package:azure_devops/src/mixins/ads_mixin.dart'; import 'package:azure_devops/src/mixins/filter_mixin.dart'; import 'package:azure_devops/src/models/commit.dart'; import 'package:azure_devops/src/models/user.dart'; import 'package:azure_devops/src/models/work_items.dart'; import 'package:azure_devops/src/router/router.dart'; +import 'package:azure_devops/src/services/ads_service.dart'; import 'package:azure_devops/src/services/azure_api_service.dart'; import 'package:azure_devops/src/services/storage_service.dart'; import 'package:azure_devops/src/theme/dev_ops_icons_icons.dart'; import 'package:azure_devops/src/theme/theme.dart'; +import 'package:azure_devops/src/widgets/ad_widget.dart'; import 'package:azure_devops/src/widgets/app_base_page.dart'; import 'package:azure_devops/src/widgets/app_page.dart'; import 'package:azure_devops/src/widgets/commit_list_tile.dart'; @@ -37,8 +40,9 @@ class ProfilePage extends StatelessWidget { Widget build(BuildContext context) { final apiService = AzureApiServiceInherited.of(context).apiService; final storageService = StorageServiceInherited.of(context).storageService; + final ads = context.adsService; return AppBasePage( - initState: () => _ProfileController._(apiService, storageService), + initState: () => _ProfileController._(apiService, storageService, ads), smartphone: (ctrl) => _ProfileScreen(ctrl, _smartphoneParameters), tablet: (ctrl) => _ProfileScreen(ctrl, _tabletParameters), ); diff --git a/lib/src/screens/profile/controller_profile.dart b/lib/src/screens/profile/controller_profile.dart index c219d968..1904229c 100644 --- a/lib/src/screens/profile/controller_profile.dart +++ b/lib/src/screens/profile/controller_profile.dart @@ -1,10 +1,11 @@ part of profile; -class _ProfileController with FilterMixin { - _ProfileController._(this.apiService, this.storageService); +class _ProfileController with FilterMixin, AdsMixin { + _ProfileController._(this.apiService, this.storageService, this.adsService); final AzureApiService apiService; final StorageService storageService; + final AdsService adsService; final recentCommits = ValueNotifier?>?>(null); @@ -42,6 +43,8 @@ class _ProfileController with FilterMixin { final myWorkItemsRes = await apiService.getMyRecentWorkItems(); myWorkItems.addAll(myWorkItemsRes.data ?? []); + await getNewNativeAds(adsService); + recentCommits.value = commits.copyWith(data: res); } diff --git a/lib/src/screens/profile/screen_profile.dart b/lib/src/screens/profile/screen_profile.dart index 2dab4b00..d58a61da 100644 --- a/lib/src/screens/profile/screen_profile.dart +++ b/lib/src/screens/profile/screen_profile.dart @@ -13,149 +13,161 @@ class _ProfileScreen extends StatelessWidget { title: 'Profile', notifier: ctrl.recentCommits, showScrollbar: true, - builder: (commits) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (ctrl.author?.descriptor != null) ...[ - Row( - children: [ - Expanded( - child: Text( - ctrl.author!.displayName!, - style: context.textTheme.headlineLarge, - ), - ), - MemberAvatar( - userDescriptor: ctrl.author!.descriptor, - radius: 60, - tappable: false, - ), - ], - ), - ], - if (ctrl.todaysCommitsPerRepo.isNotEmpty || ctrl.myWorkItems.isNotEmpty) ...[ - SectionHeader(text: "Today's summary"), - Container( - decoration: BoxDecoration( - color: context.colorScheme.surface, - borderRadius: BorderRadius.circular(AppTheme.radius), - ), - padding: const EdgeInsets.all(20), - width: double.maxFinite, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + builder: (commits) { + var adsIndex = 0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (ctrl.author?.descriptor != null) ...[ + Row( children: [ - if (ctrl.todaysCommitsPerRepo.isNotEmpty) ...[ - Text( - ctrl.getCommitsSummary(), - textAlign: TextAlign.center, - style: context.textTheme.bodyMedium!.copyWith(color: context.colorScheme.onSecondary), - ), - const SizedBox( - height: 6, + Expanded( + child: Text( + ctrl.author!.displayName!, + style: context.textTheme.headlineLarge, ), - ...ctrl.todaysCommitsPerRepo.entries - .sortedBy((e) => e.value.values.expand((e1) => e1).length) - .reversed - .map( - (p) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: p.value.entries - .sortedBy((e) => e.value.length) - .map( - (e) => InkWell( - onTap: () => ctrl.goToCommits(e.value.first), - child: Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Text.rich( - TextSpan( - children: [ - TextSpan( - text: '${e.value.length}', - style: context.textTheme.bodyMedium! - .copyWith(decoration: TextDecoration.underline), - ), - TextSpan( - text: ' in ', - style: - context.textTheme.titleSmall!.copyWith(fontWeight: FontWeight.w200), - ), - TextSpan( - text: e.key, - ), - ], + ), + MemberAvatar( + userDescriptor: ctrl.author!.descriptor, + radius: 60, + tappable: false, + ), + ], + ), + ], + if (ctrl.todaysCommitsPerRepo.isNotEmpty || ctrl.myWorkItems.isNotEmpty) ...[ + SectionHeader(text: "Today's summary"), + Container( + decoration: BoxDecoration( + color: context.colorScheme.surface, + borderRadius: BorderRadius.circular(AppTheme.radius), + ), + padding: const EdgeInsets.all(20), + width: double.maxFinite, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (ctrl.todaysCommitsPerRepo.isNotEmpty) ...[ + Text( + ctrl.getCommitsSummary(), + textAlign: TextAlign.center, + style: context.textTheme.bodyMedium!.copyWith(color: context.colorScheme.onSecondary), + ), + const SizedBox( + height: 6, + ), + ...ctrl.todaysCommitsPerRepo.entries + .sortedBy((e) => e.value.values.expand((e1) => e1).length) + .reversed + .map( + (p) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: p.value.entries + .sortedBy((e) => e.value.length) + .map( + (e) => InkWell( + onTap: () => ctrl.goToCommits(e.value.first), + child: Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${e.value.length}', + style: context.textTheme.bodyMedium! + .copyWith(decoration: TextDecoration.underline), + ), + TextSpan( + text: ' in ', + style: + context.textTheme.titleSmall!.copyWith(fontWeight: FontWeight.w200), + ), + TextSpan( + text: e.key, + ), + ], + ), ), ), ), - ), - ) - .toList(), + ) + .toList(), + ), ), + ], + if (ctrl.myWorkItems.isNotEmpty) ...[ + if (ctrl.todaysCommitsPerRepo.isNotEmpty) + const SizedBox( + height: 10, ), - ], - if (ctrl.myWorkItems.isNotEmpty) ...[ - if (ctrl.todaysCommitsPerRepo.isNotEmpty) + Text( + ctrl.getWorkItemsSummary(), + textAlign: TextAlign.center, + style: context.textTheme.bodyMedium!.copyWith(color: context.colorScheme.onSecondary), + ), const SizedBox( - height: 10, + height: 6, ), - Text( - ctrl.getWorkItemsSummary(), - textAlign: TextAlign.center, - style: context.textTheme.bodyMedium!.copyWith(color: context.colorScheme.onSecondary), - ), - const SizedBox( - height: 6, - ), - ...ctrl.myWorkItems.map( - (item) => InkWell( - onTap: () => ctrl.goToWorkItemDetail(item), - child: Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Text.rich( - TextSpan( - children: [ - TextSpan( - text: '#${item.id}', - style: context.textTheme.bodyMedium!.copyWith(decoration: TextDecoration.underline), - ), - TextSpan( - text: ' in ', - style: context.textTheme.titleSmall!.copyWith(fontWeight: FontWeight.w200), - ), - TextSpan( - text: '${item.fields.systemTeamProject} (${item.fields.systemState})', - ), - ], + ...ctrl.myWorkItems.map( + (item) => InkWell( + onTap: () => ctrl.goToWorkItemDetail(item), + child: Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: '#${item.id}', + style: context.textTheme.bodyMedium!.copyWith(decoration: TextDecoration.underline), + ), + TextSpan( + text: ' in ', + style: context.textTheme.titleSmall!.copyWith(fontWeight: FontWeight.w200), + ), + TextSpan( + text: '${item.fields.systemTeamProject} (${item.fields.systemState})', + ), + ], + ), ), ), ), ), - ), + ], ], - ], + ), ), + ], + SectionHeader.withIcon( + text: 'Recent commits', + icon: DevOpsIcons.commit, ), - ], - SectionHeader.withIcon( - text: 'Recent commits', - icon: DevOpsIcons.commit, - ), - if (commits!.isEmpty) - Padding( - padding: const EdgeInsets.only(top: 40), - child: Center(child: Text('No commits found')), - ) - else - ...commits.map( - (c) => CommitListTile( - commit: c, - showAuthor: false, - onTap: () => ctrl.goToCommitDetail(c), - isLast: c == commits.last, + if (commits!.isEmpty) + Padding( + padding: const EdgeInsets.only(top: 40), + child: Center(child: Text('No commits found')), + ) + else + ...commits.expand( + (c) sync* { + yield CommitListTile( + commit: c, + showAuthor: false, + onTap: () => ctrl.goToCommitDetail(c), + isLast: c == commits.last, + ); + + if (ctrl.shouldShowNativeAd(commits, c, adsIndex)) { + yield NativeAdWidget( + ad: ctrl.ads[adsIndex++], + ); + } + }, ), - ), - ], - ), + ], + ); + }, ); } } diff --git a/lib/src/screens/pull_requests/base_pull_requests.dart b/lib/src/screens/pull_requests/base_pull_requests.dart index 6d306111..b97a2b88 100644 --- a/lib/src/screens/pull_requests/base_pull_requests.dart +++ b/lib/src/screens/pull_requests/base_pull_requests.dart @@ -1,15 +1,19 @@ library pull_requests; +import 'package:azure_devops/src/extensions/context_extension.dart'; +import 'package:azure_devops/src/mixins/ads_mixin.dart'; import 'package:azure_devops/src/mixins/api_error_mixin.dart'; import 'package:azure_devops/src/mixins/filter_mixin.dart'; import 'package:azure_devops/src/models/project.dart'; import 'package:azure_devops/src/models/pull_request.dart'; import 'package:azure_devops/src/models/user.dart'; import 'package:azure_devops/src/router/router.dart'; +import 'package:azure_devops/src/services/ads_service.dart'; import 'package:azure_devops/src/services/azure_api_service.dart'; import 'package:azure_devops/src/services/filters_service.dart'; import 'package:azure_devops/src/services/overlay_service.dart'; import 'package:azure_devops/src/services/storage_service.dart'; +import 'package:azure_devops/src/widgets/ad_widget.dart'; import 'package:azure_devops/src/widgets/app_base_page.dart'; import 'package:azure_devops/src/widgets/app_page.dart'; import 'package:azure_devops/src/widgets/filter_menu.dart'; @@ -34,9 +38,10 @@ class PullRequestsPage extends StatelessWidget { Widget build(BuildContext context) { final apiService = AzureApiServiceInherited.of(context).apiService; final storageService = StorageServiceInherited.of(context).storageService; + final ads = context.adsService; final args = AppRouter.getPullRequestsArgs(context); return AppBasePage( - initState: () => _PullRequestsController._(apiService, storageService, args), + initState: () => _PullRequestsController._(apiService, storageService, args, ads), smartphone: (ctrl) => _PullRequestsScreen(ctrl, _smartphoneParameters), tablet: (ctrl) => _PullRequestsScreen(ctrl, _tabletParameters), ); diff --git a/lib/src/screens/pull_requests/controller_pull_requests.dart b/lib/src/screens/pull_requests/controller_pull_requests.dart index 1076a3e9..d893d242 100644 --- a/lib/src/screens/pull_requests/controller_pull_requests.dart +++ b/lib/src/screens/pull_requests/controller_pull_requests.dart @@ -1,12 +1,13 @@ part of pull_requests; -class _PullRequestsController with FilterMixin, ApiErrorHelper { - _PullRequestsController._(this.apiService, this.storageService, this.args) { +class _PullRequestsController with FilterMixin, ApiErrorHelper, AdsMixin { + _PullRequestsController._(this.apiService, this.storageService, this.args, this.adsService) { if (args?.project != null) projectsFilter = {args!.project!}; } final AzureApiService apiService; final StorageService storageService; + final AdsService adsService; final PullRequestArgs? args; final pullRequests = ValueNotifier?>?>(null); @@ -36,6 +37,11 @@ class _PullRequestsController with FilterMixin, ApiErrorHelper { _fillShortcutFilters(); } + await _getDataAndAds(); + } + + Future _getDataAndAds() async { + await getNewNativeAds(adsService); await _getData(); } @@ -82,7 +88,7 @@ class _PullRequestsController with FilterMixin, ApiErrorHelper { pullRequests.value = null; statusFilter = status; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.savePullRequestsStatusFilter(status.name); @@ -94,7 +100,7 @@ class _PullRequestsController with FilterMixin, ApiErrorHelper { pullRequests.value = null; usersFilter = users; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.savePullRequestsOpenedByFilter(users.map((p) => p.mailAddress!).toSet()); @@ -106,7 +112,7 @@ class _PullRequestsController with FilterMixin, ApiErrorHelper { pullRequests.value = null; reviewersFilter = users; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.savePullRequestsAssignedToFilter(users.map((p) => p.mailAddress!).toSet()); @@ -118,7 +124,7 @@ class _PullRequestsController with FilterMixin, ApiErrorHelper { pullRequests.value = null; projectsFilter = projects; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.savePullRequestsProjectsFilter(projects.map((p) => p.name!).toSet()); diff --git a/lib/src/screens/pull_requests/screen_pull_requests.dart b/lib/src/screens/pull_requests/screen_pull_requests.dart index 7e1347ae..1ddab0b6 100644 --- a/lib/src/screens/pull_requests/screen_pull_requests.dart +++ b/lib/src/screens/pull_requests/screen_pull_requests.dart @@ -82,18 +82,28 @@ class _PullRequestsScreen extends StatelessWidget { ), ], ), - builder: (prs) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: prs! - .map( - (pr) => PullRequestListTile( + builder: (prs) { + var adsIndex = 0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: prs!.expand( + (pr) sync* { + yield PullRequestListTile( pr: pr, onTap: () => ctrl.goToPullRequestDetail(pr), isLast: pr == prs.last, - ), - ) - .toList(), - ), + ); + + if (ctrl.shouldShowNativeAd(prs, pr, adsIndex)) { + yield NativeAdWidget( + ad: ctrl.ads[adsIndex++], + ); + } + }, + ).toList(), + ); + }, ); } } diff --git a/lib/src/screens/work_items/base_work_items.dart b/lib/src/screens/work_items/base_work_items.dart index 6eacdca5..bed4ccfc 100644 --- a/lib/src/screens/work_items/base_work_items.dart +++ b/lib/src/screens/work_items/base_work_items.dart @@ -2,6 +2,8 @@ library work_items; import 'package:azure_devops/src/extensions/area_or_iteration_extension.dart'; import 'package:azure_devops/src/extensions/child_query_extension.dart'; +import 'package:azure_devops/src/extensions/context_extension.dart'; +import 'package:azure_devops/src/mixins/ads_mixin.dart'; import 'package:azure_devops/src/mixins/api_error_mixin.dart'; import 'package:azure_devops/src/mixins/filter_mixin.dart'; import 'package:azure_devops/src/models/areas_and_iterations.dart'; @@ -11,11 +13,13 @@ import 'package:azure_devops/src/models/saved_query.dart'; import 'package:azure_devops/src/models/user.dart'; import 'package:azure_devops/src/models/work_items.dart'; import 'package:azure_devops/src/router/router.dart'; +import 'package:azure_devops/src/services/ads_service.dart'; import 'package:azure_devops/src/services/azure_api_service.dart'; import 'package:azure_devops/src/services/filters_service.dart'; import 'package:azure_devops/src/services/overlay_service.dart'; import 'package:azure_devops/src/services/storage_service.dart'; import 'package:azure_devops/src/theme/dev_ops_icons_icons.dart'; +import 'package:azure_devops/src/widgets/ad_widget.dart'; import 'package:azure_devops/src/widgets/app_base_page.dart'; import 'package:azure_devops/src/widgets/app_page.dart'; import 'package:azure_devops/src/widgets/filter_menu.dart'; @@ -42,9 +46,10 @@ class WorkItemsPage extends StatelessWidget { Widget build(BuildContext context) { final apiService = AzureApiServiceInherited.of(context).apiService; final storageService = StorageServiceInherited.of(context).storageService; + final ads = context.adsService; final args = AppRouter.getWorkItemsArgs(context); return AppBasePage( - initState: () => _WorkItemsController._(apiService, storageService, args), + initState: () => _WorkItemsController._(apiService, storageService, args, ads), smartphone: (ctrl) => _WorkItemsScreen(ctrl, _smartphoneParameters), tablet: (ctrl) => _WorkItemsScreen(ctrl, _tabletParameters), ); diff --git a/lib/src/screens/work_items/controller_work_items.dart b/lib/src/screens/work_items/controller_work_items.dart index e30e3c23..23d6029c 100644 --- a/lib/src/screens/work_items/controller_work_items.dart +++ b/lib/src/screens/work_items/controller_work_items.dart @@ -1,12 +1,13 @@ part of work_items; -class _WorkItemsController with FilterMixin, ApiErrorHelper { - _WorkItemsController._(this.apiService, this.storageService, this.args) { +class _WorkItemsController with FilterMixin, ApiErrorHelper, AdsMixin { + _WorkItemsController._(this.apiService, this.storageService, this.args, this.adsService) { if (args?.project != null) projectsFilter = {args!.project!}; } final AzureApiService apiService; final StorageService storageService; + final AdsService adsService; final WorkItemsArgs? args; final workItems = ValueNotifier?>?>(null); @@ -70,7 +71,7 @@ class _WorkItemsController with FilterMixin, ApiErrorHelper { } } - await _getData(); + await _getDataAndAds(); if (shouldPersistFilters) { if (savedFilters?.area.isNotEmpty ?? false) { @@ -89,6 +90,11 @@ class _WorkItemsController with FilterMixin, ApiErrorHelper { } } + Future _getDataAndAds() async { + await getNewNativeAds(adsService); + await _getData(); + } + /// Here we fill some filters with fake objects just to show them immediately, /// because we may not have the real object yet (areas, iterations, types and states /// need to get downloaded). @@ -160,7 +166,7 @@ class _WorkItemsController with FilterMixin, ApiErrorHelper { Future goToWorkItemDetail(WorkItem item) async { await AppRouter.goToWorkItemDetail(project: item.fields.systemTeamProject, id: item.id); - await _getData(); + await _getDataAndAds(); } void filterByProjects(Set projects) { @@ -181,7 +187,7 @@ class _WorkItemsController with FilterMixin, ApiErrorHelper { } } - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.saveWorkItemsProjectsFilter(projects.map((p) => p.name!).toSet()); @@ -223,7 +229,7 @@ class _WorkItemsController with FilterMixin, ApiErrorHelper { workItems.value = null; statesFilter = states; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.saveWorkItemsStatesFilter(states.map((p) => p.name).toSet()); @@ -235,7 +241,7 @@ class _WorkItemsController with FilterMixin, ApiErrorHelper { workItems.value = null; stateCategoriesFilter = categories; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.saveWorkItemsCategoriesFilter(stateCategoriesFilter.map((p) => p.name).toSet()); @@ -247,7 +253,7 @@ class _WorkItemsController with FilterMixin, ApiErrorHelper { workItems.value = null; typesFilter = types; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.saveWorkItemsTypesFilter(types.map((p) => p.name).toSet()); @@ -259,7 +265,7 @@ class _WorkItemsController with FilterMixin, ApiErrorHelper { workItems.value = null; usersFilter = users; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.saveWorkItemsAssigneesFilter(users.map((p) => p.mailAddress!).toSet()); @@ -271,7 +277,7 @@ class _WorkItemsController with FilterMixin, ApiErrorHelper { workItems.value = null; areaFilter = area; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.saveWorkItemsAreaFilter(area?.path ?? ''); @@ -283,7 +289,7 @@ class _WorkItemsController with FilterMixin, ApiErrorHelper { workItems.value = null; iterationFilter = iteration; - _getData(); + _getDataAndAds(); if (shouldPersistFilters) { filtersService.saveWorkItemsIterationFilter(iteration?.path ?? ''); diff --git a/lib/src/screens/work_items/screen_work_items.dart b/lib/src/screens/work_items/screen_work_items.dart index 8e075e1e..46d8b107 100644 --- a/lib/src/screens/work_items/screen_work_items.dart +++ b/lib/src/screens/work_items/screen_work_items.dart @@ -139,18 +139,27 @@ class _WorkItemsScreen extends StatelessWidget { ], ); }, - builder: (items) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: items! - .map( - (i) => WorkItemListTile( + builder: (items) { + var adsIndex = 0; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: items!.expand((i) sync* { + yield SizedBox( + child: WorkItemListTile( item: i, onTap: () => ctrl.goToWorkItemDetail(i), isLast: i == items.last, ), - ) - .toList(), - ), + ); + + if (ctrl.shouldShowNativeAd(items, i, adsIndex)) { + yield NativeAdWidget( + ad: ctrl.ads[adsIndex++], + ); + } + }).toList(), + ); + }, ); } } diff --git a/lib/src/services/ads_service.dart b/lib/src/services/ads_service.dart index bf5349cb..4a7149ab 100644 --- a/lib/src/services/ads_service.dart +++ b/lib/src/services/ads_service.dart @@ -1,17 +1,25 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:async'; import 'dart:io'; +import 'package:azure_devops/src/extensions/context_extension.dart'; import 'package:azure_devops/src/mixins/logger_mixin.dart'; +import 'package:azure_devops/src/router/router.dart'; import 'package:flutter/material.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; const _androidInterstitialAdId = String.fromEnvironment('ADMOB_INTERSTITIAL_ADID_ANDROID'); const _iosInterstitialAdId = String.fromEnvironment('ADMOB_INTERSTITIAL_ADID_IOS'); +const _androidNativeAdId = String.fromEnvironment('ADMOB_NATIVE_ADID_ANDROID'); +const _iosNativeAdId = String.fromEnvironment('ADMOB_NATIVE_ADID_IOS'); abstract interface class AdsService { Future init(); Future showInterstitialAd({VoidCallback? onDismiss}); void removeAds(); void reactivateAds(); + Future> getNewNativeAds(); } class AdsServiceImpl with AppLogger implements AdsService { @@ -23,7 +31,8 @@ class AdsServiceImpl with AppLogger implements AdsService { static const _tag = 'AdsService'; - final _adUnitId = Platform.isAndroid ? _androidInterstitialAdId : _iosInterstitialAdId; + final _interstitialAdUnitId = Platform.isAndroid ? _androidInterstitialAdId : _iosInterstitialAdId; + final _nativeAdUnitId = Platform.isAndroid ? _androidNativeAdId : _iosNativeAdId; InterstitialAd? _interstitialAd; @@ -42,10 +51,10 @@ class AdsServiceImpl with AppLogger implements AdsService { /// Loads an interstitial ad. Future _loadInterstitialAd() async { - logDebug('Loading interstitial ad: $_adUnitId'); + logDebug('Loading interstitial ad: $_interstitialAdUnitId'); await InterstitialAd.load( - adUnitId: _adUnitId, + adUnitId: _interstitialAdUnitId, request: const AdRequest(), adLoadCallback: InterstitialAdLoadCallback( onAdLoaded: (ad) { @@ -104,6 +113,72 @@ class AdsServiceImpl with AppLogger implements AdsService { logDebug('Ads reactivated'); _showAds = true; } + + @override + Future> getNewNativeAds() async { + if (!_showAds) return []; + + final ctx = AppRouter.navigatorKey.currentContext!; + + final newNativeAds = []; + final compl = Completer>(); + + const adsCount = 3; + + for (var i = 0; i < adsCount; i++) { + final nativeAd = NativeAd( + adUnitId: _nativeAdUnitId, + request: AdRequest(), + nativeAdOptions: NativeAdOptions( + mediaAspectRatio: MediaAspectRatio.portrait, + ), + listener: NativeAdListener( + onAdLoaded: (ad) { + logDebug('NativeAd loaded: ${ad.responseInfo?.responseId}.'); + newNativeAds.add(ad as AdWithView); + + if (newNativeAds.length >= adsCount) { + compl.complete(newNativeAds.where((ad) => ad.adUnitId.isNotEmpty).toList()); + } + }, + onAdFailedToLoad: (ad, error) { + logError('NativeAd failed to load: $error', error); + newNativeAds.add((ad as NativeAd).copyWith(adUnitId: '')); + ad.dispose(); + + if (newNativeAds.length >= adsCount) { + compl.complete(newNativeAds.where((ad) => ad.adUnitId.isNotEmpty).toList()); + } + }, + onAdImpression: (ad) => logDebug('NativeAd onAdImpression.'), + ), + nativeTemplateStyle: NativeTemplateStyle( + templateType: TemplateType.small, + mainBackgroundColor: ctx.themeExtension.background, + callToActionTextStyle: NativeTemplateTextStyle(size: 16), + primaryTextStyle: NativeTemplateTextStyle( + textColor: ctx.themeExtension.onBackground, + ), + ), + ); + + await nativeAd.load(); + } + + return compl.future; + } +} + +extension on NativeAd { + NativeAd copyWith({required String adUnitId}) { + return NativeAd( + adUnitId: adUnitId, + request: request, + nativeAdOptions: nativeAdOptions, + listener: listener, + nativeTemplateStyle: nativeTemplateStyle, + ); + } } class AdsServiceWidget extends InheritedWidget { diff --git a/lib/src/services/overlay_service.dart b/lib/src/services/overlay_service.dart index 8965c408..c58e9283 100644 --- a/lib/src/services/overlay_service.dart +++ b/lib/src/services/overlay_service.dart @@ -209,13 +209,13 @@ class OverlayService { /// Debouncer to avoid showing too many snackbars. static bool _isShowingSnackbar = false; - static void snackbar(String title, {bool isError = false}) { + static void snackbar(String title, {bool isError = false, VoidCallback? onDismiss}) { if (_isShowingSnackbar) return; _isShowingSnackbar = true; Timer(Duration(seconds: 2), () => _isShowingSnackbar = false); - scaffoldMessengerKey.currentState!.showMaterialBanner( + final ctrl = scaffoldMessengerKey.currentState!.showMaterialBanner( MaterialBanner( content: NavigationButton( onTap: scaffoldMessengerKey.currentState!.hideCurrentMaterialBanner, @@ -242,6 +242,9 @@ class OverlayService { surfaceTintColor: Colors.transparent, ), ); + + // callback executed when the snackbar is dismissed + ctrl.closed.then((_) => onDismiss?.call()); } static Future formBottomsheet({required String title, required String label, String? initialValue}) async { diff --git a/lib/src/widgets/ad_widget.dart b/lib/src/widgets/ad_widget.dart new file mode 100644 index 00000000..c37bd1a4 --- /dev/null +++ b/lib/src/widgets/ad_widget.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:google_mobile_ads/google_mobile_ads.dart'; + +typedef AdWithKey = ({AdWithView ad, GlobalKey key}); + +class NativeAdWidget extends StatelessWidget { + const NativeAdWidget({required this.ad}); + + final AdWithKey ad; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 160, + child: Padding( + padding: const EdgeInsets.only(top: 32), + child: AdWidget( + key: ad.key, + ad: ad.ad, + ), + ), + ); + } +} diff --git a/test/api_service_mock.dart b/test/api_service_mock.dart index b1e417ca..fc89aac6 100644 --- a/test/api_service_mock.dart +++ b/test/api_service_mock.dart @@ -35,6 +35,7 @@ import 'package:azure_devops/src/services/purchase_service.dart'; import 'package:azure_devops/src/services/storage_service.dart'; import 'package:azure_devops/src/theme/theme.dart'; import 'package:flutter/material.dart'; +import 'package:google_mobile_ads/src/ad_containers.dart'; final mockTheme = ThemeData(extensions: [AppColorsExtension(background: Colors.white, onBackground: Colors.black)]); @@ -950,6 +951,11 @@ class AdsServiceMock implements AdsService { @override Future showInterstitialAd({VoidCallback? onDismiss}) async {} + + @override + Future> getNewNativeAds() async { + return []; + } } class PurchaseServiceMock implements PurchaseService { diff --git a/test/commits_test.dart b/test/commits_test.dart index c8a75ec8..2a6ea747 100644 --- a/test/commits_test.dart +++ b/test/commits_test.dart @@ -1,12 +1,16 @@ import 'package:azure_devops/src/screens/commits/base_commits.dart'; +import 'package:azure_devops/src/services/ads_service.dart'; import 'package:azure_devops/src/services/azure_api_service.dart'; import 'package:azure_devops/src/services/storage_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:visibility_detector/visibility_detector.dart'; import 'api_service_mock.dart'; void main() { + setUp(() => VisibilityDetectorController.instance.updateInterval = Duration.zero); + TestWidgetsFlutterBinding.ensureInitialized(); testWidgets( @@ -16,9 +20,12 @@ void main() { theme: mockTheme, home: AzureApiServiceInherited( apiService: AzureApiServiceMock(), - child: StorageServiceInherited( - storageService: StorageServiceMock(), - child: CommitsPage(), + child: AdsServiceWidget( + ads: AdsServiceMock(), + child: StorageServiceInherited( + storageService: StorageServiceMock(), + child: CommitsPage(), + ), ), ), ); @@ -38,9 +45,12 @@ void main() { theme: mockTheme, home: AzureApiServiceInherited( apiService: AzureApiServiceMock(), - child: StorageServiceInherited( - storageService: StorageServiceMock(), - child: CommitsPage(), + child: AdsServiceWidget( + ads: AdsServiceMock(), + child: StorageServiceInherited( + storageService: StorageServiceMock(), + child: CommitsPage(), + ), ), ), ); diff --git a/test/pipelines_test.dart b/test/pipelines_test.dart index c8bf5d6a..275875fa 100644 --- a/test/pipelines_test.dart +++ b/test/pipelines_test.dart @@ -1,4 +1,5 @@ import 'package:azure_devops/src/screens/pipelines/base_pipelines.dart'; +import 'package:azure_devops/src/services/ads_service.dart'; import 'package:azure_devops/src/services/azure_api_service.dart'; import 'package:azure_devops/src/services/storage_service.dart'; import 'package:flutter/material.dart'; @@ -19,9 +20,12 @@ void main() { theme: mockTheme, home: StorageServiceInherited( storageService: StorageServiceMock(), - child: AzureApiServiceInherited( - apiService: AzureApiServiceMock(), - child: PipelinesPage(), + child: AdsServiceWidget( + ads: AdsServiceMock(), + child: AzureApiServiceInherited( + apiService: AzureApiServiceMock(), + child: PipelinesPage(), + ), ), ), ); @@ -41,9 +45,12 @@ void main() { theme: mockTheme, home: StorageServiceInherited( storageService: StorageServiceMock(), - child: AzureApiServiceInherited( - apiService: AzureApiServiceMock(), - child: PipelinesPage(), + child: AdsServiceWidget( + ads: AdsServiceMock(), + child: AzureApiServiceInherited( + apiService: AzureApiServiceMock(), + child: PipelinesPage(), + ), ), ), ); diff --git a/test/pull_requests_test.dart b/test/pull_requests_test.dart index 8d78b40a..8eae77c9 100644 --- a/test/pull_requests_test.dart +++ b/test/pull_requests_test.dart @@ -1,14 +1,18 @@ import 'package:azure_devops/src/router/router.dart'; import 'package:azure_devops/src/screens/pull_requests/base_pull_requests.dart'; +import 'package:azure_devops/src/services/ads_service.dart'; import 'package:azure_devops/src/services/azure_api_service.dart'; import 'package:azure_devops/src/services/storage_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:visibility_detector/visibility_detector.dart'; import 'api_service_mock.dart'; /// Mock pull requests are taken from [AzureApiServiceMock.getPullRequests] void main() { + setUp(() => VisibilityDetectorController.instance.updateInterval = Duration.zero); + TestWidgetsFlutterBinding.ensureInitialized(); testWidgets( @@ -19,9 +23,12 @@ void main() { theme: mockTheme, home: StorageServiceInherited( storageService: StorageServiceMock(), - child: AzureApiServiceInherited( - apiService: AzureApiServiceMock(), - child: PullRequestsPage(), + child: AdsServiceWidget( + ads: AdsServiceMock(), + child: AzureApiServiceInherited( + apiService: AzureApiServiceMock(), + child: PullRequestsPage(), + ), ), ), ); @@ -42,9 +49,12 @@ void main() { theme: mockTheme, home: StorageServiceInherited( storageService: StorageServiceMock(), - child: AzureApiServiceInherited( - apiService: AzureApiServiceMock(), - child: PullRequestsPage(), + child: AdsServiceWidget( + ads: AdsServiceMock(), + child: AzureApiServiceInherited( + apiService: AzureApiServiceMock(), + child: PullRequestsPage(), + ), ), ), ); diff --git a/test/work_items_test.dart b/test/work_items_test.dart index cd764dbc..e2475c5d 100644 --- a/test/work_items_test.dart +++ b/test/work_items_test.dart @@ -1,13 +1,17 @@ import 'package:azure_devops/src/router/router.dart'; import 'package:azure_devops/src/screens/work_items/base_work_items.dart'; +import 'package:azure_devops/src/services/ads_service.dart'; import 'package:azure_devops/src/services/azure_api_service.dart'; import 'package:azure_devops/src/services/storage_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:visibility_detector/visibility_detector.dart'; import 'api_service_mock.dart'; void main() { + setUp(() => VisibilityDetectorController.instance.updateInterval = Duration.zero); + TestWidgetsFlutterBinding.ensureInitialized(); testWidgets( @@ -18,9 +22,12 @@ void main() { theme: mockTheme, home: StorageServiceInherited( storageService: StorageServiceMock(), - child: AzureApiServiceInherited( - apiService: AzureApiServiceMock(), - child: WorkItemsPage(), + child: AdsServiceWidget( + ads: AdsServiceMock(), + child: AzureApiServiceInherited( + apiService: AzureApiServiceMock(), + child: WorkItemsPage(), + ), ), ), );