Anúncios do Google em um aplicativo Flutter em camadas usando gerenciamento de estado BLoC
Neste artigo, exploraremos os aspectos técnicos da integração do Google Ads e da AdMob em seu aplicativo Flutter, enfatizando a importância de padrões de arquitetura e gerenciamento de estado sustentáveis e escaláveis.
O Google Ads pode ser uma ferramenta valiosa para monetizar seu aplicativo Flutter. Eles permitem que você obtenha receita exibindo anúncios relevantes para seus usuários de uma vasta rede de anunciantes. O Google Ads usa algoritmos de segmentação sofisticados para exibir anúncios para usuários com maior probabilidade de se interessar pelos produtos ou serviços anunciados, garantindo uma ampla variedade de formatos e campanhas publicitárias. Além de tudo isso, ele fornece um conjunto de ferramentas e APIs amigáveis ao desenvolvedor para integrar facilmente anúncios ao seu aplicativo Flutter. Você pode aproveitar o Google Mobile Ads SDK for Flutter para exibir anúncios em vários formatos, como banners, intersticiais, recompensados ou anúncios nativos usando os modelos nativos mais recentes do Dart.
No entanto, os desenvolvedores adiam ou até mesmo evitam a implementação do Google Ads em seus Flutter Apps por uma série de motivos mais do que válidos:
- Priorizando a funcionalidade principal — Os desenvolvedores geralmente se concentram na implementação dos principais recursos e funcionalidades de seu aplicativo antes de integrar os anúncios. Eles podem querer garantir que o objetivo principal do aplicativo seja totalmente desenvolvido e otimizado antes de introduzir anúncios, pois os anúncios às vezes podem exigir esforços e considerações adicionais.
- Complexidade de integração — A integração de anúncios em um aplicativo Flutter envolve etapas técnicas, como instalar os SDKs necessários, configurar blocos de anúncios, lidar com solicitações e respostas de anúncios e gerenciar posicionamentos de anúncios. Esse processo de integração pode exigir tempo e esforço, especialmente para desenvolvedores novos na monetização de anúncios ou que têm experiência limitada com redes de anúncios.
- Considerações sobre a experiência do usuário: os desenvolvedores desejam garantir que os anúncios integrados não afetem negativamente a experiência do usuário. Anúncios intrusivos ou mal colocados podem levar à frustração do usuário e até mesmo resultar na desinstalação do aplicativo.
- Impacto no desempenho do aplicativo: os desenvolvedores podem estar preocupados com os possíveis efeitos negativos dos anúncios no desempenho do aplicativo. Os anúncios, se não forem implementados corretamente, podem aumentar o tempo de carregamento do aplicativo, consumir recursos excessivos ou causar falhas na interface do usuário. Abordar essas questões de desempenho requer otimização e testes cuidadosos, o que pode fazer com que alguns desenvolvedores adiem a integração.
Em última análise, esta postagem minimizará suas preocupações sobre os pontos listados acima, orientando você na integração perfeita do Google Ads e da AdMob em seu aplicativo Flutter, priorizando uma base arquitetônica sólida e práticas recomendadas de gerenciamento de estado. No final desta postagem, você terá uma versão anunciada do aplicativo Flutter padrão.
Preparar? Então aperte os cintos porque estamos prestes a mergulhar no emocionante mundo da monetização de anúncios e liberar todo o potencial de receita do seu aplicativo Flutter.
Conteudo
Configurar
Vamos começar com todas as configurações para tirá-las do caminho antes de examinarmos as coisas mais interessantes. Se você preferir ir direto ao repositório do projeto de amostra e dissecar o código você mesmo ou usá-lo como referência ao ler o guia, aqui está o link.
Primeiramente, vamos criar um Flutter App usando very_good_cli. Poderíamos ter usado “flutter create” em vez disso, mas a ferramenta cli do VGV nos ajudará a acelerar o processo, pois estrutura o código de uma maneira mais alinhada com nossas necessidades. Por exemplo, ele já inclui flutter_bloc e very_good_analysis como dependências e fornece uma versão modificada do famoso Flutter Counter App aproveitando o padrão Page-View.
Em segundo lugar, acesse o guia de introdução oficial do Google para integrar o Google Ads SDK ao seu aplicativo Flutter, crie uma conta da AdMob para registrar um aplicativo Android e iOS e adicione as configurações específicas da plataforma para cada aplicativo. Observe que você pode pular a parte Inicializar o Mobile SDK por enquanto, pois abordaremos isso mais tarde.
Feito isso, adicione os blocos de anúncios que deseja incluir em seu aplicativo. Para este guia, criei 5 anúncios diferentes para cada plataforma (Android, iOS). Certifique-se de criar o mesmo tipo de bloco de anúncios em ambas as plataformas e nomeá-los da mesma forma para acompanhá-los mais facilmente em seu código. Aqui está a lista de anúncios que você encontrará no aplicativo de exemplo:
Banner Ads
- counterPageBottomBanner
- counterPageTopBanner
Native Ad
- counterPageNativeAd
Interstitial Ads
- counterPagePlusCheckInterstitial
- counterPageMinusCheckInterstitial
Por fim, certifique-se de que seu arquivo pubspec.yaml tenha a seguinte aparência.
name: flutter_ads description: A sample Flutter App to show how to implement Google Ads in a Flutter App Using a Layered Architecture and BLoC State Management. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.0.0 <4.0.0" flutter: 3.10.5 dependencies: ads_client: path: packages/ads_client ads_repo: path: packages/ads_repo bloc: ^8.1.2 equatable: ^2.0.5 flutter: sdk: flutter flutter_bloc: ^8.1.3 flutter_localizations: sdk: flutter google_mobile_ads: ^3.0.0 intl: ^0.18.0 dev_dependencies: bloc_test: ^9.1.2 flutter_test: sdk: flutter mocktail: ^0.3.0 very_good_analysis: ^5.0.0 flutter: uses-material-design: true generate: true
ads_client
Tudo bem, vamos ao que interessa. Na raiz do diretório do seu projeto, crie uma pasta “packages”, navegue até ela e execute o seguinte comando para criar um pacote flutter
very_good create flutter_package ads_client
Em seguida, certifique-se de adicionar a dependência google_mobile_ads ao arquivo pubspec.yaml deste pacote.
Vamos agora percorrer o código exibido por este pacote.
Sob o diretório src, precisamos adicionar um arquivo ads.dart incluindo uma enumeração chamada Ads que atuará como uma enumeração auxiliar listando todos os anúncios da página de contador a serem exibidos. Observe que ele oferece suporte a anúncios iOS e Android para compilações de produção e depuração, graças aos campos obrigatórios ios, iosTest, android e androidTest. Mais importante ainda, certifique-se de que o nome do valor enum corresponda ao nome dos blocos de anúncios criados na seção anterior e adicione seus IDs de anúncios correspondentes com base na plataforma de destino. Com relação aos IDs de anúncios de teste, você pode simplesmente copiar as strings para cada campo iOSTest e androidTest, pois são estáticos e vêm direto dos guias oficiais de cada plataforma (Android, iOS).
/// {@template ads} /// Helper enum listing all the Counter Ads to be displayed. /// /// It supports iOS and Android ads for both production and debug builds. /// {@endtemplate} enum Ads { /// Banner Ad positioned at the bottom of the Counter Page. counterPageBottomBanner( iOS: 'ca-app-pub-7287154298486616/4742754588', android: 'ca-app-pub-7287154298486616/1278152293', iOSTest: 'ca-app-pub-3940256099942544/2934735716', androidTest: 'ca-app-pub-3940256099942544/6300978111', ), /// Banner Ad positioned at the top of the Counter Page. counterPageTopBanner( iOS: 'ca-app-pub-7287154298486616/6438979635', android: 'ca-app-pub-7287154298486616/6054221858', iOSTest: 'ca-app-pub-3940256099942544/2934735716', androidTest: 'ca-app-pub-3940256099942544/6300978111', ), /// Interstitial Ad displayed in the Counter Page /// when users increment the counter to a given positive threshold. counterPagePlusCheckInterstitial( iOS: 'ca-app-pub-7287154298486616/5864264568', android: 'ca-app-pub-7287154298486616/8488813505', iOSTest: 'ca-app-pub-3940256099942544/4411468910', androidTest: 'ca-app-pub-3940256099942544/1033173712', ), /// Interstitial Ad displayed in the Counter Page /// when users increment the counter to a given negative threshold. counterPageMinusCheckInterstitial( iOS: 'ca-app-pub-7287154298486616/2498120224', android: 'ca-app-pub-7287154298486616/9228794506', iOSTest: 'ca-app-pub-3940256099942544/4411468910', androidTest: 'ca-app-pub-3940256099942544/1033173712', ), /// Native Ad displayed in the Counter Page. counterPageNative( iOS: 'ca-app-pub-7287154298486616/6454457353', android: 'ca-app-pub-7287154298486616/1923405151', iOSTest: 'ca-app-pub-3940256099942544/3986624511', androidTest: 'ca-app-pub-3940256099942544/2247696110', ); const Ads({ required this.iOS, required this.android, required this.iOSTest, required this.androidTest, }); /// iOS Ad id. final String iOS; /// Android Ad id. final String android; /// iOS Ad id for testing. final String iOSTest; /// Android Ad id for testing. final String androidTest; }
Em seguida, vamos analisar a classe AdsClient.
Assim que criamos uma instância dessa classe, inicializamos todos os anúncios desejados com base na plataforma em que o Flutter App está sendo executado, bem como o tipo de compilação — se estiver em modo de depuração, não queremos exibir anúncios reais, mas sim testar anúncios apenas para garantir que tudo esteja funcionando conforme o esperado. Vale ressaltar que não estamos inicializando nenhum anúncio aqui, pois isso prejudicaria o desempenho do aplicativo na inicialização. Estamos simplesmente preenchendo um Mapa de <Ads, String> que inclui os valores da enumeração que mencionamos anteriormente como chaves e a string de ID do anúncio correspondente como um valor.
AdsClient() { _ads = _initializeAds(); } late final Map<Ads, String> _ads; Map<Ads, String> _initializeAds() { final ads = <Ads, String>{}; for (final ad in Ads.values) { if (Platform.isIOS) { if (kDebugMode) { ads[ad] = ad.iOSTest; } else { ads[ad] = ad.iOS; } } else { if (kDebugMode) { ads[ad] = ad.androidTest; } else { ads[ad] = ad.android; } } } return ads; }
Além disso, vamos discutir os métodos de preenchimento de anúncios. No código abaixo, podemos observar que as três funções se comportam de maneira semelhante. Primeiro declaramos um adCompleter como um Completer<Ad?> que precisaremos aguardar até que o anúncio desejado seja carregado e possa ser retornado. No entanto, observe como _populateInterstitialAd requer um parâmetro VoidCallback onAdDismissedFullScreenContent, que é uma função de retorno de chamada que será executada assim que o anúncio intersticial for dispensado. Esse tipo de injeção de parâmetro terá um papel crucial quando conectarmos o processo de inicialização do anúncio com o gerenciamento de estado do aplicativo, como você verá nas próximas seções. Por fim, garantimos que o tratamento de erros seja implementado adequadamente para todos os tipos de anúncios, de modo que inicializá-los nunca trave nosso aplicativo, dando-nos a chance de lidar com a exceção conforme julgarmos apropriado. Para esta demonstração, incluí apenas três tipos de anúncios, mas uma abordagem semelhante pode ser implementada para os outros tipos de anúncios, como premiado, intersticial premiado, aplicativo aberto e assim por diante.
Future<BannerAd> _populateBannerAd({ required String adUnitId, AdSize? size, }) async { try { final adCompleter = Completer<Ad?>(); await BannerAd( adUnitId: adUnitId, size: size ?? AdSize.banner, request: const AdRequest(), listener: BannerAdListener( onAdLoaded: adCompleter.complete, onAdFailedToLoad: (ad, error) { // Releases an ad resource when it fails to load ad.dispose(); adCompleter.completeError(error); }, ), ).load(); final bannerAd = await adCompleter.future; if (bannerAd == null) { throw const AdsClientException('Banner Ad was null'); } return bannerAd as BannerAd; } catch (error, stackTrace) { Error.throwWithStackTrace(AdsClientException(error), stackTrace); } } Future<NativeAd> _populateNativeAd({ required String adUnitId, TemplateType? templateType, }) async { try { final adCompleter = Completer<Ad?>(); await NativeAd( adUnitId: adUnitId, listener: NativeAdListener( onAdLoaded: adCompleter.complete, onAdFailedToLoad: (ad, error) { // Releases an ad resource when it fails to load ad.dispose(); adCompleter.completeError(error); }, ), request: const AdRequest(), nativeTemplateStyle: NativeTemplateStyle( templateType: templateType ?? TemplateType.medium, ), ).load(); final nativeAd = await adCompleter.future; if (nativeAd == null) { throw const AdsClientException('Native Ad was null'); } return nativeAd as NativeAd; } catch (error, stackTrace) { Error.throwWithStackTrace(AdsClientException(error), stackTrace); } } Future<InterstitialAd> _populateInterstitialAd({ required String adUnitId, required VoidCallback onAdDismissedFullScreenContent, }) async { try { final adCompleter = Completer<Ad?>(); await InterstitialAd.load( adUnitId: adUnitId, request: const AdRequest(), adLoadCallback: InterstitialAdLoadCallback( onAdLoaded: (ad) { ad.fullScreenContentCallback = FullScreenContentCallback( onAdDismissedFullScreenContent: (ad) { onAdDismissedFullScreenContent(); }, ); adCompleter.complete(ad); }, onAdFailedToLoad: adCompleter.completeError, ), ); final interstitialAd = await adCompleter.future; if (interstitialAd == null) { throw const AdsClientException('Interstitial Ad was null'); } return interstitialAd as InterstitialAd; } catch (error, stackTrace) { Error.throwWithStackTrace(AdsClientException(error), stackTrace); } }
Para encerrar a classe AdsClient, precisamos incluir todos os métodos públicos dedicados que nos permitirão inicializar esses anúncios sob demanda. Observe como cada método corresponde a um único anúncio definido na enumeração Anúncios. Por fim, essa abordagem é super consistente e intuitiva, pois exige apenas que os desenvolvedores saibam qual método de preenchimento precisa ser usado com base no tipo de anúncio (banner, nativo, intersticial…) e no valor de enumeração de anúncios para acessar o par chave/valor correto do mapa _ads.
/// Gets a banner Ad positioned at the top of the Counter Page. Future<BannerAd> getCounterPageTopBannerAd() async { try { return await _populateBannerAd(adUnitId: _ads[Ads.counterPageTopBanner]!); } catch (error, stackTrace) { Error.throwWithStackTrace(AdsClientException(error), stackTrace); } } /// Gets a banner Ad positioned at the bottom of the Counter Page. Future<BannerAd> getCounterPageBottomBannerAd() async { try { return await _populateBannerAd( adUnitId: _ads[Ads.counterPageBottomBanner]!, ); } catch (error, stackTrace) { Error.throwWithStackTrace(AdsClientException(error), stackTrace); } } /// Gets an interstitial Ad displayed in the Counter Page /// when users increment the counter to a given positive threshold. Future<InterstitialAd> getCounterPagePlusCheckInterstitialAd({ required VoidCallback onAdDismissedFullScreenContent, }) async { try { return await _populateInterstitialAd( adUnitId: _ads[Ads.counterPagePlusCheckInterstitial]!, onAdDismissedFullScreenContent: onAdDismissedFullScreenContent, ); } catch (error, stackTrace) { Error.throwWithStackTrace(AdsClientException(error), stackTrace); } } /// Gets an interstitial Ad displayed in the Counter Page /// when users increment the counter to a given negative threshold. Future<InterstitialAd> getCounterPageMinusCheckInterstitialAd({ required VoidCallback onAdDismissedFullScreenContent, }) async { try { return await _populateInterstitialAd( adUnitId: _ads[Ads.counterPageMinusCheckInterstitial]!, onAdDismissedFullScreenContent: onAdDismissedFullScreenContent, ); } catch (error, stackTrace) { Error.throwWithStackTrace(AdsClientException(error), stackTrace); } } /// Gets a native Ad displayed in the Counter Page. Future<NativeAd> getCounterPageNativeAd() async { try { return await _populateNativeAd(adUnitId: _ads[Ads.counterPageNative]!); } catch (error, stackTrace) { Error.throwWithStackTrace(AdsClientException(error), stackTrace); } }
ads_repo
Passando para a camada do repositório agora. Da mesma forma que fizemos com o pacote anterior, vamos criar outro pacote com o seguinte comando.
very_good create flutter_package ads_repo
Em seguida, certifique-se de adicionar o google_mobile_ads e o ads_client criado anteriormente como dependências ao arquivo pubspec.yaml deste pacote.
Este pacote irá expor dois arquivos contendo os padrões de repositório a serem retornados pelos métodos da classe AdsRepo e a implementação da referida classe.
O ads_repo_patterns.dart define assinaturas de todos os valores a serem retornados para as camadas superiores.
import 'package:google_mobile_ads/google_mobile_ads.dart'; /// Base pattern for all return patterns at the Repository level. typedef RepoPattern<O, S> = ({RepoFailure<O>? failure, S? value}); /// Pattern representing a failure returned a given Repository. typedef RepoFailure<O> = ({Object error, StackTrace stackTrace, O? optional}); /// Pattern returned by any AdsRepo method that returns a [BannerAd]. typedef BannerAdPattern = RepoPattern<void, BannerAd>; /// Pattern returned by any AdsRepo method that returns a [InterstitialAd]. typedef InterstitialAdPattern = RepoPattern<void, InterstitialAd>; /// Pattern returned by any AdsRepo method that returns a [NativeAd]. typedef NativeAdPattern = RepoPattern<void, NativeAd>;
Quanto à implementação da classe AdsRepo, garantimos a necessidade de uma instância da classe AdsClient como parâmetro para o construtor do repositório. Esse tipo de padrão de injeção de dependência percorre um longo caminho ao implementar uma arquitetura em camadas adequada e 100% de cobertura de código em sua base de código. Por fim, nos certificamos de adicionar tantos métodos públicos quanto adicionamos no nível do cliente. Nesse caso, esses métodos atuarão como intermediários, encaminhando a resposta correta de volta para as camadas superiores e manipulando adequadamente quaisquer exceções lançadas.
/// Gets a banner Ad positioned at the top of the Counter Page. Future<BannerAdPattern> getCounterPageTopBannerAd() async { try { final bannerAd = await _adsClient.getCounterPageTopBannerAd(); return (failure: null, value: bannerAd); } catch (e, st) { return ( failure: ( error: e, stackTrace: st, optional: 'Exception caught in getCounterPageTopBannerAd' ), value: null, ); } } /// Gets a banner Ad positioned at the bottom of the Counter Page. Future<BannerAdPattern> getCounterPageBottomBannerAd() async { try { final bannerAd = await _adsClient.getCounterPageBottomBannerAd(); return (failure: null, value: bannerAd); } catch (e, st) { return ( failure: ( error: e, stackTrace: st, optional: 'Exception caught in getCounterPageBottomBannerAd' ), value: null, ); } } /// Gets an interstitial Ad displayed in the Counter Page /// when users increment the counter to a given positive threshold. Future<InterstitialAdPattern> getCounterPagePlusCheckInterstitialAd({ required VoidCallback onAdDismissedFullScreenContent, }) async { try { final interstitialAd = await _adsClient.getCounterPagePlusCheckInterstitialAd( onAdDismissedFullScreenContent: onAdDismissedFullScreenContent, ); return (failure: null, value: interstitialAd); } catch (e, st) { return ( failure: ( error: e, stackTrace: st, optional: 'Exception caught in getCounterPagePlusCheckInterstitialAd', ), value: null, ); } } /// Gets an interstitial Ad displayed in the Counter Page /// when users increment the counter to a given negative threshold. Future<InterstitialAdPattern> getCounterPageMinusCheckInterstitialAd({ required VoidCallback onAdDismissedFullScreenContent, }) async { try { final interstitialAd = await _adsClient.getCounterPageMinusCheckInterstitialAd( onAdDismissedFullScreenContent: onAdDismissedFullScreenContent, ); return (failure: null, value: interstitialAd); } catch (e, st) { return ( failure: ( error: e, stackTrace: st, optional: 'Exception caught in getCounterPageMinusCheckInterstitialAd', ), value: null, ); } } /// Gets a native Ad displayed in the Counter Page. Future<NativeAdPattern> getCounterPageNativeAd() async { try { final nativeAd = await _adsClient.getCounterPageNativeAd(); return (failure: null, value: nativeAd); } catch (e, st) { return ( failure: ( error: e, stackTrace: st, optional: 'Exception caught in getCounterPageNativeAd' ), value: null, ); } }
ads_bloc
Subindo um degrau em nossa escada de arquitetura em camadas, encontramos a camada de lógica de negócios, onde colocaremos nosso AdsBloc junto com seus eventos e estado. A responsabilidade desse bloco será lidar com a solicitação e rejeição de nossos anúncios de aplicativos de maneira determinística, sem bloqueio e consistente. Em outras palavras, ele manipulará o estado de todos os nossos anúncios para garantir que a interface do usuário pareça e reaja às atualizações relacionadas ao anúncio conforme o esperado. Para isso, criaremos uma pasta ads dentro do diretório lib, e incluiremos uma pasta bloc com três arquivos: ads_bloc.dart, ads_event.dart, ads_state.dart.
Vamos primeiro olhar para os eventos do bloco. Essencialmente, declaramos um evento de solicitação e dispensa para cada um dos anúncios que queremos exibir em nosso aplicativo. Observe como o evento de solicitação de anúncios intersticiais inclui um parâmetro obrigatório, a função de retorno de chamada mencionada na seção ads_client, que nos permitirá executar algum código antes de dispensar o anúncio intersticial.
part of 'ads_bloc.dart'; @immutable abstract class AdsEvent extends Equatable { const AdsEvent(); @override List<Object> get props => []; } class AdsCounterPageBottomBannerAdRequested extends AdsEvent {} class AdsCounterPageBottomBannerAdDisposed extends AdsEvent {} class AdsCounterPageTopBannerAdRequested extends AdsEvent {} class AdsCounterPageTopBannerAdDisposed extends AdsEvent {} class AdsCounterPageNativeAdRequested extends AdsEvent {} class AdsCounterPageNativeAdDisposed extends AdsEvent {} class AdsCounterPagePlusCheckInterstitialAdRequested extends AdsEvent { const AdsCounterPagePlusCheckInterstitialAdRequested({ required this.onAdDismissedFullScreenContent, }); final VoidCallback onAdDismissedFullScreenContent; @override List<Object> get props => [onAdDismissedFullScreenContent]; } class AdsCounterPagePlusCheckInterstitialAdDisposed extends AdsEvent {} class AdsCounterPageMinusCheckInterstitialAdRequested extends AdsEvent { const AdsCounterPageMinusCheckInterstitialAdRequested({ required this.onAdDismissedFullScreenContent, }); final VoidCallback onAdDismissedFullScreenContent; @override List<Object> get props => [onAdDismissedFullScreenContent]; } class AdsCounterPageMinusCheckInterstitialAdDisposed extends AdsEvent {}
Em seguida, vamos analisar o estado do bloco. A classe AdsState simplesmente armazena o estado do AdsBloc, mantendo uma referência aos 5 anúncios diferentes que queremos mostrar em nosso App. Observe que esses valores podem ser nulos em qualquer ponto, então adicionamos alguns getters para melhorar a legibilidade no nível da interface do usuário. Vale a pena notar que temos apenas um único estado imutável que expõe uma série de métodos copyWith que nos permitirão emitir novos estados com os campos desejados.
// ignore_for_file: public_member_api_docs, sort_constructors_first part of 'ads_bloc.dart'; class AdsState extends Equatable { const AdsState({ this.counterPageBottomBannerAd, this.counterPageTopBannerAd, this.counterPageNativeAd, this.counterPagePlusCheckInterstitialAd, this.counterPageMinusCheckInterstitialAd, }); final BannerAd? counterPageBottomBannerAd; final BannerAd? counterPageTopBannerAd; final NativeAd? counterPageNativeAd; final InterstitialAd? counterPagePlusCheckInterstitialAd; final InterstitialAd? counterPageMinusCheckInterstitialAd; bool get didCounterPageBottomBannerAdLoad => counterPageBottomBannerAd != null; bool get didCounterPageTopBannerAdLoad => counterPageTopBannerAd != null; bool get didCounterPagePlusCheckInterstitialAdAdLoad => counterPagePlusCheckInterstitialAd != null; bool get didCounterPageMinusCheckInterstitialAdAdLoad => counterPageMinusCheckInterstitialAd != null; bool get didCounterPageNativeAdLoad => counterPageNativeAd != null; @override List<Object?> get props => [ counterPageBottomBannerAd, counterPageTopBannerAd, counterPageNativeAd, counterPagePlusCheckInterstitialAd, counterPageMinusCheckInterstitialAd, ]; AdsState copyWith({ BannerAd? counterPageBottomBannerAd, BannerAd? counterPageTopBannerAd, NativeAd? counterPageNativeAd, InterstitialAd? counterPagePlusCheckInterstitialAd, InterstitialAd? counterPageMinusCheckInterstitialAd, }) { return AdsState( counterPageBottomBannerAd: counterPageBottomBannerAd ?? this.counterPageBottomBannerAd, counterPageTopBannerAd: counterPageTopBannerAd ?? this.counterPageTopBannerAd, counterPageNativeAd: counterPageNativeAd ?? this.counterPageNativeAd, counterPagePlusCheckInterstitialAd: counterPagePlusCheckInterstitialAd ?? this.counterPagePlusCheckInterstitialAd, counterPageMinusCheckInterstitialAd: counterPageMinusCheckInterstitialAd ?? this.counterPageMinusCheckInterstitialAd, ); } AdsState copyWithoutCounterPageBottomBannerAd() { return AdsState( counterPageTopBannerAd: counterPageTopBannerAd, counterPageNativeAd: counterPageNativeAd, counterPagePlusCheckInterstitialAd: counterPagePlusCheckInterstitialAd, counterPageMinusCheckInterstitialAd: counterPageMinusCheckInterstitialAd, ); } AdsState copyWithoutCounterPageTopBannerAd() { return AdsState( counterPageBottomBannerAd: counterPageBottomBannerAd, counterPageNativeAd: counterPageNativeAd, counterPagePlusCheckInterstitialAd: counterPagePlusCheckInterstitialAd, counterPageMinusCheckInterstitialAd: counterPageMinusCheckInterstitialAd, ); } AdsState copyWithoutCounterPageNativeAd() { return AdsState( counterPageTopBannerAd: counterPageTopBannerAd, counterPageBottomBannerAd: counterPageBottomBannerAd, counterPagePlusCheckInterstitialAd: counterPagePlusCheckInterstitialAd, counterPageMinusCheckInterstitialAd: counterPageMinusCheckInterstitialAd, ); } AdsState copyWithoutCounterPagePlusCheckInterstitialAd() { return AdsState( counterPageBottomBannerAd: counterPageBottomBannerAd, counterPageTopBannerAd: counterPageTopBannerAd, counterPageNativeAd: counterPageNativeAd, counterPageMinusCheckInterstitialAd: counterPageMinusCheckInterstitialAd, ); } AdsState copyWithoutCounterPageMinusCheckInterstitialAd() { return AdsState( counterPageBottomBannerAd: counterPageBottomBannerAd, counterPageTopBannerAd: counterPageTopBannerAd, counterPageNativeAd: counterPageNativeAd, counterPagePlusCheckInterstitialAd: counterPagePlusCheckInterstitialAd, ); } }
Por último, mas não menos importante, vamos mergulhar na implementação da classe AdsBloc. Essa classe nos ajuda a lidar com eventos acionados e emitir o estado do bloco correspondente. Observe como temos um único método privado (handler) por BlocEvent, forçando o desacoplamento lógico e a devida separação de preocupações, mesmo que a implementação de cada método seja bastante repetitiva e sistemática. Por fim, aproveitamos a mesma abordagem de injeção de dependência mencionada anteriormente, exigindo uma instância de AdsRepo como um parâmetro do construtor, o que nos permitirá comunicar com o nível do repositório.
import 'dart:async'; import 'package:ads_repo/ads_repo.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; part 'ads_event.dart'; part 'ads_state.dart'; class AdsBloc extends Bloc<AdsEvent, AdsState> { AdsBloc({ required AdsRepo adsRepo, }) : _adsRepo = adsRepo, super(const AdsState()) { on<AdsCounterPageBottomBannerAdRequested>( _counterPageBottomBannerAdRequested, ); on<AdsCounterPageBottomBannerAdDisposed>( _counterPageBottomBannerAdDisposed, ); on<AdsCounterPageTopBannerAdRequested>(_counterPageTopBannerAdRequested); on<AdsCounterPageTopBannerAdDisposed>(_counterPageTopBannerAdDisposed); on<AdsCounterPageNativeAdRequested>(_counterPageNativeAdRequested); on<AdsCounterPageNativeAdDisposed>(_counterPageNativeAdDisposed); on<AdsCounterPagePlusCheckInterstitialAdRequested>( _counterPagePlusCheckInterstitialAdRequested, ); on<AdsCounterPagePlusCheckInterstitialAdDisposed>( _counterPagePlusCheckInterstitialAdDisposed, ); on<AdsCounterPageMinusCheckInterstitialAdRequested>( _counterPageMinusCheckInterstitialAdRequested, ); on<AdsCounterPageMinusCheckInterstitialAdDisposed>( _counterPageMinusCheckInterstitialAdDisposed, ); } final AdsRepo _adsRepo; FutureOr<void> _counterPageBottomBannerAdRequested( AdsCounterPageBottomBannerAdRequested event, Emitter<AdsState> emit, ) async { final pattern = await _adsRepo.getCounterPageBottomBannerAd(); switch (pattern) { case (failure: null, value: final BannerAd ad): return emit(state.copyWith(counterPageBottomBannerAd: ad)); case (failure: final RepoFailure<String> failure, value: null): addError(failure.error, failure.stackTrace); } } FutureOr<void> _counterPageBottomBannerAdDisposed( AdsCounterPageBottomBannerAdDisposed event, Emitter<AdsState> emit, ) { state.counterPageTopBannerAd?.dispose(); emit(state.copyWithoutCounterPageBottomBannerAd()); } FutureOr<void> _counterPageTopBannerAdRequested( AdsCounterPageTopBannerAdRequested event, Emitter<AdsState> emit, ) async { final pattern = await _adsRepo.getCounterPageTopBannerAd(); switch (pattern) { case (failure: null, value: final BannerAd ad): return emit(state.copyWith(counterPageTopBannerAd: ad)); case (failure: final RepoFailure<String> failure, value: null): addError(failure.error, failure.stackTrace); } } FutureOr<void> _counterPageTopBannerAdDisposed( AdsCounterPageTopBannerAdDisposed event, Emitter<AdsState> emit, ) { state.counterPageTopBannerAd?.dispose(); emit(state.copyWithoutCounterPageTopBannerAd()); } FutureOr<void> _counterPagePlusCheckInterstitialAdRequested( AdsCounterPagePlusCheckInterstitialAdRequested event, Emitter<AdsState> emit, ) async { if (state.didCounterPagePlusCheckInterstitialAdAdLoad) return; final pattern = await _adsRepo.getCounterPagePlusCheckInterstitialAd( onAdDismissedFullScreenContent: event.onAdDismissedFullScreenContent, ); switch (pattern) { case (failure: null, value: final InterstitialAd ad): return emit(state.copyWith(counterPagePlusCheckInterstitialAd: ad)); case (failure: final RepoFailure<String> failure, value: null): addError(failure.error, failure.stackTrace); } } FutureOr<void> _counterPagePlusCheckInterstitialAdDisposed( AdsCounterPagePlusCheckInterstitialAdDisposed event, Emitter<AdsState> emit, ) { state.counterPageMinusCheckInterstitialAd?.dispose(); emit(state.copyWithoutCounterPagePlusCheckInterstitialAd()); } FutureOr<void> _counterPageMinusCheckInterstitialAdRequested( AdsCounterPageMinusCheckInterstitialAdRequested event, Emitter<AdsState> emit, ) async { if (state.didCounterPageMinusCheckInterstitialAdAdLoad) return; final pattern = await _adsRepo.getCounterPageMinusCheckInterstitialAd( onAdDismissedFullScreenContent: event.onAdDismissedFullScreenContent, ); switch (pattern) { case (failure: null, value: final InterstitialAd ad): return emit(state.copyWith(counterPageMinusCheckInterstitialAd: ad)); case (failure: final RepoFailure<String> failure, value: null): addError(failure.error, failure.stackTrace); } } FutureOr<void> _counterPageMinusCheckInterstitialAdDisposed( AdsCounterPageMinusCheckInterstitialAdDisposed event, Emitter<AdsState> emit, ) { state.counterPageMinusCheckInterstitialAd?.dispose(); emit(state.copyWithoutCounterPageMinusCheckInterstitialAd()); } FutureOr<void> _counterPageNativeAdRequested( AdsCounterPageNativeAdRequested event, Emitter<AdsState> emit, ) async { final pattern = await _adsRepo.getCounterPageNativeAd(); switch (pattern) { case (failure: null, value: final NativeAd ad): return emit(state.copyWith(counterPageNativeAd: ad)); case (failure: final RepoFailure<String> failure, value: null): addError(failure.error, failure.stackTrace); } } FutureOr<void> _counterPageNativeAdDisposed( AdsCounterPageNativeAdDisposed event, Emitter<AdsState> emit, ) { state.counterPageNativeAd?.dispose(); emit(state.copyWithoutCounterPageNativeAd()); } }
IU do aplicativo
É hora do Flutter!
Vamos criar uma pasta de widgets no diretório de anúncios e colocar vários arquivos para lidar com a criação dos widgets que desenharão o banner e os anúncios nativos na interface do usuário do aplicativo Counter.
counter_page_bottom_banner_ad.dart
import 'package:flutter/material.dart'; import 'package:flutter_ads/ads/ads.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; class CounterPageBottomBannerAd extends StatefulWidget { const CounterPageBottomBannerAd({super.key}); static const height = 72.0; @override State<CounterPageBottomBannerAd> createState() => _CounterPageBottomBannerAdState(); } class _CounterPageBottomBannerAdState extends State<CounterPageBottomBannerAd> { late AdsBloc adsBloc; @override void dispose() { adsBloc.add(AdsCounterPageBottomBannerAdDisposed()); super.dispose(); } @override Widget build(BuildContext context) { adsBloc = context.read<AdsBloc>() ..add(AdsCounterPageBottomBannerAdRequested()); return BlocBuilder<AdsBloc, AdsState>( buildWhen: (pre, cur) => pre.counterPageBottomBannerAd != cur.counterPageBottomBannerAd, builder: (context, state) { if (!state.didCounterPageBottomBannerAdLoad) { return const SizedBox.shrink(); } return SizedBox( width: double.infinity, height: CounterPageBottomBannerAd.height, child: Stack( children: [ AdWidget(ad: state.counterPageBottomBannerAd!), RemoveAdButton( onTap: () { adsBloc.add(AdsCounterPageBottomBannerAdDisposed()); }, ), ], ), ); }, ); } }
counter_page_top_banner_ad.dart
import 'package:flutter/material.dart'; import 'package:flutter_ads/ads/ads.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; class CounterPageTopBannerAd extends StatefulWidget { const CounterPageTopBannerAd({super.key}); static const height = 72.0; @override State<CounterPageTopBannerAd> createState() => _CounterPageTopBannerAdState(); } class _CounterPageTopBannerAdState extends State<CounterPageTopBannerAd> { late AdsBloc adsBloc; @override void dispose() { adsBloc.add(AdsCounterPageTopBannerAdDisposed()); super.dispose(); } @override Widget build(BuildContext context) { adsBloc = context.read<AdsBloc>() ..add(AdsCounterPageTopBannerAdRequested()); return BlocBuilder<AdsBloc, AdsState>( buildWhen: (pre, cur) => pre.counterPageTopBannerAd != cur.counterPageTopBannerAd, builder: (context, state) { if (!state.didCounterPageTopBannerAdLoad) { return const SizedBox.shrink(); } return SizedBox( width: double.infinity, height: CounterPageTopBannerAd.height, child: Stack( children: [ AdWidget(ad: state.counterPageTopBannerAd!), RemoveAdButton( onTap: () { adsBloc.add(AdsCounterPageTopBannerAdDisposed()); }, ), ], ), ); }, ); } }
counter_page_native_ad.dart
import 'package:flutter/material.dart'; import 'package:flutter_ads/ads/ads.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; class CounterPageNativeAd extends StatefulWidget { const CounterPageNativeAd({super.key}); @override State<CounterPageNativeAd> createState() => _CounterPageNativeAdState(); } class _CounterPageNativeAdState extends State<CounterPageNativeAd> { late AdsBloc adsBloc; @override void dispose() { adsBloc.add(AdsCounterPageNativeAdDisposed()); super.dispose(); } @override Widget build(BuildContext context) { adsBloc = context.read<AdsBloc>()..add(AdsCounterPageNativeAdRequested()); return BlocBuilder<AdsBloc, AdsState>( buildWhen: (pre, cur) => pre.counterPageNativeAd != cur.counterPageNativeAd, builder: (context, state) { if (!state.didCounterPageNativeAdLoad) return const SizedBox.shrink(); return ConstrainedBox( constraints: const BoxConstraints( minWidth: 320, // minimum recommended width minHeight: 320, // minimum recommended height maxHeight: 320, ), child: Stack( children: [ AdWidget(ad: state.counterPageNativeAd!), RemoveAdButton( alignment: Alignment.topLeft, margin: const EdgeInsets.only(left: 16, top: 16), onTap: () { adsBloc.add(AdsCounterPageNativeAdDisposed()); }, ), ], ), ); }, ); } }
remove_ad_button.dart
import 'package:flutter/material.dart'; class RemoveAdButton extends StatelessWidget { const RemoveAdButton({ required this.onTap, this.margin = const EdgeInsets.only(left: 16), this.alignment = Alignment.centerLeft, super.key, }); final VoidCallback onTap; final EdgeInsets margin; final Alignment alignment; @override Widget build(BuildContext context) { return Align( alignment: alignment, child: Padding( padding: margin, child: InkWell( borderRadius: BorderRadius.circular(60), onTap: onTap, child: Container( width: 32, height: 32, decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), color: Colors.red[400], ), child: const Icon( Icons.clear_rounded, size: 24, color: Colors.white, ), ), ), ), ); } }
Observando os widgets acima, notamos que todos seguem uma implementação bastante semelhante:
- No momento em que o widget é criado, um evento AdsBloc é acionado para solicitar o anúncio correspondente.
- O BlocBuilder aciona a reconstrução do widget com base na alteração do estado do anúncio. Se for nulo, não será exibido, caso contrário, será.
- Um RemoveAddButton é adicionado ao anúncio para permitir que o usuário remova o anúncio sob demanda. Observe que a função onTap acionará o evento de bloco de dispensa de anúncio correspondente.
- Se o widget for descartado por qualquer motivo, um evento de bloco de dispensa de anúncio será acionado para evitar vazamentos de memória e impacto negativo no desempenho.
Na inicialização, nosso aplicativo executará primeiro o código em nosso arquivo main_common.dart personalizado, onde garantimos que WidgetsFlutterBinding seja inicializado e, em seguida, inicializamos MobileAds.instance, AdsClient e AdsRepo. Por fim, injetamos a instância do repo na classe App.
import 'package:ads_client/ads_client.dart'; import 'package:ads_repo/ads_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ads/app/app.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; Future<Widget> mainCommon() async { WidgetsFlutterBinding.ensureInitialized(); await MobileAds.instance.initialize(); final adsClient = AdsClient(); final adsRepo = AdsRepo(adsClient: adsClient); return App(adsRepo: adsRepo); }
Observando o arquivo app.dart, notamos que a única diferença em relação ao aplicativo contador padrão é que agrupamos a página de rota inicial com um RepositoryProvider para que o CounterPage tenha acesso à instância _adsRepo.
import 'package:ads_repo/ads_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ads/counter/counter.dart'; import 'package:flutter_ads/l10n/l10n.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class App extends StatelessWidget { const App({required AdsRepo adsRepo, super.key}) : _adsRepo = adsRepo; final AdsRepo _adsRepo; @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( appBarTheme: const AppBarTheme(color: Color(0xFF13B9FF)), colorScheme: ColorScheme.fromSwatch( accentColor: const Color(0xFF13B9FF), ), ), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: RepositoryProvider( create: (context) => _adsRepo, child: const CounterPage(), ), ); } }
Por fim, vamos analisar a interface do usuário real do nosso novo aplicativo de contador com anúncios do Google.
A IU acima foi criada ajustando o aplicativo Flutter Counter padrão e adicionando os widgets de anúncios que analisamos anteriormente. Confira o código abaixo.
import 'package:ads_repo/ads_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ads/ads/ads.dart'; import 'package:flutter_ads/counter/counter.dart'; import 'package:flutter_ads/l10n/l10n.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CounterPage extends StatelessWidget { const CounterPage({super.key}); @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider( create: (_) => CounterCubit(), ), BlocProvider( create: (context) => AdsBloc(adsRepo: context.read<AdsRepo>()), ), ], child: const CounterView(), ); } } class CounterView extends StatelessWidget { const CounterView({super.key}); @override Widget build(BuildContext context) { final l10n = context.l10n; return Scaffold( appBar: AppBar(title: Text(l10n.counterAppBarTitle)), body: const Stack( children: [ Positioned( top: 0, right: 0, left: 0, child: CounterPageTopBannerAd(), ), Positioned( bottom: 0, right: 0, left: 0, child: CounterPageBottomBannerAd(), ), Center( child: CounterPageNativeAd(), ), Center(child: CounterText()), ], ), floatingActionButton: const Column( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, children: [ PlusCounterButton(), SizedBox(height: 8), MinusCounterButton(), ], ), ); } } class MinusCounterButton extends StatelessWidget { const MinusCounterButton({ super.key, }); @override Widget build(BuildContext context) { final adsBloc = context.read<AdsBloc>(); final count = context.select((CounterCubit cubit) => cubit.state); return BlocListener<AdsBloc, AdsState>( listenWhen: (pre, cur) => pre.counterPageMinusCheckInterstitialAd == null && cur.counterPageMinusCheckInterstitialAd != null, listener: (context, state) { state.counterPageMinusCheckInterstitialAd?.show(); }, child: FloatingActionButton( onPressed: () { context.read<CounterCubit>().decrement(); if (count % 5 == 0 && count < 0) { adsBloc.add( AdsCounterPageMinusCheckInterstitialAdRequested( onAdDismissedFullScreenContent: () { adsBloc.add( AdsCounterPageMinusCheckInterstitialAdDisposed(), ); }, ), ); } }, child: const Icon(Icons.remove), ), ); } } class PlusCounterButton extends StatelessWidget { const PlusCounterButton({ super.key, }); @override Widget build(BuildContext context) { final adsBloc = context.read<AdsBloc>(); final count = context.select((CounterCubit cubit) => cubit.state); return BlocListener<AdsBloc, AdsState>( listenWhen: (pre, cur) => pre.counterPagePlusCheckInterstitialAd == null && cur.counterPagePlusCheckInterstitialAd != null, listener: (context, state) { state.counterPagePlusCheckInterstitialAd?.show(); }, child: FloatingActionButton( onPressed: () { context.read<CounterCubit>().increment(); if (count % 5 == 0 && count > 0) { adsBloc.add( AdsCounterPagePlusCheckInterstitialAdRequested( onAdDismissedFullScreenContent: () { adsBloc.add(AdsCounterPagePlusCheckInterstitialAdDisposed()); }, ), ); } }, child: const Icon(Icons.add), ), ); } } class CounterText extends StatelessWidget { const CounterText({super.key}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final count = context.select((CounterCubit cubit) => cubit.state); return ClipOval( child: Container( constraints: const BoxConstraints( minWidth: 200, minHeight: 200, ), padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(50), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( '$count', style: theme.textTheme.displayLarge?.copyWith( color: Colors.white, ), ), ], ), ), ); } }
Observe como usamos o padrão Page-View para fornecer o bloco logo acima do widget AppView para que ele tenha acesso a ele na árvore do widget. Além disso, modificamos os botões de contagem de mais e menos para que exibam uma adição intersticial assim que o valor do contador for divisível por 5. Observe como a função de callback de dispensa de anúncio nada mais é do que o evento de bloco que aciona a dispensa do anúncio intersticial correspondente. Que ovo de Páscoa chato, hein?
Considerações finais
Vimos como implementar o Google Ads em um aplicativo Flutter de maneira consistente, sistemática e intuitiva, contando com as melhores práticas e padrões para arquiteturas em camadas, injeção de dependência e gerenciamento de estado com bloco. Quero enfatizar que o número de anúncios e como/quando/onde eles serão exibidos ao usuário depende do desenvolvedor. No entanto, vale a pena observar que, embora o Google Ads possa ser uma poderosa ferramenta de monetização, é importante encontrar um equilíbrio entre gerar receita e proporcionar uma boa experiência ao usuário. Certifique-se de que os anúncios não sejam intrusivos ou perturbem a funcionalidade e o conteúdo do seu aplicativo, pois manter uma experiência positiva do usuário é crucial para o sucesso a longo prazo.
Não deixe de conhecer meus Ebooks de Flutter/Dart