언어·프레임워크/Flutter

[Flutter] "Do it! 플러터 앱 프로그래밍" - 파이어베이스와 광고 수입 얻기 | 데이터베이스를 이용한 메모장 앱

DandyNow 2022. 2. 21. 23:51
728x90
반응형

"조준수. (2021). Do it! 플러터 앱 프로그래밍. 이지스퍼블리싱", 13장 중 파이어베이스를 이용한 메모장 앱 만들기를 실습하였다. 파이어베이스의 애널리틱스, 푸시 알림 서비스, 애드몹을 이용한 실습도 진행했다.

 

Do it! 플러터 앱 프로그래밍

플러터 기본 & 고급 위젯은 물론오픈 API와 파이어베이스를 이용한 앱 개발부터 배포까지!플러터 SDK 2.x 버전을 반영한 개정판!이 책은 플러터의 기초부터 고급 활용법까지 다루어 다양한 영역에

book.naver.com

 

데이터베이스를 이용한 메모장 앱

실시간 데이터베이스(Realtime Database) 만들기

파이어베이스에서 Realtime Database를 생성한다. 위치는 미국, 테스트 모드에서 시작, 규칙은 읽기/쓰기 true 설정한다.

 

패키지 추가

pubspec.yaml의 dependencies 항목에 다음과 같이 패키지를 추가한다. 유의해야 할 점은 버전을 동일하게 맞추어야 한다. 버전이 다를 경우 이후 작성하게 될 dart 파일 코드에 오류가 발생하게 된다.

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  firebase_core: ^1.2.1
  firebase_analytics: ^8.1.1
  firebase_database: ^7.1.0
  
  # 아래는 실습할 당시 최신 버전이었는데 의존성 주입후 이후 작성하게될 메모 앱 소스 작성시 문법 오류가 발생했다.
  # firebase_core: ^1.12.0
  # firebase_analytics: ^9.1.0
  # firebase_database: ^9.0.6

 

메모장 앱 만들기

메모 추가 기능을 갖춘 메모장 앱을 만든다. 

// main.dart
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_analytics/observer.dart';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart'; // 추가
import 'tabsPage.dart';
import 'memoPage.dart';

void main() async {
  // async 추가
  WidgetsFlutterBinding.ensureInitialized(); // 추가
  await Firebase.initializeApp(); // 추가
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  static FirebaseAnalytics analytics =
      FirebaseAnalytics(); // 본문의 코드. FirebaseAnalytics()에서 문법 에러 발생, pubspec.yaml의 firebase_analytics: ^8.1.1 설정하면 정상 작동함
  // static FirebaseAnalytics analytics = FirebaseAnalytics
  //     .instance; // FirebaseAnalytics()를 FirebaseAnalytics.instance로 수정
  static FirebaseAnalyticsObserver observer =
      FirebaseAnalyticsObserver(analytics: analytics);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Firebase Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      navigatorObservers: <NavigatorObserver>[observer],
      // home: FirebaseApp(
      //   analytics: analytics,
      //   observer: observer,
      // ),
      home: MemoPage(),
    );
  }
}

class FirebaseApp extends StatefulWidget {
  FirebaseApp({Key? key, required this.analytics, required this.observer})
      : super(key: key);
  final FirebaseAnalytics analytics;
  final FirebaseAnalyticsObserver observer;

  @override
  _FirebaseAppState createState() => _FirebaseAppState(analytics, observer);
}

class _FirebaseAppState extends State<FirebaseApp> {
  _FirebaseAppState(this.analytics, this.observer);

  final FirebaseAnalyticsObserver observer;
  final FirebaseAnalytics analytics;
  String _message = '';

  void setMessage(String message) {
    setState(() {
      _message = message;
    });
  }

  Future<void> _sendAnalyticsEvent() async {
    // 애널리틱스의 logEvent를 호출해 test_event라는 키값으로 데이터 저장
    await analytics.logEvent(
      name: 'test_event',
      parameters: <String, dynamic>{
        'string': 'hello flutter',
        'int': 100,
      },
    );
    setMessage('Analytics 보내기 성공');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Firebase Example'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            ElevatedButton(
              child: Text('테스트'),
              onPressed: _sendAnalyticsEvent,
            ),
            Text(_message, style: const TextStyle(color: Colors.blueAccent)),
          ],
          mainAxisAlignment: MainAxisAlignment.center,
        ),
      ),
      floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.tab),
          // 플로팅 버튼을 눌렀을 때 라우트 기능을 이용해 tabsPage로 이동하는 코드
          onPressed: () {
            Navigator.of(context).push(MaterialPageRoute<TabsPage>(
                settings: RouteSettings(name: '/tab'),
                builder: (BuildContext context) {
                  return TabsPage(observer);
                }));
          }),
    );
  }
}

 

// memo.dart
import 'package:firebase_database/firebase_database.dart';

class Memo {
  String? key;
  String title;
  String content;
  String createTime;

  Memo(this.title, this.content, this.createTime);

  Memo.fromSnapshot(DataSnapshot snapshot)
      : key = snapshot.key,
        title = snapshot.value[
            'title'], // firebase_database: ^7.1.0이 아닌 최신 버전에서는 "["에서 에러가 발생했다.
        content = snapshot.value['content'],
        createTime = snapshot.value['createTime'];

  toJson() {
    return {
      'title': title,
      'content': content,
      'createTime': createTime,
    };
  }
}

 

// memoAdd.dart
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'memo.dart';

class MemoAddApp extends StatefulWidget {
  final DatabaseReference reference;

  MemoAddApp(this.reference);

  @override
  State<StatefulWidget> createState() => _MemoAddApp();
}

class _MemoAddApp extends State<MemoAddApp> {
  TextEditingController? titleController;
  TextEditingController? contentController;

  @override
  void initState() {
    super.initState();
    titleController = TextEditingController();
    contentController = TextEditingController();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('메모 추가'),
      ),
      body: Container(
        padding: EdgeInsets.all(20),
        child: Center(
          child: Column(
            children: <Widget>[
              TextField(
                controller: titleController,
                decoration: InputDecoration(
                    labelText: '제목', fillColor: Colors.blueAccent),
              ),
              Expanded(
                  child: TextField(
                controller: contentController,
                keyboardType: TextInputType.multiline,
                maxLines: 100,
                decoration: InputDecoration(labelText: '내용'),
              )),
              MaterialButton(
                onPressed: () {
                  // reference.push().set() 함수로 데이터 베이스에 저장
                  widget.reference
                      .push()
                      .set(Memo(
                              titleController!.value.text,
                              contentController!.value.text,
                              DateTime.now().toIso8601String())
                          .toJson())
                      .then((_) {
                    Navigator.of(context).pop();
                  });
                },
                child: Text('저장하기'),
                shape:
                    OutlineInputBorder(borderRadius: BorderRadius.circular(1)),
              )
            ],
          ),
        ),
      ),
    );
  }
}

 

// memoPage.dart
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'memo.dart';
import 'memoAdd.dart';

class MemoPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _MemoPage();
}

class _MemoPage extends State<MemoPage> {
  FirebaseDatabase? _database;
  DatabaseReference? reference;
  String _databaseURL = '### 데이터베이스 URL ###';
  List<Memo> memos = new List.empty(growable: true);

  @override
  void initState() {
    super.initState();
    _database = FirebaseDatabase(databaseURL: _databaseURL);
    reference = _database!
        .reference()
        .child('memo'); // memo 컬렉션을 만든다. 이 컬렉션 안에서 데이터를 쓰거나 읽는다.

    // 데이터베이스에 저장된 데이터를 가져온다.
    reference!.onChildAdded.listen((event) {
      print(event.snapshot.value.toString());
      setState(() {
        memos.add(Memo.fromSnapshot(event.snapshot)); // 데이터를 memos 리스트에 추가한다.
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('메모 앱'),
      ),
      body: Container(
        child: Center(
          child: memos.length == 0
              ? CircularProgressIndicator() // 데이터베이스에서 불러온 데이터가 없으면 프로그레스 표시
              // 데이터가 있으면 그리드뷰를 만든다.
              : GridView.builder(
                  // 정형화된 그리드뷰 생성(SliverGridDelegateWithFixedCrossAxisCount)
                  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                      crossAxisCount: 2), // 열 개수 2
                  itemBuilder: (context, index) {
                    return Card(
                      child: GridTile(
                        child: Container(
                          padding: EdgeInsets.only(top: 20, bottom: 20),
                          child: SizedBox(
                            child: GestureDetector(
                              // 메모 상세보기 화면으로 이동
                              onTap: () async {},
                              // 길게 클릭 시 메모 삭제
                              onLongPress: () {},
                              child: Text(memos[index].content),
                            ),
                          ),
                        ),
                        header: Text(memos[index].title),
                        footer: Text(memos[index].createTime.substring(0, 10)),
                      ),
                    );
                  },
                  itemCount: memos.length,
                ),
        ),
      ),
      // 메모를 추가하는 페이지로 이동
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.of(context).push(
              MaterialPageRoute(builder: (context) => MemoAddApp(reference!)));
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

 

[그림 1] 메모 앱에서 메모 추가

 


 

데이터 수정/삭제 기능 추가

// memoDetail.dart
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'memo.dart';

class MemoDetailPage extends StatefulWidget {
  final DatabaseReference reference;
  final Memo memo;

  MemoDetailPage(this.reference, this.memo);

  @override
  State<StatefulWidget> createState() => _MemoDetailPage();
}

class _MemoDetailPage extends State<MemoDetailPage> {
  TextEditingController? titleController;
  TextEditingController? contentController;

  @override
  void initState() {
    super.initState();
    titleController = TextEditingController(text: widget.memo.title);
    contentController = TextEditingController(text: widget.memo.content);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.memo.title),
      ),
      body: Container(
        padding: EdgeInsets.all(20),
        child: Center(
          child: Column(
            children: <Widget>[
              TextField(
                controller: titleController,
                decoration: InputDecoration(
                    labelText: '제목', fillColor: Colors.blueAccent),
              ),
              Expanded(
                  child: TextField(
                controller: contentController,
                keyboardType: TextInputType.multiline,
                maxLines: 100,
                decoration: InputDecoration(labelText: '내용'),
              )),
              MaterialButton(
                onPressed: () {
                  Memo memo = Memo(titleController!.value.text,
                      contentController!.value.text, widget.memo.createTime);
                  widget.reference
                      .child(widget
                          .memo.key!) // 메모의 key값을 가져와서 같은 key에 해당하는 데이터를 수정한다.
                      .set(memo.toJson())
                      .then((_) {
                    Navigator.of(context).pop(memo);
                  });
                },
                child: Text('수정하기'),
                shape:
                    OutlineInputBorder(borderRadius: BorderRadius.circular(1)),
              )
            ],
          ),
        ),
      ),
    );
  }
}

 

// memoPage.dart
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'memo.dart';
import 'memoAdd.dart';
// -----------------------
import 'memoDetail.dart';
// -----------------------

class MemoPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _MemoPage();
}

class _MemoPage extends State<MemoPage> {
  FirebaseDatabase? _database;
  DatabaseReference? reference;
  String _databaseURL = '### 데이터베이스 URL ###';
  List<Memo> memos = new List.empty(growable: true);

  @override
  void initState() {
    super.initState();
    _database = FirebaseDatabase(databaseURL: _databaseURL);
    reference = _database!
        .reference()
        .child('memo'); // memo 컬렉션을 만든다. 이 컬렉션 안에서 데이터를 쓰거나 읽는다.

    // 데이터베이스에 저장된 데이터를 가져온다.
    reference!.onChildAdded.listen((event) {
      print(event.snapshot.value.toString());
      setState(() {
        memos.add(Memo.fromSnapshot(event.snapshot)); // 데이터를 memos 리스트에 추가한다.
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('메모 앱'),
      ),
      body: Container(
        child: Center(
          child: memos.length == 0
              ? CircularProgressIndicator() // 데이터베이스에서 불러온 데이터가 없으면 프로그레스 표시
              // 데이터가 있으면 그리드뷰를 만든다.
              : GridView.builder(
                  // 정형화된 그리드뷰 생성(SliverGridDelegateWithFixedCrossAxisCount)
                  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                      crossAxisCount: 2), // 열 개수 2
                  itemBuilder: (context, index) {
                    return Card(
                      child: GridTile(
                        child: Container(
                          padding: EdgeInsets.only(top: 20, bottom: 20),
                          child: SizedBox(
                            child: GestureDetector(
                              // --------- 메모 상세보기 화면으로 이동 ----------
                              onTap: () async {
                                Memo? memo = await Navigator.of(context).push(
                                    MaterialPageRoute<Memo>(
                                        builder: (BuildContext context) =>
                                            MemoDetailPage(
                                                reference!, memos[index])));
                                if (memo != null) {
                                  setState(() {
                                    memos[index].title = memo.title;
                                    memos[index].content = memo.content;
                                  });
                                }
                              },
                              // ----------------------------------------------
                              // ----------- 길게 클릭 시 메모 삭제 -------------
                              onLongPress: () {
                                showDialog(
                                    context: context,
                                    builder: (context) {
                                      return AlertDialog(
                                        title: Text(memos[index].title),
                                        content: Text('삭제하시겠습니까?'),
                                        actions: <Widget>[
                                          FlatButton(
                                              onPressed: () {
                                                reference!
                                                    .child(memos[index].key!)
                                                    .remove()
                                                    .then((_) {
                                                  setState(() {
                                                    memos.removeAt(index);
                                                    Navigator.of(context).pop();
                                                  });
                                                });
                                              },
                                              child: Text('예')),
                                          FlatButton(
                                              onPressed: () {
                                                Navigator.of(context).pop();
                                              },
                                              child: Text('아니요')),
                                        ],
                                      );
                                    });
                              },
                              // ----------------------------------------------
                              child: Text(memos[index].content),
                            ),
                          ),
                        ),
                        header: Text(memos[index].title),
                        footer: Text(memos[index].createTime.substring(0, 10)),
                      ),
                    );
                  },
                  itemCount: memos.length,
                ),
        ),
      ),
      // 메모를 추가하는 페이지로 이동
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.of(context).push(
              MaterialPageRoute(builder: (context) => MemoAddApp(reference!)));
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

 

[그림 2] 메모 수정/삭제

 


 

푸시 알림 보내기

패키지 추가

pubspec.yaml의 dependencies 항목에 다음과 같이 패키지를 추가한다. 

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  firebase_core: ^1.2.1
  firebase_analytics: ^8.1.1
  firebase_database: ^7.1.0
  # --- 파이어베이스 메시징 패키지 ---
  firebase_messaging: ^10.0.1
  # --------------------------------

 

// main.dart
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_analytics/observer.dart';
import 'package:flutter/material.dart';
// --------------- 파이어베이스 푸시 알림 받기 ----------------
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
// ----------------------------------------------------------
import 'tabsPage.dart';
import 'memoPage.dart';

void main() async {
  // async 추가
  WidgetsFlutterBinding.ensureInitialized(); // 추가
  await Firebase.initializeApp(); // 추가
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  static FirebaseAnalytics analytics =
      FirebaseAnalytics(); // 본문의 코드. FirebaseAnalytics()에서 문법 에러 발생, pubspec.yaml의 firebase_analytics: ^8.1.1 설정하면 정상 작동함
  // static FirebaseAnalytics analytics = FirebaseAnalytics
  //     .instance; // FirebaseAnalytics()를 FirebaseAnalytics.instance로 수정
  static FirebaseAnalyticsObserver observer =
      FirebaseAnalyticsObserver(analytics: analytics);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Firebase Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      navigatorObservers: <NavigatorObserver>[observer],
      // home: FirebaseApp(
      //   analytics: analytics,
      //   observer: observer,
      // ),
      // home: MemoPage(),
// -------------------- 파이어베이스 푸시 알림 받기 -------------------------
      home: FutureBuilder(
        future: Firebase.initializeApp(), // 선언해야 할 함수
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            // 만약 선언 시 에러가 나면 출력될 위젯
            return Center(
              child: Text('Error'),
            );
          }
          // 선언 완료 후 표시할 위젯
          if (snapshot.connectionState == ConnectionState.done) {
            _initFirebaseMessaging(context);
            _getToken();
            return MemoPage();
          }
          // 선언되는 동안 표시할 위젯
          return Center(
            child: CircularProgressIndicator(),
          );
        },
      ),
    );
  }

  _getToken() async {
    FirebaseMessaging messaging = FirebaseMessaging.instance;
    print("messaging.getToken() , ${await messaging.getToken()}");
  }

  _initFirebaseMessaging(BuildContext context) {
    FirebaseMessaging.onMessage.listen((RemoteMessage event) {
      print(event.notification!.title);
      print(event.notification!.body);
      // 메시지가 화면에 표시되도록 처리
      showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              title: Text("알림"),
              content: Text(event.notification!.body!),
              actions: [
                TextButton(
                  child: Text("Ok"),
                  onPressed: () {
                    Navigator.of(context).pop();
                  },
                )
              ],
            );
          });
    });
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {});
  }
}
// ----------------------------------------------------------------------------

 

파이어베이스에서 푸시 알림 보내고 앱에서 확인

파이어베이스 콘솔에서 "참여 → Cloud Messaging → Send your first message" 클릭.

 

[그림 3] 푸시 알림 테스트

 


 

앱에 광고 넣어 수익화하기

앱에 광고를 넣어도 광고가 바로 노출되지 않는다. 애드몹에서 광고를 심의하고 기타 내부 처리를 진행하는 데 1~2일 정도 소요되기 때문이다. 우선 파이어베이스 콘솔에서 애드몹( AdMob)에 가입한 후 광고 단위를 만든다. 

 

앱에 광고 넣기

pubspec.yaml의 dependencies에 파이어베이스 애드몹 패키지를 추가한다.

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  firebase_core: ^1.2.1
  firebase_analytics: ^8.1.1
  firebase_database: ^7.1.0
  firebase_messaging: ^10.0.1
  # --- 파이어베이스 애드몹 패키지 ---
  google_mobile_ads: ^0.13.0
  # --------------------------------

 

AndroidManifest.xml에 meta-data 태그를 추가한다.

<!-- android/app/src/main/AndroidManifest.xml -->
<meta-data
    android:name="com.google.android.gms.ads.APPLICATION_ID"
    android:value="### 애드몹 앱 ID ###"/>

 

main.dart의 상단에 애드몹 패키지를 import 하고 main 함수에서 애드몹 초기화 함수를 호출한다. 

// main.dart
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_analytics/observer.dart';
import 'package:flutter/material.dart';
// --------------- 파이어베이스 푸시 알림 받기 ----------------
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
// ----------------------------------------------------------
import 'tabsPage.dart';
import 'memoPage.dart';
// --------------- 앱에 광고 넣기(애드몹 패키지) --------------
import 'package:google_mobile_ads/google_mobile_ads.dart';
// ----------------------------------------------------------

void main() async {
  // async 추가
  WidgetsFlutterBinding.ensureInitialized(); // 추가
  await Firebase.initializeApp(); // 추가
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  static FirebaseAnalytics analytics =
      FirebaseAnalytics(); // 본문의 코드. FirebaseAnalytics()에서 문법 에러 발생, pubspec.yaml의 firebase_analytics: ^8.1.1 설정하면 정상 작동함
  // static FirebaseAnalytics analytics = FirebaseAnalytics
  //     .instance; // FirebaseAnalytics()를 FirebaseAnalytics.instance로 수정
  static FirebaseAnalyticsObserver observer =
      FirebaseAnalyticsObserver(analytics: analytics);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Firebase Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      navigatorObservers: <NavigatorObserver>[observer],
      // home: FirebaseApp(
      //   analytics: analytics,
      //   observer: observer,
      // ),
      // home: MemoPage(),
// -------------------- 파이어베이스 푸시 알림 받기 -------------------------
      home: FutureBuilder(
        future: Firebase.initializeApp(), // 선언해야 할 함수
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            // 만약 선언 시 에러가 나면 출력될 위젯
            return Center(
              child: Text('Error'),
            );
          }
          // 선언 완료 후 표시할 위젯
          if (snapshot.connectionState == ConnectionState.done) {
            _initFirebaseMessaging(context);
            _getToken();
            return MemoPage();
          }
          // 선언되는 동안 표시할 위젯
          return Center(
            child: CircularProgressIndicator(),
          );
        },
      ),
    );
  }

  _getToken() async {
    FirebaseMessaging messaging = FirebaseMessaging.instance;
    print("messaging.getToken() , ${await messaging.getToken()}");
  }

  _initFirebaseMessaging(BuildContext context) {
    FirebaseMessaging.onMessage.listen((RemoteMessage event) {
      print(event.notification!.title);
      print(event.notification!.body);
      // 메시지가 화면에 표시되도록 처리
      showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              title: Text("알림"),
              content: Text(event.notification!.body!),
              actions: [
                TextButton(
                  child: Text("Ok"),
                  onPressed: () {
                    Navigator.of(context).pop();
                  },
                )
              ],
            );
          });
    });
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {});
  }
}
// ----------------------------------------------------------------------------

 

memoPage.dart에 하단 배너 광고를 넣는다. 플로팅 버튼과 하단 배너 광고가 겹쳐 보이는 문제도 해결한다.

// memoPage.dart
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'memo.dart';
import 'memoAdd.dart';
// -----------------------
import 'memoDetail.dart';
// -----------------------
// --------------- 앱에 광고 넣기(애드몹 패키지) --------------
import 'package:google_mobile_ads/google_mobile_ads.dart';
// ----------------------------------------------------------

class MemoPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _MemoPage();
}

class _MemoPage extends State<MemoPage> {
  FirebaseDatabase? _database;
  DatabaseReference? reference;
  String _databaseURL = '### 데이터베이스 URL ###';
  List<Memo> memos = new List.empty(growable: true);

  // --- 광고 클래스 및 광고가 지금 로드되었는지 확인하는 변수 ---
  BannerAd? _banner;
  bool _loadingBanner = false;
  // ---------------------------------------------------------

  // --------------------- 배너 광고에 대한 정보 입력 ------------------------
  Future<void> _createBanner(BuildContext context) async {
    final AnchoredAdaptiveBannerAdSize? size =
        await AdSize.getAnchoredAdaptiveBannerAdSize(
      Orientation.portrait,
      MediaQuery.of(context).size.width.truncate(),
    );
    if (size == null) {
      return;
    }
    final BannerAd banner = BannerAd(
      size: size, // 광고의 크기
      request: AdRequest(),
      // 처음 애드몹 광고를 신청하면 아직 광고를 배정받지 못해서 로드에 실패한다.
      // 그래서 테스트용 값을 이용해 배너 광고를 표시하도록 한다.
      adUnitId: BannerAd.testAdUnitId, // '### 하단 배너 광고 ID ###',
      listener: BannerAdListener(
        // 애드몹에서 광고를 호출할 때 받아오는 데이터타입
        onAdLoaded: (Ad ad) {
          print('$BannerAd loaded.');
          setState(() {
            _banner = ad as BannerAd?;
          });
        },
        // 배너 로드 실패
        onAdFailedToLoad: (Ad ad, LoadAdError error) {
          print('$BannerAd failedToLoad: $error');
          ad.dispose();
        },
        // 배너 로드 완료
        onAdOpened: (Ad ad) => print('$BannerAd onAdOpened.'),
        onAdClosed: (Ad ad) => print('$BannerAd onAdClosed.'),
      ),
    );
    return banner.load();
  }
  // ------------------------------------------------------------------------

  @override
  void initState() {
    super.initState();
    _database = FirebaseDatabase(databaseURL: _databaseURL);
    reference = _database!
        .reference()
        .child('memo'); // memo 컬렉션을 만든다. 이 컬렉션 안에서 데이터를 쓰거나 읽는다.

    // 데이터베이스에 저장된 데이터를 가져온다.
    reference!.onChildAdded.listen((event) {
      print(event.snapshot.value.toString());
      setState(() {
        memos.add(Memo.fromSnapshot(event.snapshot)); // 데이터를 memos 리스트에 추가한다.
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    // --- 현재 배너가 로드되어 있지 않으면 _createBanner 함수를 호출하여 배너 생성 ---
    if (!_loadingBanner) {
      _loadingBanner = true;
      _createBanner(context);
    }
    // ----------------------------------------------------------------------------
    return Scaffold(
      appBar: AppBar(
        title: Text('메모 앱'),
      ),
      // -------------------- body에 Stack 추가, 스택의 가장 마지막 위젯에 광고 표시 ----------------------
      body: Stack(
        alignment: AlignmentDirectional.bottomCenter,
        children: <Widget>[
          Container(
            child: Center(
              child: memos.length == 0
                  ? CircularProgressIndicator() // 데이터베이스에서 불러온 데이터가 없으면 프로그레스 표시
                  // 데이터가 있으면 그리드뷰를 만든다.
                  : GridView.builder(
                      // 정형화된 그리드뷰 생성(SliverGridDelegateWithFixedCrossAxisCount)
                      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                          crossAxisCount: 2), // 열 개수 2
                      itemBuilder: (context, index) {
                        return Card(
                          child: GridTile(
                            child: Container(
                              padding: EdgeInsets.only(top: 20, bottom: 20),
                              child: SizedBox(
                                child: GestureDetector(
                                  // --------- 메모 상세보기 화면으로 이동 ----------
                                  onTap: () async {
                                    Memo? memo = await Navigator.of(context)
                                        .push(MaterialPageRoute<Memo>(
                                            builder: (BuildContext context) =>
                                                MemoDetailPage(
                                                    reference!, memos[index])));
                                    if (memo != null) {
                                      setState(() {
                                        memos[index].title = memo.title;
                                        memos[index].content = memo.content;
                                      });
                                    }
                                  },
                                  // ----------------------------------------------
                                  // ----------- 길게 클릭 시 메모 삭제 -------------
                                  onLongPress: () {
                                    showDialog(
                                        context: context,
                                        builder: (context) {
                                          return AlertDialog(
                                            title: Text(memos[index].title),
                                            content: Text('삭제하시겠습니까?'),
                                            actions: <Widget>[
                                              FlatButton(
                                                  onPressed: () {
                                                    reference!
                                                        .child(
                                                            memos[index].key!)
                                                        .remove()
                                                        .then((_) {
                                                      setState(() {
                                                        memos.removeAt(index);
                                                        Navigator.of(context)
                                                            .pop();
                                                      });
                                                    });
                                                  },
                                                  child: Text('예')),
                                              FlatButton(
                                                  onPressed: () {
                                                    Navigator.of(context).pop();
                                                  },
                                                  child: Text('아니요')),
                                            ],
                                          );
                                        });
                                  },
                                  // ----------------------------------------------
                                  child: Text(memos[index].content),
                                ),
                              ),
                            ),
                            header: Text(memos[index].title),
                            footer:
                                Text(memos[index].createTime.substring(0, 10)),
                          ),
                        );
                      },
                      itemCount: memos.length,
                    ),
            ),
          ),
          // ----------------- 배너 광고 -----------------
          if (_banner != null)
            Container(
              color: Colors.green,
              width: _banner!.size.width.toDouble(),
              height: _banner!.size.height.toDouble(),
              child: AdWidget(ad: _banner!),
            ),
          // ---------------------------------------------
        ],
      ),
      // -----------------------------------------------------------------------------------------------
      // 메모를 추가하는 페이지로 이동
      // --- 배너 광고에 버튼 가려지는 것 수정 ---
      floatingActionButton: Padding(
        padding: EdgeInsets.only(bottom: 50),
        child: FloatingActionButton(
       // ---------------------------------------
          onPressed: () {
            Navigator.of(context).push(MaterialPageRoute(
                builder: (context) => MemoAddApp(reference!)));
          },
          child: Icon(Icons.add),
        ),
      ),
    );
  }

  // --- 페이지가 사라졌을 때 배너도 사라지게 한다 ---
  @override
  void dispose() {
    super.dispose();
    _banner?.dispose();
  }
  // ----------------------------------------------
}

 

[그림 4] 배너 광고 넣기

 

Multidex 이슈 해결

이상의 실습을 하는 과정에서 다음의 에러를 만났다.

[!] App requires Multidex support

 

build.gradle에 Multidex 이슈 해결 위한 코드를 추가하면 된다.

// android/app/build.gradle

defaultConfig {
    applicationId "com.example.flutter_firebase_example"
    minSdkVersion 19
    targetSdkVersion flutter.targetSdkVersion
    versionCode flutterVersionCode.toInteger()
    versionName flutterVersionName
    // --- Multidex 이슈 해결 위해 추가 ---
    multiDexEnabled true
    // ------------------------------------

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    // --------- Multidex 이슈 해결 위해 추가 ----------
    implementation 'com.android.support:multidex:1.0.3'
    // -------------------------------------------------

 

전면 광고 만들기

메시지를 저장할 때 전면 광고가 노출되게 하였다. 전면 광고는 효과는 좋지만 자주 노출되면 사용자에게 피로감을 줄 수 있다.

// memoAdd.dart
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'memo.dart';
// ---------------- 전면 광고 만들기 -----------------------
import 'package:google_mobile_ads/google_mobile_ads.dart';
// -------------------------------------------------------

class MemoAddApp extends StatefulWidget {
  final DatabaseReference reference;

  MemoAddApp(this.reference);

  @override
  State<StatefulWidget> createState() => _MemoAddApp();
}

class _MemoAddApp extends State<MemoAddApp> {
  TextEditingController? titleController;
  TextEditingController? contentController;

  // ----- 전면 광고 만들기: 전면 광고를 사용할 수 있는 준비 ------
  InterstitialAd? _interstitialAd;

  void _createInterstitialAd() {
    InterstitialAd.load(
        adUnitId: InterstitialAd.testAdUnitId,
        request: AdRequest(),
        adLoadCallback: InterstitialAdLoadCallback(
          onAdLoaded: (InterstitialAd ad) {
            print('$ad loaded');
            _interstitialAd = ad;
          },
          onAdFailedToLoad: (LoadAdError error) {
            print('InterstitialAd failed to load: $error.');
            _interstitialAd = null;
          },
        ));
  }
  // ----------------------------------------------------------

  @override
  void initState() {
    super.initState();
    titleController = TextEditingController();
    contentController = TextEditingController();
    // --- 전면 광고 만들기 ---
    _createInterstitialAd(); // 전면 광고를 사용할 수 있는 준비
    // -----------------------
  }

  // --- 전면 광고 만들기: 전면 광고가 노출된 후 콜백을 이용하여 이후의 처리까지 할 수 있도록 한다 ---
  void _showInterstitialAd() {
    if (_interstitialAd == null) {
      return;
    }
    _interstitialAd!.fullScreenContentCallback = FullScreenContentCallback(
      onAdShowedFullScreenContent: (InterstitialAd ad) =>
          print('ad onAdShowedFullScreenContent.'),
      onAdDismissedFullScreenContent: (InterstitialAd ad) {
        print('$ad onAdDismissedFullScreenContent.');
        ad.dispose();
        _createInterstitialAd();
      },
      onAdFailedToShowFullScreenContent: (InterstitialAd ad, AdError error) {
        print('$ad onAdFailedToShowFullScreenContent: $error');
        ad.dispose();
        _createInterstitialAd(); // 전면 광고는 재사용이 어렵기 때문에 _createInterstitialAd로 다시 초기화
      },
    );
    _interstitialAd!.show();
    _interstitialAd = null;
  }
  // ------------------------------------------------------------------------------------------

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('메모 추가'),
      ),
      body: Container(
        padding: EdgeInsets.all(20),
        child: Center(
          child: Column(
            children: <Widget>[
              TextField(
                controller: titleController,
                decoration: InputDecoration(
                    labelText: '제목', fillColor: Colors.blueAccent),
              ),
              Expanded(
                  child: TextField(
                controller: contentController,
                keyboardType: TextInputType.multiline,
                maxLines: 100,
                decoration: InputDecoration(labelText: '내용'),
              )),
              MaterialButton(
                onPressed: () {
                  // reference.push().set() 함수로 데이터 베이스에 저장
                  widget.reference
                      .push()
                      .set(Memo(
                              titleController!.value.text,
                              contentController!.value.text,
                              DateTime.now().toIso8601String())
                          .toJson())
                      .then((_) {
                    Navigator.of(context).pop();
                  });
                  // --- 전면 광고 만들기 ---
                  _showInterstitialAd(); // 전면 광고 호출
                  // -----------------------
                },
                child: Text('저장하기'),
                shape:
                    OutlineInputBorder(borderRadius: BorderRadius.circular(1)),
              )
            ],
          ),
        ),
      ),
    );
  }
}

 

[그림 5] 전면 광고 넣기

 

728x90
반응형