Skip to content

Commit

Permalink
Add native ads in list pages (#55)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sstasi95 authored Feb 7, 2025
1 parent a6adbaf commit e4aa36f
Show file tree
Hide file tree
Showing 26 changed files with 509 additions and 235 deletions.
17 changes: 17 additions & 0 deletions lib/src/mixins/ads_mixin.dart
Original file line number Diff line number Diff line change
@@ -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<AdWithKey> ads = [];

/// Load new native ads and map them to [AdWithKey] objects with a new global key to force refresh the UI.
Future<void> 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<T>(List<T> items, T item, int index) =>
items.indexOf(item) % 5 == 4 && item != items.first && index < ads.length;
}
5 changes: 4 additions & 1 deletion lib/src/screens/board_detail/base_board_detail.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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),
);
Expand Down
12 changes: 10 additions & 2 deletions lib/src/screens/board_detail/controller_board_detail.dart
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<BoardDetailWithItems>?>(null);
Expand Down Expand Up @@ -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<void> _showInterstitialAd({VoidCallback? onDismiss}) async {
await ads.showInterstitialAd(onDismiss: onDismiss);
}
}
7 changes: 6 additions & 1 deletion lib/src/screens/commits/base_commits.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
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';
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';
Expand All @@ -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),
);
Expand Down
14 changes: 10 additions & 4 deletions lib/src/screens/commits/controller_commits.dart
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<List<Commit>?>?>(null);
Expand All @@ -29,6 +30,11 @@ class _CommitsController with FilterMixin, ApiErrorHelper {
_fillShortcutFilters();
}

await _getDataAndAds();
}

Future<void> _getDataAndAds() async {
await getNewNativeAds(adsService);
await _getData();
}

Expand Down Expand Up @@ -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());
Expand All @@ -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());
Expand Down
28 changes: 19 additions & 9 deletions lib/src/screens/commits/screen_commits.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
);
},
);
}
}
7 changes: 6 additions & 1 deletion lib/src/screens/pipelines/base_pipelines.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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),
);
Expand Down
22 changes: 14 additions & 8 deletions lib/src/screens/pipelines/controller_pipelines.dart
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<List<Pipeline>?>?>(null);
Expand Down Expand Up @@ -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;
Expand All @@ -72,6 +73,11 @@ class _PipelinesController with FilterMixin, ApiErrorHelper {
}
}

Future<void> _getDataAndAds() async {
await getNewNativeAds(adsService);
await _getData();
}

void _fillSavedFilters() {
final savedFilters = filtersService.getPipelinesSavedFilters();
_fillFilters(savedFilters);
Expand Down Expand Up @@ -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());
Expand All @@ -164,7 +170,7 @@ class _PipelinesController with FilterMixin, ApiErrorHelper {

pipelines.value = null;
resultFilter = result;
_getData();
_getDataAndAds();

if (shouldPersistFilters) {
filtersService.savePipelinesResultFilter(result.stringValue);
Expand All @@ -176,7 +182,7 @@ class _PipelinesController with FilterMixin, ApiErrorHelper {

pipelines.value = null;
statusFilter = status;
_getData();
_getDataAndAds();

if (shouldPersistFilters) {
filtersService.savePipelinesStatusFilter(status.stringValue);
Expand All @@ -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());
Expand All @@ -200,7 +206,7 @@ class _PipelinesController with FilterMixin, ApiErrorHelper {

pipelines.value = null;
pipelineNamesFilter = names;
_getData();
_getDataAndAds();

if (shouldPersistFilters) {
filtersService.savePipelinesNamesFilter(names);
Expand Down
48 changes: 30 additions & 18 deletions lib/src/screens/pipelines/screen_pipelines.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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++],
);
}
},
),
),
],
),
],
);
},
),
);
}
Expand Down
6 changes: 5 additions & 1 deletion lib/src/screens/profile/base_profile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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),
);
Expand Down
Loading

0 comments on commit e4aa36f

Please sign in to comment.