Flutter - Google Mobile Ads - 구글 애드몹을 활용하여 앱에 추가하고 수익 창출하기(Android/iOS)

 

Overview

앱 내의 광고를 탑재하여 수익을 창출 할 수 있습니다.

이번 포스팅에서는 구글 애드몹 패키지를 활용하여 플러터에 광고를 탑재하고 표시하는 것을 다룹니다.

  • 앱 시작시 전면 광고 출력
  • 앱 상단의 배너 광고 표시
  • 앱 바 우측 상단의 달러 모양 '$' 클릭 시 보상형 광고 표시 및 보상 지급 안내 문구 출력


포스팅에서 진행한 환경

  • OS : Windows 10
  • IDE : Visual Studio Code
  • Emulator : Pixel 3a API 33 (Android-x86 emulator)

 

포스팅에서의 예시 프로젝트 다운로드

  • 포스팅에서 다루는 예시 프로젝트는 아래의 깃허브 링크에서 다운로드 받을 수 있습니다.
  • '8_add_google_mobile_ads' 폴더를 확인해주세요.
  • https://github.com/luvris2/flutter_memo_app

 

다른 설명이 필요하면 아래의 링크를 참고해주세요.


구글 애드몹 (Google AdMob)

  • 인앱 광고를 비롯하여 앱 비지니스 성장을 위한 강력하고 손쉬운 제품
  • 앱 개발자가 광고를 표시하여 모바일 앱에서 수익을 창출할 수 있는 모바일 광고 플랫폼
  • 다양한 유형의 광고를 통해 수익 제공
    • 배너 광고 : 기기 화면의 상단이나 하단에 표시되는 직사각형 광고입니다. 배너 광고는 사용자가 앱과 상호작용하는 동안 화면에 표시되며 일정 시간이 지나면 자동으로 새로고침될 수 있습니다. 모바일 광고를 처음 시작하는 경우 이 형식부터 이용해 보시기 바랍니다.
    • 전면 광고 : 사용자가 닫을 때까지 앱의 인터페이스를 완전히 덮는 전체 화면 광고입니다. 게임 레벨이 바뀌는 사이 또는 작업 완료 직후와 같이 앱 이용이 잠시 중단될 때 자연스럽게 광고가 게재되는 것이 가장 좋습니다.
    • 보상형 광고 : 짧은 동영상을 시청하거나 플레이어블 광고 및 설문조사와 상호작용한 사용자에게 보상을 제공하는 광고 무료 게임 사용자로부터 수익을 창출하는 데 효과적입니다.
    • 네이티브 광고 : 앱의 디자인과 분위기에 어울리게 맞춤설정할 수 있는 광고입니다. 광고 배치 방법 및 위치를 정할 수 있으므로 광고 레이아웃과 앱 디자인의 일관성 유지가 가능합니다.

구글 애드몹 시작하기 / 앱 추가하기

 

Google AdMob: 모바일 앱 수익 창출

인앱 광고를 사용하여 모바일 앱에서 더 많은 수익을 창출하고, 사용이 간편한 도구를 통해 유용한 분석 정보를 얻고 앱을 성장시켜 보세요.

admob.google.com

 

  • '시작하기' 버튼 클릭

 

  • 본인 인증 및 로그인

 

  • 이후 초기 시작 관련 페이지를 넘어가고 나면 아래와 같은 홈페이지로 이동
    • 저 같은 경우는 이미 블로그 애드센스가 있기 때문에 따로 화면 첨부를 하지 못하였습니다.
  • 구글 애드몹 페이지의 홈 메뉴에서 '시작하기' 버튼 클릭

 

  • 플랫폼 선택 : Android / iOS
    • 광고를 탑재할 앱이 지원하는 플랫폼 선택
  • 앱 스토어 등록 여부 선택
    • 앱이 스토어에 등록되어 있을 경우 예, 아닐 경우 아니오
  • '계속' 버튼 클릭

 

  • 앱 이름 지정
    • 구분 할 이름 설정
  • 사용자 측정항목 선택
    • 설정 : Firebse에 연결하여 구글 애널리틱스 계정 데이터를 사용할 수 있음, 제품 기능이 향상되고 앱 수익이 늘어 날 수 있음
    • 설정 해제 : 구글 애널리틱스 데이터 사용하지 않음
  • '앱 추가' 버튼 클릭

 

  • '완료' 버튼 클릭

 

  • '앱' 메뉴에서 추가한 광고 앱이 생성되었는지 확인
  • 카테고리의 '앱 설정' 클릭 - 앱 ID 확인


플러터에서의 애드몹 설정

구글 애드 몹 기본 요건


프로젝트에 애드몹 패키지 추가

  • 터미널에서 아래의 코드 입력
flutter pub add google_mobile_ads

Android/iOS 기기별 설정

기기에 따라 아래의 설정을 진행해주세요.

 

안드로이드 Manifest.xml 설정

  • (프로젝트 디렉토리 내) android - app - src - main - AndroidManifest.xml 파일 열기

 

  • <application> 하위에 <meta-data> 코드 추가
    • <meta-data> 태그를 복사하여 Manifest.xml에 추가
    • 구글 애드몹 페이지에서 광고 앱을 추가한 앱 ID 입력
<manifest>
    <application>
        <!-- Sample AdMob app ID: ca-app-pub-3940256099942544~3347511713 -->
        <meta-data
            android:name="com.google.android.gms.ads.APPLICATION_ID"
            android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy"/>
    <application>
<manifest>


iOS Info.plist 설정

  • (프로젝프 디렉토리 내) ios - Runner - Info.plist 파일 열기

 

  • 키 추가
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-################~##########</string>


코딩 시 애드몹 참고사항

테스트 광고 (데모 광고) ID

* 통합 앱 ID가 생겨서 실제로 발급 받을 때는 디바이스에 따라 ID 부여를 하지 않아도 됩니다.

  • Android
광고 형식 데모 광고 단위 ID
App Open ca-app-pub-3940256099942544/3419835294
Banner ca-app-pub-3940256099942544/6300978111
Interstitial ca-app-pub-3940256099942544/1033173712
Interstitial Video ca-app-pub-3940256099942544/8691691433
Rewarded ca-app-pub-3940256099942544/5224354917
Rewarded Interstitial ca-app-pub-3940256099942544/5354046379
Native Advanced ca-app-pub-3940256099942544/2247696110
Native Advanced Video ca-app-pub-3940256099942544/1044960115

 

  • iOS
광고 형식 데모 광고 단위 ID
앱 오프닝 광고 ca-app-pub-3940256099942544/5662855259
배너 ca-app-pub-3940256099942544/2934735716
전면 광고 ca-app-pub-3940256099942544/4411468910
동영상 전면 광고 ca-app-pub-3940256099942544/5135589807
보상형 광고 ca-app-pub-3940256099942544/1712485313
보상형 전면 광고 ca-app-pub-3940256099942544/6978759866
네이티브 광고 고급형 ca-app-pub-3940256099942544/3986624511
네이티브 광고 고급형 동영상 ca-app-pub-3940256099942544/2521693316

배너 광고 크기

크기(폭x높이) 설명 AdSize 상수
320x50 표준 배너 banner
320x100 대형 배너 largeBanner
320x250 중간 직사각형 mediumRectangle
468x60 전체 크기 배너 fullBanner
728x90 리더보드 leaderboard
화면 폭x32|50|90 스마트 배너 getSmartBanner(Orientation) 사용

광고 ID 만들기

  • (구글 애드몹 홈페이지의 메뉴에서) 앱 - 자신의 앱 클릭

 

  • 선택한 앱의 '광고 단위' 카테고리 클릭

 

  • '광고 단위 추가' 버튼 클릭

 

  • 지급 받을 광고 ID 종류 선택

 

  • 광고 단위 이름과 설정을 지정
  • '광고 단위 만들기' 버튼 클릭

 

  • 이 후 목록에서 광고 단위를 누르면 자신의 광고 ID를 확인할 수 있음


코딩

모바일 광고 인스턴스 초기화

  • 광고를 로드하기 전에 앱에서 모바일 광고 SDK를 초기화해야합니다.
    • 초기화가 되거나 제한 시간인 30초 후에 Future 객체가 반환됩니다.
    • 초기화 작업은 앱을 실행하기 직전에 한 번만 하는 것이 좋습니다.
  • google mobile ads 패키지를 임포트하고 메인 함수에 아래의 코드 추가합니다.
    • 패키지 임포트는 코드를 추가한 후, 퀵 픽스를 통해 쉽게 할 수 있습니다.
// ignore_for_file: avoid_print

import 'package:flutter/material.dart';
import 'package:flutter_memo_app/loginPage/loginMainPage.dart';
import 'package:flutter_memo_app/memoPage/memoListProvider.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:provider/provider.dart';

void main() {
  // 플러터 바인딩 초기화 및 바인딩 설정 체크
  WidgetsFlutterBinding.ensureInitialized();
  // 모바일 광고 SDK 초기화
  MobileAds.instance.initialize();
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => MemoUpdator()),
      ],
      child: const MyApp(),
    ),
  );
}

애드몹 광고 클래스 정의

1) 광고를 보여주는 로직을 메인 코드 내에 작성하여 할 경우, 코드가 매우 복잡해지기 때문에 클래스를 생성하여 호출하는 형식으로 진행합니다.

2) 이 포스팅에서는 static 키워드를 사용하여 싱글톤으로 구현하였으나 여러가지 광고 보상의 다양성을 구현하려면 static키워드를 빼고 코드를 구현하시길 바랍니다.

 

애드몹 광고 클래스를 정의할 파일을 생성합니다.

  • 파일명 : googleAdMob.dart

 

  • 코드 내에 필요한 파일들을 호출(import)합니다.
import 'package:flutter/material.dart';

// 비동기 작업의 완료 상태 확인을 위함
import 'dart:async';

// 구글 애드몹 사용을 위함
import 'package:google_mobile_ads/google_mobile_ads.dart';

 

  • 클래스를 정의 합니다.
    • 포스팅에서는 GoogleAdMob 이라고 정의하였습니다.
class GoogleAdMob {
	// 이후 광고 코드는 다 여기에 입력
}

배너 광고

배너 광고 인스턴스 생성

  • 배너 광고는 앱 실행시 딱 한번만 로드해도 되기 때문에 인스턴스 생성 함수를 정의합니다.
  • 이 함수는 플러터 앱이 실행 될 때 호출합니다.
  // 배너 광고 인스턴스를 생성하고 로딩하기 위한 함수
  static BannerAd loadBannerAd() {
    // 배너 광고
    BannerAd myBanner = BannerAd(
      adUnitId: 'ca-app-pub-9914634594152112/4988082034', // 내 리워드 광고 통합 APP ID
      /*
      크기(폭x높이)        설명               AdSize 상수
      -----------------------------------------------------------------------
      320x50	            표준 배너	          banner
      320x100	            대형 배너	          largeBanner
      320x250	            중간 직사각형	      mediumRectangle
      468x60	            전체 크기 배너	    fullBanner
      728x90	            리더보드	          leaderboard
      화면 폭x32|50|90	  스마트 배너	        getSmartBanner(Orientation) 사용
      */
      size: AdSize.banner,
      request: AdRequest(),
      listener: BannerAdListener(
        // 광고가 성공적으로 수신된 경우
        onAdLoaded: (Ad ad) => print('Ad loaded.'),
        // 광고 요청이 실패한 경우
        onAdFailedToLoad: (Ad ad, LoadAdError error) {
          // 광고 요청 오류 시 광고를 삭제하여 리소스 확보
          ad.dispose();
          print('Ad failed to load: $error');
        },
        // 광고가 화면을 덮는 오버레이를 열었을 때 호출
        // 사용자가 광고를 클릭하거나 특정 조건이 충족되어 광고가 표시 될 때 발생
        onAdOpened: (Ad ad) => print('Ad opened.'),
        // 광고가 화면을 덮는 오버레이를 닫았을 때 호출
        // 사용자가 광고를 닫거나 자동으로 닫힐 때 발생
        onAdClosed: (Ad ad) => print('Ad closed.'),
        // 광고가 노출 될 때 호출
        // 광고가 사용자에게 보여질 때 발생
        onAdImpression: (Ad ad) => print('Ad impression.'),
      ),
    );
    return myBanner;
  }

배너 광고 노출하기

  • 플러터 앱이 실행될 때 호출된 배너 광고의 인스턴스를 매개 변수로 입력합니다.
  • 배너 광고 노출을 위해 UI 디자인을 합니다. 반환 값은 컨테이너 위젯으로 호출 시 바로 노출되게끔 설계합니다.
    • 물론, 환경에 따라 직접 UI 디자인을 하셔도 됩니다.
  // 배너 광고를 화면에 보여주기 위한 함수, 파라미터로 로드된 배너 인스턴스 필요
  static Container showBannerAd(BannerAd myBanner) {
    // 광고 디스플레이
    // 배너 광고를 위젯으로 표시하기 위해 지원되는 광고를 사용하여 AdWidget 인스턴스화
    final Container adContainer = Container(
      alignment: Alignment.center,
      width: myBanner.size.width.toDouble(),
      height: myBanner.size.height.toDouble(),
      child: AdWidget(ad: myBanner),
    );

    return adContainer;
  }

전면 광고 노출하기

  • 전면 광고는 1회성이기 때문에 호출 시 인스턴스를 생성하고 바로 광고를 보여주도록 합니다.
  • 반환 값은 없으며 호출 즉시 광고가 노출됩니다.
  // 전면 광고를 인스턴스를 생성하고 로딩하기 위한 함수
  static void showInterstitialAd() {
    // 전면 광고 로드
    InterstitialAd.load(
      adUnitId: 'ca-app-pub-9914634594152112/3639341340', // 내 리워드 광고 통합 APP ID
      request: AdRequest(),
      adLoadCallback: InterstitialAdLoadCallback(
        // 전면 광고 로드 완료
        onAdLoaded: (InterstitialAd ad) {
          // 광고 보여주기
          ad.show();
          // 리소스 해제
          ad.dispose();
        },
        // 전면 광고 로드 실패
        onAdFailedToLoad: (LoadAdError error) {
          print('InterstitialAd failed to load: $error');
        },
      ),
    );
  }

보상형 광고 노출하기

블로그를 작성하면서 가장 많이 애를 먹은 부분입니다... 리워드 관련으로 반환값 때문에...

  • 보상 지급하는 방법
    • 구글 애드몹 홈페이지에서 해당 보상형 광고 단위 설정 페이지를 들어갑니다.
    • 리워드 설정 부분의 '리워드 수량' 과 '리워드 상품'을 입력합니다.
    • 플러터 코드 설계 시, 지급되는 reward 변수의 속성명은 amount(리워드 수량), type(리워드 상품)으로 구분됩니다.

  • 보상 확인 방법
    • Completer 클래스를 이용하여 비동기 작업의 상태를 확인합니다.
    • 비동기 작업이 끝나면 작업의 값을 반환합니다.
    • 반환된 값을 토대로 보상을 지급합니다.
    • reward의 amount, type은 애드몹 홈페이지에서 설정한 값을 반환합니다.
      • 리워드 수량 : 1 / 리워드 상품 : Reward
  // 보상형 광고 보여주기 위한 함수, bool 반환값을 통해 보상이 지급되었는지 확인
  static Future<bool> showRewardedAd() async {
    print('******************showRewardedAD******************');

    // 비동기 작업을 확인하기 위한 bool 타입의 Completer
    Completer<bool> completer = Completer<bool>();
    // 보상 지급 확인 변수
    bool isRewarded = false;

    await RewardedAd.load(
      adUnitId: 'ca-app-pub-9914634594152112/1617126316', // 내 리워드 광고 통합 APP ID
      request: AdRequest(),
      rewardedAdLoadCallback: RewardedAdLoadCallback(
        // 보상 광고 로드 완료
        onAdLoaded: (rewardedAd) async {
          rewardedAd.fullScreenContentCallback = FullScreenContentCallback(
            // 광고가 표시 될 때 호출
            onAdShowedFullScreenContent: (ad) =>
                print('$ad onAdShowedFullScreenContent.'),
            // 광고가 닫힐 때 호출
            onAdDismissedFullScreenContent: (ad) {
              print('$ad onAdDismissedFullScreenContent.');
              // 광고 리소스 해제
              ad.dispose();
              completer.complete(isRewarded);
            },
            // 광고가 표시되지 못했을 때의 호출, 오류 정보 제공
            onAdFailedToShowFullScreenContent: (ad, AdError error) {
              print('$ad onAdFailedToShowFullScreenContent: $error');
              ad.dispose();
              completer.complete(isRewarded);
            },
            // 광고가 노출되었을 때 호출
            onAdImpression: (ad) => print('$ad impression occurred.'),
          );
          // 광고 보여주기
          await rewardedAd.show(
            onUserEarnedReward: (ad, reward) {
              // 광고 보상 지급 코드
              print(
                  'Reward ad : $ad    reward : $reward,   type : ${reward.type},    amount : ${reward.amount}');
              // 보상 지급 완료 처리
              isRewarded = true;
            },
          );
        },
        // 보상 광고 로드 실패
        onAdFailedToLoad: (LoadAdError error) {
          print('RewardedAd failed to load: $error');
          completer.complete(isRewarded);
        },
      ),
    );
    return completer.future;
  }

메인 페이지에 광고 기능 붙이기

기능 설계

  • 배너 광고
    • 메모 페이지 호출 시 배너 광고 표시
    • 배너 광고의 위치는 앱의 가장 상단에 표시
      • 단, 앱 바 영역을 침범해서는 안된다. (앱 바 바로 밑에 위치하도록 설계)
  • 전면 광고
    • 메모 페이지 호출 시 전면 광고 표시
    • 전면 광고는 앱 실행 시 단 한 번만 노출되도록 한다.
  • 보상형 광고
    • 앱의 앱 바 우측 상단에 표시
    • 아이콘은 달러 모양으로하여 직관적으로 보상형 광고임을 암시하도록 한다.
    • 아이콘 클릭 시 보상형 광고가 노출되며, 광고를 모두 볼 경우 보상을 지급하는 로직을 작성한다.

코드

  • 앱 실행 시 배너 광고, 전면 광고 노출하기
    • 페이지가 표시 될 때 initState에 코드를 작성하여 광고를 바로 보여줄 수 있도록 함
/* memoMainPage.dart */
// 애드몹 배너 객체 생성
  late BannerAd myBanner;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    
    // 메모 리스트 출력
    getMemoList();
    
    // 배너 광고 로드
    myBanner = GoogleAdMob.loadBannerAd();
    myBanner.load();

    // 전면 광고 로드
    GoogleAdMob.showInterstitialAd();
  }

 

  • 앱 바의 보상형 광고 버튼
    • 보상형 광고 버튼을 추가하고 광고의 보상을 지급함
    • 광고 보상에 대한 로직은 지급하는 상품, 갯수, 방식이 다르므로 각자 구현하세요!
/* header&footer.dart */

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: CupertinoNavigationBar(
        middle: const Text('Title'),
        trailing: Row(
          mainAxisSize: MainAxisSize.min,
          children: [

            // 보상형 광고 보기 버튼
            CupertinoButton(
              padding: EdgeInsets.zero,
              child: Icon(
                CupertinoIcons.money_dollar_circle,
                size: 30,
              ),
              onPressed: () async {
                // 보상형 광고 보여주기
                var rewardedAds = await GoogleAdMob.showRewardedAd();

                // 광고 보상 지급 완료 메시지
                if (rewardedAds) {
                  showDialog(
                    context: context,
                    builder: (BuildContext context) {
                      return AlertDialog(
                        content: Text('보상이 지급되었습니다.'),
                        actions: [
                          TextButton(
                            onPressed: () {
                              Navigator.of(context).pop(); // 다이얼로그 닫기,
                            },
                            child: Text('닫기'),
                          ),
                        ],
                      );
                    },
                  );
                }
              },
            ),

			// 코드 생략

전체 소스 코드

  • googleAdMob.dart
    • 광고 정의 클래스
더보기
import 'package:flutter/material.dart';
// 비동기 작업의 완료 상태 확인을 위함
import 'dart:async';
// 구글 애드몹 사용을 위함
import 'package:google_mobile_ads/google_mobile_ads.dart';

class GoogleAdMob {
  // 배너 광고 인스턴스를 생성하고 로딩하기 위한 함수
  static BannerAd loadBannerAd() {
    // 배너 광고
    BannerAd myBanner = BannerAd(
      adUnitId: 'ca-app-pub-9914634594152112/4988082034', // 내 리워드 광고 통합 APP ID
      /*
      크기(폭x높이)        설명               AdSize 상수
      -----------------------------------------------------------------------
      320x50	            표준 배너	          banner
      320x100	            대형 배너	          largeBanner
      320x250	            중간 직사각형	      mediumRectangle
      468x60	            전체 크기 배너	    fullBanner
      728x90	            리더보드	          leaderboard
      화면 폭x32|50|90	  스마트 배너	        getSmartBanner(Orientation) 사용
      */
      size: AdSize.banner,
      request: AdRequest(),
      listener: BannerAdListener(
        // 광고가 성공적으로 수신된 경우
        onAdLoaded: (Ad ad) => print('Ad loaded.'),
        // 광고 요청이 실패한 경우
        onAdFailedToLoad: (Ad ad, LoadAdError error) {
          // 광고 요청 오류 시 광고를 삭제하여 리소스 확보
          ad.dispose();
          print('Ad failed to load: $error');
        },
        // 광고가 화면을 덮는 오버레이를 열었을 때 호출
        // 사용자가 광고를 클릭하거나 특정 조건이 충족되어 광고가 표시 될 때 발생
        onAdOpened: (Ad ad) => print('Ad opened.'),
        // 광고가 화면을 덮는 오버레이를 닫았을 때 호출
        // 사용자가 광고를 닫거나 자동으로 닫힐 때 발생
        onAdClosed: (Ad ad) => print('Ad closed.'),
        // 광고가 노출 될 때 호출
        // 광고가 사용자에게 보여질 때 발생
        onAdImpression: (Ad ad) => print('Ad impression.'),
      ),
    );
    return myBanner;
  }

  // 배너 광고를 화면에 보여주기 위한 함수, 파라미터로 로드된 배너 인스턴스 필요
  static Container showBannerAd(BannerAd myBanner) {
    // 광고 디스플레이
    // 배너 광고를 위젯으로 표시하기 위해 지원되는 광고를 사용하여 AdWidget 인스턴스화
    final Container adContainer = Container(
      alignment: Alignment.center,
      width: myBanner.size.width.toDouble(),
      height: myBanner.size.height.toDouble(),
      child: AdWidget(ad: myBanner),
    );

    return adContainer;
  }

  // 전면 광고를 인스턴스를 생성하고 로딩하기 위한 함수
  static void showInterstitialAd() {
    // 전면 광고 로드
    InterstitialAd.load(
      adUnitId: 'ca-app-pub-9914634594152112/3639341340', // 내 리워드 광고 통합 APP ID
      request: AdRequest(),
      adLoadCallback: InterstitialAdLoadCallback(
        // 전면 광고 로드 완료
        onAdLoaded: (InterstitialAd ad) {
          // 광고 보여주기
          ad.show();
          // 리소스 해제
          ad.dispose();
        },
        // 전면 광고 로드 실패
        onAdFailedToLoad: (LoadAdError error) {
          print('InterstitialAd failed to load: $error');
        },
      ),
    );
  }

  // 보상형 광고 보여주기 위한 함수, bool 반환값을 통해 보상이 지급되었는지 확인
  static Future<bool> showRewardedAd() async {
    print('******************showRewardedAD******************');

    // 비동기 작업을 확인하기 위한 bool 타입의 Completer
    Completer<bool> completer = Completer<bool>();
    // 보상 지급 확인 변수
    bool isRewarded = false;

    await RewardedAd.load(
      adUnitId: 'ca-app-pub-9914634594152112/1617126316', // 내 리워드 광고 통합 APP ID
      request: AdRequest(),
      rewardedAdLoadCallback: RewardedAdLoadCallback(
        // 보상 광고 로드 완료
        onAdLoaded: (rewardedAd) async {
          rewardedAd.fullScreenContentCallback = FullScreenContentCallback(
            // 광고가 표시 될 때 호출
            onAdShowedFullScreenContent: (ad) =>
                print('$ad onAdShowedFullScreenContent.'),
            // 광고가 닫힐 때 호출
            onAdDismissedFullScreenContent: (ad) {
              print('$ad onAdDismissedFullScreenContent.');
              // 광고 리소스 해제
              ad.dispose();
              completer.complete(isRewarded);
            },
            // 광고가 표시되지 못했을 때의 호출, 오류 정보 제공
            onAdFailedToShowFullScreenContent: (ad, AdError error) {
              print('$ad onAdFailedToShowFullScreenContent: $error');
              ad.dispose();
              completer.complete(isRewarded);
            },
            // 광고가 노출되었을 때 호출
            onAdImpression: (ad) => print('$ad impression occurred.'),
          );
          // 광고 보여주기
          await rewardedAd.show(
            onUserEarnedReward: (ad, reward) {
              // 광고 보상 지급 코드
              print(
                  'Reward ad : $ad    reward : $reward,   type : ${reward.type},    amount : ${reward.amount}');
              // 보상 지급 완료 처리
              isRewarded = true;
            },
          );
        },
        // 보상 광고 로드 실패
        onAdFailedToLoad: (LoadAdError error) {
          print('RewardedAd failed to load: $error');
          completer.complete(isRewarded);
        },
      ),
    );
    return completer.future;
  }
}

 

  • memoMainPage.dart
    • 배너 광고, 전면 광고 표시 페이지
더보기
// 메모 페이지
// 앱의 상태를 변경해야하므로 StatefulWidget 상속
// ignore_for_file: avoid_print, use_build_context_synchronously

import 'package:flutter/material.dart';
import 'package:flutter_memo_app/adMob/googleAdMob.dart';
import 'package:flutter_memo_app/memoPage/memoDB.dart';
import 'package:flutter_memo_app/memoPage/memoListProvider.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:provider/provider.dart';

import 'memoDetailPage.dart';

class MyMemoPage extends StatefulWidget {
  const MyMemoPage({super.key});

  @override
  MyMemoState createState() => MyMemoState();
}

class MyMemoState extends State<MyMemoPage> {
  // 검색어
  String searchText = '';

  // 플로팅 액션 버튼을 이용하여 항목을 추가할 제목과 내용
  final TextEditingController titleController = TextEditingController();
  final TextEditingController contentController = TextEditingController();

  // 메모 리스트 저장 변수
  List items = [];

  // 메모 리스트 출력
  Future<void> getMemoList() async {
    List memoList = [];
    // DB에서 메모 정보 호출
    var result = await selectMemoALL();

    print(result?.numOfRows);

    // 메모 리스트 저장
    for (final row in result!.rows) {
      var memoInfo = {
        'id': row.colByName('id'),
        'userIndex': row.colByName('userIndex'),
        'userName': row.colByName('userName'),
        'memoTitle': row.colByName('memoTitle'),
        'memoContent': row.colByName('memoContent'),
        'createDate': row.colByName('createDate'),
        'updateDate': row.colByName('updateDate')
      };
      memoList.add(memoInfo);
    }

    print('MemoMainPage - getMemoList : $memoList');
    context.read<MemoUpdator>().updateList(memoList);
  }

  // 애드몹 배너 객체 생성
  late BannerAd myBanner;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    // 메모 리스트 출력
    getMemoList();

    // 배너 광고 로드
    myBanner = GoogleAdMob.loadBannerAd();
    myBanner.load();

    // 전면 광고 로드
    GoogleAdMob.showInterstitialAd();

    // 보상 광고 로드
    //GoogleAdMob.showRewardedAd();
  }

  // 리스트뷰 카드 클릭 이벤트
  void cardClickEvent(BuildContext context, int index) async {
    dynamic content = items[index];
    print('content : $content');
    // 메모 리스트 업데이트 확인 변수 (false : 업데이트 되지 않음, true : 업데이트 됨)
    var isMemoUpdate = await Navigator.push(
      context,
      MaterialPageRoute(
        // 정의한 ContentPage의 폼 호출
        builder: (context) => ContentPage(content: content),
      ),
    );

    // 메모 수정이 일어날 경우, 메모 메인 페이지의 리스트 새로고침
    if (isMemoUpdate != null) {
      setState(() {
        getMemoList();
        items = Provider.of<MemoUpdator>(context, listen: false).memoList;
      });
    }
  }

  // 플로팅 액션 버튼 클릭 이벤트
  Future<void> addItemEvent(BuildContext context) {
    // 다이얼로그 폼 열기
    return showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('메모 추가'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              TextField(
                controller: titleController,
                decoration: InputDecoration(
                  labelText: '제목',
                ),
              ),
              TextField(
                controller: contentController,
                maxLines: null, // 다중 라인 허용
                decoration: InputDecoration(
                  labelText: '내용',
                ),
              ),
            ],
          ),
          actions: <Widget>[
            TextButton(
              child: Text('취소'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
            TextButton(
              child: Text('추가'),
              onPressed: () async {
                String title = titleController.text;
                String content = contentController.text;
                // 메모 추가
                await addMemo(title, content);

                setState(() {
                  // 메모 리스트 새로고침
                  print("MemoMainPage - addMemo/setState");
                  getMemoList();
                });
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          // 상단에 배너 광고 표시
          GoogleAdMob.showBannerAd(myBanner),
          Padding(
            padding: const EdgeInsets.all(20.0),
            child: TextField(
              decoration: InputDecoration(
                hintText: '검색어를 입력해주세요.',
                border: OutlineInputBorder(),
              ),
              onChanged: (value) {
                setState(() {
                  searchText = value;
                });
              },
            ),
          ),
          Expanded(
            child: Builder(
              builder: (context) {
                // 메모 수정이 일어날 경우 메모 리스트 새로고침
                items = context.watch<MemoUpdator>().memoList;

                // 메모가 없을 경우의 페이지
                if (items.isEmpty) {
                  return Center(
                    child: Text(
                      "표시할 메모가 없습니다.",
                      style: TextStyle(fontSize: 20),
                    ),
                  );
                }
                // 메모가 있을 경우의 페이지
                else {
                  // items 변수에 저장되어 있는 모든 값 출력
                  return ListView.builder(
                    itemCount: items.length,
                    itemBuilder: (BuildContext context, int index) {
                      // 메모 정보 저장
                      dynamic memoInfo = items[index];
                      String userName = memoInfo['userName'];
                      String memoTitle = memoInfo['memoTitle'];
                      String memoContent = memoInfo['memoContent'];
                      String createDate = memoInfo['createDate'];
                      String updateDate = memoInfo['updateDate'];

                      // 검색 기능, 검색어가 있을 경우, 제목으로만 검색
                      if (searchText.isNotEmpty &&
                          !items[index]['memoTitle']
                              .toLowerCase()
                              .contains(searchText.toLowerCase())) {
                        return SizedBox.shrink();
                      }
                      // 검색어가 없을 경우
                      // 혹은 모든 항목 표시
                      else {
                        return Card(
                          elevation: 3,
                          shape: RoundedRectangleBorder(
                              borderRadius:
                                  BorderRadius.all(Radius.elliptical(20, 20))),
                          child: ListTile(
                            leading: Text(userName),
                            title: Text(memoTitle),
                            subtitle: Text(memoContent),
                            trailing: Text(updateDate),
                            onTap: () => cardClickEvent(context, index),
                          ),
                        );
                      }
                    },
                  );
                }
              },
            ),
          ),
        ],
      ),
      // 플로팅 액션 버튼
      floatingActionButton: FloatingActionButton(
        onPressed: () => addItemEvent(context), // 버튼을 누를 경우 메모 추가 UI 표시
        tooltip: 'Add Item', // 플로팅 액션 버튼 설명
        child: Icon(Icons.add), // + 모양 아이콘
      ),
    );
  }
}

 

  • header&footer.dart
    • 상 단의 앱 바에 보상 광고 보기 버튼 페이지
더보기
// 기본 홈
// ignore_for_file: use_build_context_synchronously

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_memo_app/adMob/googleAdMob.dart';
import 'package:flutter_memo_app/loginPage/loginMainPage.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'communityPage/communityMainPage.dart';
import 'memoPage/memoMainPage.dart';
import 'myInfoPage/myInfoMainPage.dart';

class MyAppPage extends StatefulWidget {
  const MyAppPage({super.key});

  @override
  State<MyAppPage> createState() => MyAppState();
}

class MyAppState extends State<MyAppPage> {
  // 바텀 내비게이션 바 인덱스
  int _selectedIndex = 0;

  // 바텀 내비게이션 바의 연결될 페이지를 인덱스 순으로 나열
  final List<Widget> _navIndex = [
    MyMemoPage(),
    CommunityPage(),
    MyInfoPage(),
  ];

  // 바텀 내비게이션 바가 클릭되었을 때 인덱스 상태 변경
  void _onNavTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: CupertinoNavigationBar(
        middle: const Text('Title'),
        trailing: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            // 보상형 광고 보기 버튼
            CupertinoButton(
              padding: EdgeInsets.zero,
              child: Icon(
                CupertinoIcons.money_dollar_circle,
                size: 30,
              ),
              onPressed: () async {
                // 보상형 광고 보여주기
                var rewardedAds = await GoogleAdMob.showRewardedAd();

                // 광고 보상 지급 완료 메시지
                if (rewardedAds) {
                  showDialog(
                    context: context,
                    builder: (BuildContext context) {
                      return AlertDialog(
                        content: Text('보상이 지급되었습니다.'),
                        actions: [
                          TextButton(
                            onPressed: () {
                              Navigator.of(context).pop(); // 다이얼로그 닫기,
                            },
                            child: Text('닫기'),
                          ),
                        ],
                      );
                    },
                  );
                }
              },
            ),

            // 로그아웃 기능
            CupertinoButton(
              padding: EdgeInsets.zero,
              child: const Icon(
                CupertinoIcons.arrowshape_turn_up_left,
                size: 30,
              ),
              onPressed: () {
                showCupertinoModalPopup<void>(
                  context: context,
                  builder: (BuildContext context) => CupertinoAlertDialog(
                    title: const Text('알림'),
                    content: const Text('로그아웃하시겠습니까?'),
                    actions: <CupertinoDialogAction>[
                      CupertinoDialogAction(
                        isDefaultAction: true,
                        onPressed: () => Navigator.pop(context),
                        child: const Text('아니오'),
                      ),
                      CupertinoDialogAction(
                        isDestructiveAction: true,
                        onPressed: () async {
                          SharedPreferences prefs =
                              await SharedPreferences.getInstance();
                          await prefs.remove('token');
                          Navigator.push(
                            context,
                            MaterialPageRoute(
                              builder: (builder) => LoginMainPage(),
                            ),
                          );
                        },
                        child: Text('예'),
                      ),
                    ],
                  ),
                );
              },
            ),
          ],
        ),
      ),
      // 본문은 바텀 내비게이션 바의 인덱스에 따라 페이지 전환
      body: _navIndex.elementAt(_selectedIndex),
      // 바텀 내비게이션 바
      bottomNavigationBar: BottomNavigationBar(
        fixedColor: Colors.blue,
        unselectedItemColor: Colors.blueGrey,
        showUnselectedLabels: true,
        type: BottomNavigationBarType.fixed,
        items: const [
          // BottomNavigationBarItem(
          //   icon: Icon(Icons.home_filled),
          //   label: '홈',
          //   backgroundColor: Colors.white,
          // ),
          BottomNavigationBarItem(
            icon: Icon(Icons.my_library_books_outlined),
            label: '메모',
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.chat_bubble_2),
            label: '커뮤니티',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person_outline),
            label: '내 정보',
          ),
        ],
        currentIndex: _selectedIndex,
        onTap: _onNavTapped,
      ),
    );
  }
}

참고