ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Flutter | 간단 메모 앱 만들어보고 광고까지 달아서 배포하기 - 6. 메모 앱에 앱 광고 애드 몹 (Ad Mob) 달아보기
    개발/Flutter 2023. 2. 16. 20:07
    반응형

    이제 메모 앱은 다 만들었습니다. 하지만 이 앱으로 광고를 달아서 돈을 벌 수 있도록 만들어 보겠습니다. 하지만 광고를 게재할때 가이드라인은 꼭 지켜주셔야 합니다. 또한 광고들에는 어떤 형태의 광고들이 있는지 확인해 보시고 내 앱에 맞는 광고를 선택하시면 보기에도 좋고 나에게 이득이 될 수 있습니다. 우선 애드몹 계정을 만들어 주시면 됩니다. 애드몹 계정을 만드시면 구글 애드센스 구글 애즈의 계정이 함께 생성됩니다. 애드센스는 웹페이지에 광고를 게재할때 사용합니다. admob.com 으로 이동하신 후 가입을 하시면 됩니다. 국가는 대한민국으로 해주시면 됩니다. 시간대는 서울을 선택해 주시고 결제 통화는 특별한 경우가 아니면 원 을 선택해 주시면 됩니다. 이제 광고 단위를 만들어 볼텐데 광고 단위를 만들려면 광고 단위가 속할 앱을 먼저 추가해줘야 합니다. 안드로이드 단위로 한번 해보겠습니다. 우선 앱 메뉴를 선택해 앱 추가 버튼을 눌러 줍니다. 플랫폼은 안드로이드를 선택해 주고 아직 앱이 배포 되지 않았으므로 아니오를 선택해 줍니다.

    그리고 앱이름을 넣어 주신뒤 사용자 측정항목을 설정으로 해주겠습니다. 별도의 툴 없이 앱 사용자 정보를 확인 할 수 있습니다.

    앱 추가를 눌러 추가해 주시면 됩니다. 앱을 추가 하면 앱마다 고유한 앱 ID가 설정 되는데 이를 확인하려면 앱을 누르고 원하는 앱을 선택한 뒤에 앱 설정을 눌러주시면 앱 ID를 볼 수 있습니다. 그럼 이제 광고 단위를 추가해 보겠습니다. 원하는 앱 선택 후 광고단위를 눌러주시고 시작하기를 눌러주시면 됩니다. 형식에서 배너를 선택해 줍니다. 광고 단위 이름은 화면 하단 배너라고 해주겠습니다.

    우선 지금은 고급 설정은 넘어가 주도록 하겠습니다. 광고단위만들기를 눌러서 만들어 주고 완료를 눌러주시면 됩니다. 광고단위 추가를 눌러서 전면 광고를 하나 더 만들어 보도록 하겠습니다. 이름은 노트 편집화면 전면으로 해주겠습니다. 하지만 편집화면을 할때마다 계속나오면 불편하기때문에 이를 조절하기 위해서 고급설정을 누르고 게재빈도를 사용해줍니다. 5분에 1번정도로 해주겠습니다.

    그리고 완료를 해주시면 됩니다. 이제 앱에 적용하기 위해 패키지를 추가해보겠습니다. pub.dev에서 google mobile ads를 검색해서 전과 같은 방법으로 추가해 주시면 됩니다. 저는 2.3.0버전을 사용해 보겠습니다. pubspec.yaml에 sqflite 밑에 추가해 주시고 pub get을 해주시면 됩니다. 설치가 끝났다면 프로젝트에서 사용하기 위해 몇가지를 수정하겠습니다. android - app - scr - main 폴더 아래에 AndroidManifest.xml을 실행해줍니다. 여기에 애드몹 앱 ID를 넣어주어야 합니다. <application> 태그안의 가장 하단의 </application> 위쪽에 meta-data를 추가해 주고 여기에 설정을 해줍니다. app id와 그 value를 넣어주는데 아직 출시를 안했으므로 테스트 id를 넣어 주겠습니다.

    <meta-data
        android:name="com.google.android.gms.ads.APPLICATION_ID"
        android:value="ca-app-pub-3940256099942544~3347511713"
    />

    다음으로 android - app 에 build.gradle 파일을 열어줍니다. 그리고 defaultConfig 로 와서 minSdkVersion을 21로 해주시고 다른 21보다 큰 설정이면 그대로 두셔도 됩니다. IOS 9.0 에서만 애드몹이 작동하므로 플랫폼을 9.0이상으로 수정해 줘야 합니다. ios - Podfile 을 열어서 두번째 줄의 주석만 지워주시면 됩니다. 그리고 ios - Runner 폴더에 info.plist에 </dict> 위에 <key></key>를 생성해주시고 그 안에 GADApplicationIdentifier를 입력합니다. 그 아래에 <string></string>에 ios용 앱ID를 넣어줍니다. 준비 되지 않았다면 테스트용 id ca-app-pub-3940256099942544~1458002511를 집어 넣어줍니다.

    이제 배너광고를 집어 넣어 보겠습니다. 애드몹 클래스를 만들어 주겠습니다. lib 아래에 ad_helper.dart 파일을 만들어 줍니다. AdHelper라는 클래스를 만들어 줍니다. 여기에서 광고와 관련된 작업을 구현해 볼것입니다. 앱에서 광고를 사용하려면 광고와 관련된 작업인 광고 SDK 를 초기화 해줘야 하는데 광고SDK 초기화 여부를 확인 할 수 있도록 _isInitialized 를 false로 지정해 줍니다. 그리고 초기화 해주는 함수인 _initialize()를 만들어 주고 초기화 하는데 시간이 걸리므로 Future<void>를 반환 하도록 해줍니다. 그리고 _isInitialized가 false 일때 초기화 시켜주기 위해서 MobiledAds에서 instance에서 initialize() 함수를 사용해 줍니다. 그리고 MobileAds의 initialize가 끝나면 _isInitialized를 true로 바꿔줍니다. 이때 MobileAds의 initialize는 시간이 걸리므로 await를 써주고 함수에 async를 추가해 줍니다.

    bool _isInitialized = false;
    
    Future<void> _initialize() async {
      if (!_isInitialized) {
        await MobileAds.instance.initialize();
        _isInitialized = true;
      }
    }

    다음으로 배너광고를 요청하는 함수를 구현해 보겠습니다. void로 loadBanner 함수를 만들어 줍니다. 인자로는 BannerAd를 인자로 받는 리스너를 추가해 줍니다.

    void loadBanner(Function(BannerAd) onBannerLoaded){
      
    }

    그리고 _initialize()함수를 호출해서 초기화가 되어있지 않으면 초기화 해줍니다. 이때 async와 await를 추가해 줍니다. 그리고 BannerAd를 통해 배너를 불러주겠습니다. 속성으로 adUniId에 id를 추가해 줘야 하는데 플랫폼에 맞는 id를 추가해 주시면 됩니다. 이 함수는 나중에 추가해 주기 위해 다른 속성부터 추가해 주겠습니다. 우선 size를 지정해 줘야 하는데 320 * 50 의 광고를 사용하는데 이를 위해 AdSize.banner을 사용해 주면 됩니다. 다음으로 광고 서빙 옵션을 정하는 request 옵션에 AdRequest를 넣어줍니다. 여기서는 기본값 그대로 사용하겠습니다. 다음은 로드 결과를 알 수 있는 listener을 설정하겠습니다. listener에 BannerAdListener을 넣어주고 광고가 로드 되었을때 이벤트를 받고 싶기 때문에 onAdLoaded에 맵핑을 하는데 광고를 받았을때 위의 listener를 통해 광고 액션을 넘겨주기 위해서 onBannerLoaded에 ad를 넘겨주는데 BannerAd로 변경해서 해주시면 됩니다. 그리고 banner을 load 해주시면 됩니다. 이제 광고 단위 id를 반환하는 함수를 추가해 주겠습니다. 광고 단위 id는 String 으로 반환 되어지고 )getBannerAdUnitId로 함수명을 하시면 됩니다. 안드로이드 일때와 ios일때 id가 다르므로 이를 나눠서 처리하겠습니다. 본인의 광고 Id가 없다면 test용 id를 사용 하시면 됩니다. if에 Platform.isAndroid 일때와 Platform.isIOS로 나눠주시면 됩니다. test Id는 하단에 적어 두겠습니다. 그리고 Platform을 import 해주실때 dart:io 로 해주셔야 플랫폼 확인을 하는 함수를 불러올 수 있습니다. 그리고 위 두가지 플랫폼이 아니면 사용할 수 없으므로 에러메시지를 출력하겠습니다. throw StateError('Unsupported platform');와 같이 함수 안에 넣어주시면 됩니다.

    String _getBannerAdUnitId(){
      if(Platform.isAndroid){
        return 'ca-app-pub-3940256099942544/6300978111';
      }
      if(Platform.isIOS){
        return 'ca-app-pub-3940256099942544/2934735716';
      }
      throw StateError('Unsupported platform');
    }

     그리고 다시 위로 올라가서 adUnitId에 방금 만든 함수를 넣어 주시면 됩니다.

    이로써 AdHelper의 구현은 끝났습니다. 다음은 각 화면에서 사용할 수 있도록 providers를 수정해 주겠습니다. providers.dart로 이동한뒤 noteManager과 같이 상단에 AdHelper를 생성하고 함수로 adHelper를 만들어 준뒤 _adHelper이 생성이 안되있다면 생성해주고 그 값을 반환 시켜주면 됩니다.

    AdHelper? _adHelper;
    
    AdHelper adHelper(){
      if(_adHelper == null){
        _adHelper = AdHelper();
      }
      return _adHelper!;
    }

    이제 note_list_page로 이동해서 수정해 주겠습니다. 로드한 banner 정보를 가지고 있을 변수를 추가해 줍니다. 상태 클래스 안에 build 위젯 위에 만들어 주시면 됩니다.

    BannerAd? _banner;

    배너는 페이지가 호출되는 시점에 요청할 것인데 이를 위해 initState 함수를 추가해서 그 안에서 요청해 주겠습니다. adHelper에 loadBanner을 호출해 줍니다. 그 인자로 리스너 함수를 넘겨주고 있는데 광고가 로드되면 이를 통해서 배너광고 객체를 받을 수 있습니다. 그래서 상단에서 지정한 _banner에 ad를 넣어주시고 화면에 띄워주기 위해서 setState안에 넣어주시면 됩니다. 이제 화면에 띄워줄텐데 GridView의 위쪽에 넣어주겠습니다. 광고가 로드가 되었을 때와 아닐 경우를 모두 고려해서 만들어 줘야 합니다. 그래서 화면에 따라 코드를 다르게 만들어 주겠습니다. 우선 배너를 보여줄 부분을 함수로 만들어 보겠습니다. 하단에 Widget로 _buildBanner을 만들어 주고 인자로 BannerAd ad를 넣어주겠습니다. return에 SizedBox를 만들어서 width와 height를 광고 사이즈 만큼 지정해 줍니다. 숫자로 320 , 50 으로 해도 좋지만 ad.size.width.toDouble()로 광고의 사이즈를 받아 오면 됩니다. width와 height를 지정해준뒤 child에 AdWidget 위젯에 ad옵션에 ad를 넣어주시면 됩니다. 이 부분이 보이게 되면 위가 너무 붙게 되므로 Padding로 감싸서 EdgeInsets.only로 top의 값만 12를 넣어주겠습니다.

    Widget _buildBanner(BannerAd ad) {
      return Padding(
        padding: const EdgeInsets.only(
          top: 12,
        ),
        child: SizedBox(
          width: ad.size.width.toDouble(),
          height: ad.size.height.toDouble(),
          child: AdWidget(
            ad: ad,
          ),
        ),
      );
    }

    이제 GrideView.builder로 이동하겠습니다. 이 return 부분을 final gridView로 변경해서 변수로 바꿔 줍니다. 그리고 하단에 banner에 _banner을 받아 줍니다. 다음에 return에 banner이 null이 아닐 경우에는 Column안에 _buildBanner(banner)를 넣어줘서 광고를 보여주고 다음으로 gridView를 보여줘서 메모를 보여줍니다. 하지만 이렇게만 하면 gridView가 무한으로 늘어나서 error가 발생하므로 gridView 부분만 Expanded로 감싸주면 됩니다. 그리고 삼항의 false 부분에는 gridView만 넣어줘서 현재와 똑같이 보이게 해주시면 됩니다.

    마지막으로 페이지가 완전히 닫혀서 배너 광고를 더 이상 표시하지 않을때 리소스를 반환해야 합니다. 이를 위해 빌드함수 아래서 dispose 함수를 추가해 줍니다. _banner?.dispose();로 반환해주면 됩니다. ?를 추가한 이유는 null이 아닐때만 되도록 하기 위함입니다. 이제 확인을 해보면 잘 출력된것을 확인할 수 있습니다.

    이제 전면광고를 추가해보겠습니다. ad_helper로 이동해서 전면광고 id를 추가해 주겠습니다. 위의 배너 함수를 그대로 복사하셔서 _getInterstitialAdUniId로 변경해주셔도 됩니다. 각 id값은 하단을 참고하시면 됩니다.

    String _getInterstitialAdUnitId() {
      if (Platform.isAndroid) {
        return 'ca-app-pub-3940256099942544/1033173712';
      }
      if (Platform.isIOS) {
        return 'ca-app-pub-3940256099942544/4411468910';
      }
      throw StateError('Unsupported platform');
    }
    String _getInterstitialAdUnitId() {
      if (Platform.isAndroid) {
        return 'ca-app-pub-3940256099942544/1033173712';
      }
      if (Platform.isIOS) {
        return 'ca-app-pub-3940256099942544/4411468910';
      }
      throw StateError('Unsupported platform');
    }

    다음은 전면광고 요청 함수를 만들겠습니다. 위의것을 복사해도 좋습니다. loadInterstitial로 함수명을 해줍니다. 배너와 마찬가지로 광고를 요청받았을때 이벤트를 발생할 리스너를 인자로 받습니다. InterstitialAd로 추가해 주시면 됩니다. 그리고 광고SDK를 우선 초기화 해줘야 합니다. async와 await를 추가해 줍니다. 그리고 InterstitialAd.load로 광고를 요청합니다. adUnitId는 먼저 구현한 _getInterstitialAdUnitId를 추가해 줍니다. request에는 배너와 마찬가지로 AdRequest의 기본값을 사용해 줍니다. 다음으로 adLoadCallback는 광고결과를 확인 할 수 있도록 InterstitialAdLoadCallback을 추가해 줍니다. 제대로 불러왔을 때인 onAdLoaded에는 (ad)를 불러왔을때 onLoaded(ad)로 넘겨주면 됩니다. 실패했을때는 onAdFailedToLoad에는 (e)로 {}안에 debugPrint로 e.toString()의 에러를 보여주겠습니다. 배너와는 다르게 load를 사용하지 않아도 됩니다.

    void loadInterstitial(Function(InterstitialAd) onLoaded) async {
      await _initialize();
      InterstitialAd.load(
        adUnitId: _getInterstitialAdUnitId(),
        request: const AdRequest(),
        adLoadCallback: InterstitialAdLoadCallback(
          onAdLoaded: (ad)=>onLoaded(ad),
          onAdFailedToLoad: (e){
            debugPrint(e.toString());
          },
        ),
      );
    }

    이 전면 광고는 사용자가 edit페이지에서 저장을 누르고 이전 화면으로 넘어갈때 보여주도록 하겠습니다. note_edit_page로 이동해 줍니다. 우선 객체를 저장할 변수를 추가해 줍니다. 상태 클래스 안에 build 위젯 위에 InterstitialAd? _interstitial;을 해주시면 됩니다. 그리고 여기서는 언제 사용자가 전면광고를 띄울 행동을 할지 모르지만 우선 앱을 불러올때 불러오시면 됩니다. 그러므로 initState에 adHelper의 loadInterstitial 함수를 불러와 주시고 (ad){}로 해주시면 됩니다. 그리고 방금 만들어둔 _interstitial에 ad를 저장해 주면 됩니다. 그리고 우리가 화면 전환에 이용할 것이므로 saveNote로 이동해 줍니다. 그리고 화면을 끄기 전인 pop 전에 _interstitial.show로 광고를 띄워줍니다. 광고가 있을때만 띄우기 위해서 _interstitial?로 해주시면 됩니다. 그리고 페이지가 종료될 때 로드되어있는 전면광고 객체를 dispose해주겠습니다. 이것도 _intestitla?.dispose()로 해주시면 됩니다. 이제 실행을 해서 새로운 노트를 만들면 정상적으로 출력되는 것을 볼 수 있습니다.

    광고는 이렇게 마치고 다른 형태는 구글링이나 예제를 찾아보시면 될 것 같습니다. 다음에는 배포를 한번 해보도록 하겠습니다.

    오늘의 전체코드

    import 'dart:io';
    
    import 'package:flutter/material.dart';
    import 'package:google_mobile_ads/google_mobile_ads.dart';
    
    class AdHelper {
      bool _isInitialized = false;
    
      void loadBanner(Function(BannerAd) onBannerLoaded) async {
        await _initialize();
    
        final banner = BannerAd(
          size: AdSize.banner,
          adUnitId: _getBannerAdUnitId(),
          listener: BannerAdListener(
            onAdLoaded: (ad) => onBannerLoaded(ad as BannerAd),
          ),
          request: const AdRequest(),
        );
        banner.load();
      }
    
      void loadInterstitial(Function(InterstitialAd) onLoaded) async {
        await _initialize();
        InterstitialAd.load(
          adUnitId: _getInterstitialAdUnitId(),
          request: const AdRequest(),
          adLoadCallback: InterstitialAdLoadCallback(
            onAdLoaded: (ad) => onLoaded(ad),
            onAdFailedToLoad: (e) {
              debugPrint(e.toString());
            },
          ),
        );
      }
    
      Future<void> _initialize() async {
        if (!_isInitialized) {
          await MobileAds.instance.initialize();
          _isInitialized = true;
        }
      }
    
      String _getBannerAdUnitId() {
        if (Platform.isAndroid) {
          return 'ca-app-pub-3940256099942544/6300978111';
        }
        if (Platform.isIOS) {
          return 'ca-app-pub-3940256099942544/2934735716';
        }
        throw StateError('Unsupported platform');
      }
    
      String _getInterstitialAdUnitId() {
        if (Platform.isAndroid) {
          return 'ca-app-pub-3940256099942544/1033173712';
        }
        if (Platform.isIOS) {
          return 'ca-app-pub-3940256099942544/4411468910';
        }
        throw StateError('Unsupported platform');
      }
    }
    

    ad_helper.dart

    import 'package:sticky_memo/ad_helper.dart';
    import 'package:sticky_memo/data/note_manager.dart';
    
    AdHelper? _adHelper;
    
    NoteManager? _noteManager;
    
    AdHelper adHelper(){
      _adHelper ??= AdHelper();
      return _adHelper!;
    }
    
    NoteManager noteManager(){
      _noteManager ??= NoteManager();
      return _noteManager!;
    }

    providers.dart

    import 'package:flutter/material.dart';
    import 'package:google_mobile_ads/google_mobile_ads.dart';
    import 'package:sticky_memo/data/note.dart';
    import 'package:sticky_memo/providers.dart';
    
    class NoteEditPage extends StatefulWidget {
      const NoteEditPage({Key? key, this.id}) : super(key: key);
    
      static const routeName = '/edit';
    
      final int? id;
    
      @override
      State<NoteEditPage> createState() => _NoteEditPageState();
    }
    
    class _NoteEditPageState extends State<NoteEditPage> {
      final TextEditingController titleController = TextEditingController();
      final TextEditingController bodyController = TextEditingController();
    
      Color memoColor = Note.colorDefault;
    
      InterstitialAd? _interstitial;
    
      @override
      void initState() {
        // TODO: implement initState
        super.initState();
        final noteId = widget.id;
        if (noteId != null) {
          noteManager().getNote(noteId).then((note) {
            titleController.text = note.title;
            bodyController.text = note.body;
            setState(() {
              memoColor = note.color;
            });
          });
        }
        adHelper().loadInterstitial((ad){
          _interstitial = ad;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('노트 편집'),
            backgroundColor: Colors.blue,
            actions: [
              IconButton(
                onPressed: _displayColorSelectionDialog,
                icon: const Icon(Icons.color_lens),
                tooltip: '배경색 선택',
              ),
              IconButton(
                onPressed: _saveNote,
                icon: const Icon(Icons.save),
                tooltip: '저장',
              ),
            ],
          ),
          body: SizedBox.expand(
            child: Container(
              color: memoColor,
              child: SingleChildScrollView(
                padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    TextField(
                      decoration: const InputDecoration(
                        border: OutlineInputBorder(),
                        label: Text('제목 입력'),
                      ),
                      maxLines: 1,
                      style: const TextStyle(
                        fontSize: 20,
                      ),
                      controller: titleController,
                    ),
                    const SizedBox(
                      height: 8,
                    ),
                    TextField(
                      decoration: const InputDecoration(
                        border: InputBorder.none,
                        hintText: '내용 입력',
                      ),
                      maxLines: null,
                      keyboardType: TextInputType.multiline,
                      controller: bodyController,
                    ),
                  ],
                ),
              ),
            ),
          ),
        );
      }
       @override
      void dispose() {
        _interstitial?.dispose();
        super.dispose();
      }
    
      void _displayColorSelectionDialog() {
        FocusManager.instance.primaryFocus!.unfocus();
    
        showDialog(
          context: context,
          builder: (context) {
            return AlertDialog(
              title: const Text('배경색 선택'),
              content: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  ListTile(
                    title: const Text('없음'),
                    onTap: () => _applyColor(Note.colorDefault),
                  ),
                  ListTile(
                    leading: const CircleAvatar(
                      backgroundColor: Note.colorRed,
                    ),
                    title: Text('빨간색'),
                    onTap: () => _applyColor(Note.colorRed),
                  ),
                  ListTile(
                    leading: const CircleAvatar(
                      backgroundColor: Note.colorLime,
                    ),
                    title: Text('연두색'),
                    onTap: () => _applyColor(Note.colorLime),
                  ),
                  ListTile(
                    leading: const CircleAvatar(
                      backgroundColor: Note.colorBlue,
                    ),
                    title: Text('파란색'),
                    onTap: () => _applyColor(Note.colorBlue),
                  ),
                  ListTile(
                    leading: const CircleAvatar(
                      backgroundColor: Note.colorOrange,
                    ),
                    title: Text('주황색'),
                    onTap: () => _applyColor(Note.colorOrange),
                  ),
                  ListTile(
                    leading: const CircleAvatar(
                      backgroundColor: Note.colorYellow,
                    ),
                    title: Text('노란색'),
                    onTap: () => _applyColor(Note.colorYellow),
                  ),
                ],
              ),
            );
          },
        );
      }
    
      void _applyColor(Color newColor) {
        setState(() {
          Navigator.pop(context);
          memoColor = newColor;
        });
      }
    
      void _saveNote() {
        if (bodyController.text.isNotEmpty) {
          final note = Note(
            bodyController.text,
            title: titleController.text,
            color: memoColor,
          );
          final noteIndex = widget.id;
          if (noteIndex != null) {
            noteManager().updateNote(noteIndex, note);
          } else {
            noteManager().addNote(note);
          }
          _interstitial?.show();
          Navigator.pop(context);
        } else {
          ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
            content: Text('내용을 입력하세요.'),
            behavior: SnackBarBehavior.floating,
          ));
        }
      }
    }
    

    note_edit_page.dart

    import 'package:flutter/material.dart';
    import 'package:google_mobile_ads/google_mobile_ads.dart';
    import 'package:sticky_memo/data/note.dart';
    import 'package:sticky_memo/page/note_edit_page.dart';
    import 'package:sticky_memo/page/note_view_page.dart';
    import 'package:sticky_memo/providers.dart';
    
    class NoteListPage extends StatefulWidget {
      const NoteListPage({Key? key}) : super(key: key);
    
      static const routeName = '/';
    
      @override
      State<NoteListPage> createState() => _NoteListPageState();
    }
    
    class _NoteListPageState extends State<NoteListPage> {
      BannerAd? _banner;
    
      @override
      void initState() {
        // TODO: implement initState
        super.initState();
        adHelper().loadBanner((ad) {
          setState(() {
            _banner = ad;
          });
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Sticky Memo'),
            backgroundColor: Colors.blue,
          ),
          body: FutureBuilder<List<Note>>(
            future: noteManager().listNotes(),
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return const Center(
                  child: CircularProgressIndicator(),
                );
              }
              if (snapshot.hasError) {
                return Scaffold(
                  appBar: AppBar(),
                  body: const Center(
                    child: Text('오류가 발생했습니다.'),
                  ),
                );
              }
              final notes = snapshot.requireData;
              final gridView = GridView.builder(
                gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 2,
                  childAspectRatio: 1,
                ),
                padding: const EdgeInsets.symmetric(
                  horizontal: 12,
                  vertical: 16,
                ),
                itemCount: notes.length,
                itemBuilder: (BuildContext context, int index) =>
                    _buildCard(notes[index]),
              );
              final banner = _banner;
              return banner != null
                  ? Column(
                      children: [
                        _buildBanner(banner),
                        Expanded(child: gridView),
                      ],
                    )
                  : gridView;
            },
          ),
          floatingActionButton: FloatingActionButton(
            tooltip: '새 노트',
            onPressed: () {
              Navigator.pushNamed(context, NoteEditPage.routeName).then((_) {
                setState(() {});
              });
            },
            child: const Icon(Icons.add),
          ),
        );
      }
    
      @override
      void dispose() {
        _banner?.dispose();
        super.dispose();
    
      }
    
      Widget _buildCard(Note note) {
        return InkWell(
          onTap: () {
            Navigator.pushNamed(context, NoteViewPage.routeName, arguments: note.id)
                .then((_) {
              setState(() {});
            });
          },
          child: Card(
            color: note.color,
            child: Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    note.title.isEmpty ? '(제목없음)' : note.title,
                    style: const TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(
                    height: 16,
                  ),
                  Expanded(
                    child: Text(
                      note.body,
                      overflow: TextOverflow.fade,
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    
      Widget _buildBanner(BannerAd ad) {
        return Padding(
          padding: const EdgeInsets.only(
            top: 12,
          ),
          child: SizedBox(
            width: ad.size.width.toDouble(),
            height: ad.size.height.toDouble(),
            child: AdWidget(
              ad: ad,
            ),
          ),
        );
      }
    }
    

    note_list_page.dart

    반응형

    댓글

Designed by Tistory.