[Flutter] "Do it! 플러터 앱 프로그래밍" - 파이어베이스와 광고 수입 얻기 | 데이터베이스를 이용한 메모장 앱
"조준수. (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),
),
);
}
}
데이터 수정/삭제 기능 추가
// 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),
),
);
}
}
푸시 알림 보내기
패키지 추가
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" 클릭.
앱에 광고 넣어 수익화하기
앱에 광고를 넣어도 광고가 바로 노출되지 않는다. 애드몹에서 광고를 심의하고 기타 내부 처리를 진행하는 데 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();
}
// ----------------------------------------------
}
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)),
)
],
),
),
),
);
}
}