- [Flutter] "Do it! 플러터 앱 프로그래밍" - 오픈 API를 활용한 여행 정보 앱 프로젝트 | 메인, 상세보기, 즐겨찾기, 설정 화면 만들기2022년 02월 27일 00시 57분 41초에 업로드 된 글입니다.작성자: DandyNow728x90반응형
"조준수. (2021). Do it! 플러터 앱 프로그래밍. 이지스퍼블리싱", 15장 오픈 API를 활용한 여행 정보 앱 만들기를 실습했다. 메인, 상세보기, 즐겨찾기, 설정 화면을 각각 만들었다. 이 과정에서 관광 정보 오픈 API와 구글 맵 API를 연동하였다. 그리고 푸시 알림과 배너 광고 기능도 적용하였다.
메인 화면 만들기
메인 화면 기본 골격
// main/mapPage.dart import 'package:flutter/material.dart'; // 메인 화면에서 관광지 목록 구성(관광 정보 오픈 API 연동) class MapPage extends StatefulWidget { @override State<StatefulWidget> createState() => _MapPage(); } class _MapPage extends State<MapPage> { @override Widget build(BuildContext context) { return Scaffold(); } }
// main/favoritePage.dart import 'package:flutter/material.dart'; // 즐겨찾기 화면 class FavoritePage extends StatefulWidget { @override State<StatefulWidget> createState() => _FavoritePage(); } class _FavoritePage extends State<FavoritePage> { @override Widget build(BuildContext context) { return Scaffold(); } }
// main/settingPage.dart import 'package:flutter/material.dart'; // 설정 화면 class SettingPage extends StatefulWidget { @override State<StatefulWidget> createState() => _SettingPage(); } class _SettingPage extends State<SettingPage> { @override Widget build(BuildContext context) { return Scaffold(); } }
// mainPage.dart import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/material.dart'; import 'main/favoritePage.dart'; import 'main/settingPage.dart'; import 'main/mapPage.dart'; // 메인 화면 class MainPage extends StatefulWidget { @override State<StatefulWidget> createState() => _MainPage(); } class _MainPage extends State<MainPage> with SingleTickerProviderStateMixin { TabController? controller; FirebaseDatabase? _database; DatabaseReference? reference; String _databaseURL = '### 데이터베이스 URL ###'; String? id; @override void initState() { super.initState(); controller = TabController(length: 3, vsync: this); _database = FirebaseDatabase(databaseURL: _databaseURL); reference = _database!.reference().child('tour'); } @override void dispose() { controller!.dispose(); super.dispose(); } @override Widget build(BuildContext context) { id = ModalRoute.of(context)!.settings.arguments as String?; // id 변수는 String형이므로 형변환한다. return Scaffold( body: TabBarView( children: <Widget>[ // TabBarView에 채울 위젯들 MapPage(), // 관광 정보 FavoritePage(), // 즐겨찾기 SettingPage() // 설정 ], controller: controller, ), // bottomNavigationBar를 이용해 메인 페이지를 만든다. bottomNavigationBar: TabBar( tabs: <Tab>[ Tab( icon: Icon(Icons.map), ), Tab( icon: Icon(Icons.star), ), Tab( icon: Icon(Icons.settings), ) ], labelColor: Colors.amber, indicatorColor: Colors.deepOrangeAccent, controller: controller, )); } }
// main.dart import 'package:flutter/material.dart'; import 'signPage.dart'; import 'login.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; import 'mainPage.dart'; // 로그인 후 이동할 메인 화면 void main() { WidgetsFlutterBinding.ensureInitialized(); MobileAds.instance.initialize(); runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: '부버의 여행', theme: ThemeData( primarySwatch: Colors.blue, ), initialRoute: '/', routes: { '/': (context) => LoginPage(), '/sign': (context) => SignPage(), '/main': (context) => MainPage(), // 로그인 후 메인 화면으로 이동 }, ); } }
관광 정보 API를 이용해 데이터 불러오기
// data/tour.dart // 관광지 데이터 구성 class TourData { String? title; String? tel; String? zipcode; String? address; var id; var mapx; var mapy; String? imagePath; TourData( {this.id, this.title, this.tel, this.zipcode, this.address, this.mapx, this.mapy, this.imagePath}); TourData.fromJson(Map data) : id = data['contentid'], title = data['title'], tel = data['tel'], zipcode = data['zipcode'], address = data['addr1'], mapx = data['mapx'], mapy = data['mapy'], imagePath = data['firstimage']; Map<String, dynamic> toMap() { return { 'id': id, 'title': title, 'tel': tel, 'zipcode': zipcode, 'address': address, 'mapx': mapx, 'mapy': mapy, 'imagePath': imagePath, }; } }
// main/mapPage.dart import 'package:flutter/material.dart'; import 'dart:convert'; import 'package:firebase_database/firebase_database.dart'; import 'package:http/http.dart' as http; import '/data/tour.dart'; import '/data/listData.dart'; import 'package:sqflite/sqflite.dart'; // 메인 화면에서 관광지 목록 구성(관광 정보 오픈 API 연동) class MapPage extends StatefulWidget { final DatabaseReference? databaseReference; // 실시간 데이터베이스 변수 final Future<Database>? db; // 내부에 저장되는 데이터베이스 final String? id; // 로그인한 아이디 MapPage({this.databaseReference, this.db, this.id}); @override State<StatefulWidget> createState() => _MapPage(); } class _MapPage extends State<MapPage> { List<DropdownMenuItem<Item>> list = List.empty(growable: true); List<DropdownMenuItem<Item>> sublist = List.empty(growable: true); List<TourData> tourData = List.empty(growable: true); ScrollController? _scrollController; // 오픈 API 인증키 String authKey = '### 오픈 API 키(일반 인증키) ###'; Item? area; Item? kind; int page = 1; @override void initState() { super.initState(); list = Area().seoulArea; // 지역 목록을 list에 저장 sublist = Kind().kinds; // 관광지 종류를 sublist에 저장 area = list[0].value; kind = sublist[0].value; _scrollController = new ScrollController(); _scrollController!.addListener(() { if (_scrollController!.offset >= _scrollController!.position.maxScrollExtent && !_scrollController!.position.outOfRange) { page++; getAreaList(area: area!.value, contentTypeId: kind!.value, page: page); } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('검색하기'), ), body: Container( child: Center( child: Column( children: <Widget>[ Row( children: <Widget>[ DropdownButton<Item>( value: area, onChanged: (value) { Item selectedItem = value!; setState(() { area = selectedItem; }); }, items: list, ), SizedBox( width: 10, ), DropdownButton<Item>( items: sublist, onChanged: (value) { Item selectedItem = value!; setState(() { kind = selectedItem; }); }, value: kind, ), SizedBox( width: 10, ), MaterialButton( onPressed: () { page = 1; tourData.clear(); getAreaList( area: area!.value, contentTypeId: kind!.value, page: page); }, child: Text( '검색하기', style: TextStyle(color: Colors.white), ), color: Colors.blueAccent, ) ], mainAxisAlignment: MainAxisAlignment.spaceAround, ), Expanded( child: ListView.builder( itemBuilder: (context, index) { return Card( child: InkWell( child: Row( children: <Widget>[ // 아이템을 클릭했을 때 애니메이션 효과로 이어지면서 관광지 상세보기 페이지로 넘어간다. Hero( tag: 'tourinfo$index', child: Container( margin: EdgeInsets.all(10), width: 100.0, height: 100.0, // 원형, 검은색 테두리 decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: Colors.black, width: 1), image: DecorationImage( fit: BoxFit.fill, image: getImage( tourData[index].imagePath))))), SizedBox( width: 20, ), Container( child: Column( children: <Widget>[ Text( tourData[index].title!, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold), ), Text('주소 : ${tourData[index].address}'), tourData[index].tel != null ? Text('전화 번호 : ${tourData[index].tel}') : Container(), ], mainAxisAlignment: MainAxisAlignment.spaceEvenly, ), width: MediaQuery.of(context).size.width - 150, ) ], ), onTap: () {}, ), ); }, itemCount: tourData.length, controller: _scrollController, )) ], mainAxisAlignment: MainAxisAlignment.start, ), ), ), ); } ImageProvider getImage(String? imagePath) { if (imagePath != null) { return NetworkImage(imagePath); } else { return AssetImage('repo/images/map_location.png'); } } void getAreaList( {required int area, required int contentTypeId, required int page}) async { var url = 'http://api.visitkorea.or.kr/openapi/service/rest/KorService/areaBasedList?ServiceKey=$authKey&MobileOS=AND&MobileApp=ModuTour&_type=json&areaCode=1&numOfRows=10&sigunguCode=$area&pageNo=$page'; if (contentTypeId != 0) { url = url + '&contentTypeId=$contentTypeId'; } var response = await http.get(Uri.parse(url)); String body = utf8.decode(response.bodyBytes); print(body); var json = jsonDecode(body); if (json['response']['header']['resultCode'] == "0000") { if (json['response']['body']['items'] == '') { showDialog( context: context, builder: (context) { return AlertDialog( content: Text('마지막 데이터입니다'), ); }); } else { List jsonArray = json['response']['body']['items']['item']; for (var s in jsonArray) { setState(() { tourData.add(TourData.fromJson(s)); }); } } } else { print('error'); } } }
// data/listData.dart import 'package:flutter/material.dart'; // 관광지 지역과 종류 데이터 구성 class Item { String title; int value; Item(this.title, this.value); } class Area { List<DropdownMenuItem<Item>> seoulArea = List.empty(growable: true); Area() { seoulArea.add(DropdownMenuItem( child: Text('강남구'), value: Item('강남구', 1), )); seoulArea.add(DropdownMenuItem( child: Text('강동구'), value: Item('강동구', 2), )); seoulArea.add(DropdownMenuItem( child: Text('강북구'), value: Item('강북구', 3), )); seoulArea.add(DropdownMenuItem( child: Text('강서구'), value: Item('강서구', 4), )); seoulArea.add(DropdownMenuItem( child: Text('관악구'), value: Item('관악구', 5), )); seoulArea.add(DropdownMenuItem( child: Text('광진구'), value: Item('광진구', 6), )); seoulArea.add(DropdownMenuItem( child: Text('구로구'), value: Item('구로구', 7), )); seoulArea.add(DropdownMenuItem( child: Text('금천구'), value: Item('금천구', 8), )); seoulArea.add(DropdownMenuItem( child: Text('노원구'), value: Item('노원구', 9), )); seoulArea.add(DropdownMenuItem( child: Text('도봉구'), value: Item('도봉구', 10), )); seoulArea.add(DropdownMenuItem( child: Text('동대문구'), value: Item('동대문구', 11), )); seoulArea.add(DropdownMenuItem( child: Text('동작구'), value: Item('동작구', 12), )); seoulArea.add(DropdownMenuItem( child: Text('마포구'), value: Item('마포구', 13), )); seoulArea.add(DropdownMenuItem( child: Text('서대문구'), value: Item('서대문구', 14), )); seoulArea.add(DropdownMenuItem( child: Text('서초구'), value: Item('서초구', 15), )); seoulArea.add(DropdownMenuItem( child: Text('성동구'), value: Item('성동구', 16), )); seoulArea.add(DropdownMenuItem( child: Text('성북구'), value: Item('성북구', 17), )); seoulArea.add(DropdownMenuItem( child: Text('송파구'), value: Item('송파구', 18), )); seoulArea.add(DropdownMenuItem( child: Text('양천구'), value: Item('양천구', 19), )); seoulArea.add(DropdownMenuItem( child: Text('영등포구'), value: Item('영등포구', 20), )); seoulArea.add(DropdownMenuItem( child: Text('용산구'), value: Item('용산구', 21), )); seoulArea.add(DropdownMenuItem( child: Text('은평구'), value: Item('은평구', 22), )); seoulArea.add(DropdownMenuItem( child: Text('종로구'), value: Item('종로구', 23), )); seoulArea.add(DropdownMenuItem( child: Text('중구'), value: Item('중구', 24), )); seoulArea.add(DropdownMenuItem( child: Text('중랑구'), value: Item('중랑구', 25), )); print(seoulArea.length); } } class Kind { List<DropdownMenuItem<Item>> kinds = List.empty(growable: true); Kind() { kinds.add(DropdownMenuItem( child: Text('관광지'), value: Item('관광지', 12), )); kinds.add(DropdownMenuItem( child: Text('문화시설'), value: Item('문화시설', 14), )); kinds.add(DropdownMenuItem( child: Text('축제/공연'), value: Item('축제/공연', 15), )); kinds.add(DropdownMenuItem( child: Text('여행코스'), value: Item('여행코스', 25), )); kinds.add(DropdownMenuItem( child: Text('레포츠'), value: Item('레포츠', 28), )); kinds.add(DropdownMenuItem( child: Text('숙박'), value: Item('숙박', 32), )); kinds.add(DropdownMenuItem( child: Text('쇼핑'), value: Item('쇼핑', 38), )); kinds.add(DropdownMenuItem( child: Text('음식'), value: Item('음식', 39), )); kinds.add(DropdownMenuItem( child: Text('전체'), value: Item('전체', 0), )); } }
에러 처리
첫 빌드시 로그인 화면은 정상적으로 작동했다. 하지만 "검색하기" 버튼을 눌렀을 때 앱이 멈췄다. lib/src/painting/image_provider.dart에서 에러가 발생한 것이다. main/mapPage.dart의 AssetImage의 경로에 해당하는 'repo/images/map_location.png'가 없어서 생긴 문제로 보고 해당 폴더와 이미지를 넣어 준 후 pubspec.yaml에서 assets을 설정해주니 정상 작동하였다.
상세보기 화면 만들기 - 구글 지도 넣기
구글 맵 API 키 얻기
https://cloud.google.com/maps-platform 에서 API 키를 발급받는다.
# pubspec.yaml dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 firebase_core: ^1.3.0 firebase_analytics: ^8.1.2 firebase_database: ^7.1.1 firebase_messaging: ^10.0.1 google_mobile_ads: ^0.13.0 http: ^0.13.1 sqflite: ^2.0.0+3 path: ^1.8.0 shared_preferences: ^2.0.5 crypto: ^3.0.1 # -------------------------- # 구글 맵 API 연동 google_maps_flutter: ^2.0.3 # --------------------------
<!-- android/app/src/main/AndroidManifest.xml --> <meta-data android:name="com.google.android.geo.API_KEY" android:value="### 구글 맵 API 키 ###"/>
// main/mapPage.dart import 'package:flutter/material.dart'; import 'dart:convert'; import 'package:firebase_database/firebase_database.dart'; import 'package:http/http.dart' as http; import '/data/tour.dart'; import '/data/listData.dart'; import 'package:sqflite/sqflite.dart'; // -------------------------- // 구글 맵 API 연동 import 'tourDetailPage.dart'; // -------------------------- // 메인 화면에서 관광지 목록 구성(관광 정보 오픈 API 연동) class MapPage extends StatefulWidget { final DatabaseReference? databaseReference; // 실시간 데이터베이스 변수 final Future<Database>? db; // 내부에 저장되는 데이터베이스 final String? id; // 로그인한 아이디 MapPage({this.databaseReference, this.db, this.id}); @override State<StatefulWidget> createState() => _MapPage(); } class _MapPage extends State<MapPage> { List<DropdownMenuItem<Item>> list = List.empty(growable: true); List<DropdownMenuItem<Item>> sublist = List.empty(growable: true); List<TourData> tourData = List.empty(growable: true); ScrollController? _scrollController; // 오픈 API 인증키 String authKey = '### 오픈 API 키(일반 인증키) ###'; Item? area; Item? kind; int page = 1; @override void initState() { super.initState(); list = Area().seoulArea; // 지역 목록을 list에 저장 sublist = Kind().kinds; // 관광지 종류를 sublist에 저장 area = list[0].value; kind = sublist[0].value; _scrollController = new ScrollController(); _scrollController!.addListener(() { if (_scrollController!.offset >= _scrollController!.position.maxScrollExtent && !_scrollController!.position.outOfRange) { page++; getAreaList(area: area!.value, contentTypeId: kind!.value, page: page); } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('검색하기'), ), body: Container( child: Center( child: Column( children: <Widget>[ Row( children: <Widget>[ DropdownButton<Item>( value: area, onChanged: (value) { Item selectedItem = value!; setState(() { area = selectedItem; }); }, items: list, ), SizedBox( width: 10, ), DropdownButton<Item>( items: sublist, onChanged: (value) { Item selectedItem = value!; setState(() { kind = selectedItem; }); }, value: kind, ), SizedBox( width: 10, ), MaterialButton( onPressed: () { page = 1; tourData.clear(); getAreaList( area: area!.value, contentTypeId: kind!.value, page: page); }, child: Text( '검색하기', style: TextStyle(color: Colors.white), ), color: Colors.blueAccent, ) ], mainAxisAlignment: MainAxisAlignment.spaceAround, ), Expanded( child: ListView.builder( itemBuilder: (context, index) { return Card( child: InkWell( child: Row( children: <Widget>[ // 아이템을 클릭했을 때 애니메이션 효과로 이어지면서 관광지 상세보기 페이지로 넘어간다. Hero( tag: 'tourinfo$index', child: Container( margin: EdgeInsets.all(10), width: 100.0, height: 100.0, // 원형, 검은색 테두리 decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: Colors.black, width: 1), image: DecorationImage( fit: BoxFit.fill, image: getImage( tourData[index].imagePath))))), SizedBox( width: 20, ), Container( child: Column( children: <Widget>[ Text( tourData[index].title!, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold), ), Text('주소 : ${tourData[index].address}'), tourData[index].tel != null ? Text('전화 번호 : ${tourData[index].tel}') : Container(), ], mainAxisAlignment: MainAxisAlignment.spaceEvenly, ), width: MediaQuery.of(context).size.width - 150, ) ], ), // ----------------------------------------------------- // 구글 맵 API 연동 onTap: () { Navigator.of(context).push(MaterialPageRoute( builder: (context) => TourDetailPage( id: widget.id, tourData: tourData[index], index: index, databaseReference: widget.databaseReference, ))); // ----------------------------------------------------- }, ), ); }, itemCount: tourData.length, controller: _scrollController, )) ], mainAxisAlignment: MainAxisAlignment.start, ), ), ), ); } ImageProvider getImage(String? imagePath) { if (imagePath != null) { return NetworkImage(imagePath); } else { return AssetImage('repo/images/map_location.png'); } } void getAreaList( {required int area, required int contentTypeId, required int page}) async { var url = 'http://api.visitkorea.or.kr/openapi/service/rest/KorService/areaBasedList?ServiceKey=$authKey&MobileOS=AND&MobileApp=ModuTour&_type=json&areaCode=1&numOfRows=10&sigunguCode=$area&pageNo=$page'; if (contentTypeId != 0) { url = url + '&contentTypeId=$contentTypeId'; } var response = await http.get(Uri.parse(url)); String body = utf8.decode(response.bodyBytes); print(body); var json = jsonDecode(body); if (json['response']['header']['resultCode'] == "0000") { if (json['response']['body']['items'] == '') { showDialog( context: context, builder: (context) { return AlertDialog( content: Text('마지막 데이터입니다'), ); }); } else { List jsonArray = json['response']['body']['items']['item']; for (var s in jsonArray) { setState(() { tourData.add(TourData.fromJson(s)); }); } } } else { print('error'); } } }
// data/reviews.dart import 'package:firebase_database/firebase_database.dart'; // 후기 데이터 구성 class Review { String id; String review; String createTime; Review(this.id, this.review, this.createTime); Review.fromSnapshot(DataSnapshot snapshot) : id = snapshot.value['id'], review = snapshot.value['review'], createTime = snapshot.value['createTime']; toJson() { return { 'id': id, 'review': review, 'createTime': createTime, }; } }
// data/disableInfo.dart import 'package:firebase_database/firebase_database.dart'; // 장애인 이용 정보 데이터 구성 class DisableInfo { String? key; int? disable1; int? disable2; String? id; String? createTime; DisableInfo(this.id, this.disable1, this.disable2, this.createTime); DisableInfo.fromSnapshot(DataSnapshot snapshot) : key = snapshot.key, id = snapshot.value['id'], disable1 = snapshot.value['disable1'], disable2 = snapshot.value['disable2'], createTime = snapshot.value['createTime']; toJson() { return { 'id': id, 'disable1': disable1, 'disable2': disable2, 'createTime': createTime, }; } }
// main/tourDetailPage.dart import 'dart:async'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import '/data/disableInfo.dart'; import '/data/reviews.dart'; import 'dart:math' as math; import '/data/tour.dart'; // 관광지 상세보기 화면(구글 맵 API 연동) class TourDetailPage extends StatefulWidget { final TourData? tourData; final int? index; final DatabaseReference? databaseReference; final String? id; TourDetailPage({this.tourData, this.index, this.databaseReference, this.id}); @override State<StatefulWidget> createState() => _TourDetailPage(); } class _TourDetailPage extends State<TourDetailPage> { Completer<GoogleMapController> _controller = Completer(); Map<MarkerId, Marker> markers = <MarkerId, Marker>{}; CameraPosition? _GoogleMapCamera; TextEditingController? _reviewTextController; Marker? marker; List<Review> reviews = List.empty(growable: true); bool _disableWidget = false; DisableInfo? _disableInfo; double disableCheck1 = 0; double disableCheck2 = 0; @override void initState() { super.initState(); widget.databaseReference! .child('tour') .child(widget.tourData!.id.toString()) .child('review') .onChildAdded .listen((event) { if (event.snapshot.value != null) { setState(() { reviews.add(Review.fromSnapshot(event.snapshot)); }); } }); _reviewTextController = TextEditingController(); // --------------------------------------------------------------- // 지도에서 관광지 위치를 표시하기 위한 변수 _GoogleMapCamera = CameraPosition( target: LatLng(double.parse(widget.tourData!.mapy.toString()), double.parse(widget.tourData!.mapx.toString())), zoom: 16, ); // --------------------------------------------------------------- MarkerId markerId = MarkerId(widget.tourData.hashCode.toString()); marker = Marker( position: LatLng(double.parse(widget.tourData!.mapy.toString()), double.parse(widget.tourData!.mapx.toString())), flat: true, markerId: markerId); markers[markerId] = marker!; getDisableInfo(); } @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( slivers: <Widget>[ SliverAppBar( expandedHeight: 150, flexibleSpace: FlexibleSpaceBar( title: Text( '${widget.tourData!.title}', style: TextStyle(color: Colors.white, fontSize: 40), ), centerTitle: true, titlePadding: EdgeInsets.only(top: 10), ), pinned: true, backgroundColor: Colors.deepOrangeAccent, ), SliverList( delegate: SliverChildListDelegate([ SizedBox( height: 20, ), Container( child: Center( child: Column( children: <Widget>[ Hero( tag: 'tourinfo${widget.index}', child: Container( width: 300.0, height: 300.0, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Colors.black, width: 1), image: DecorationImage( fit: BoxFit.fill, image: getImage(widget.tourData!.imagePath), )))), Padding( padding: EdgeInsets.only(top: 20, bottom: 20), child: Text( widget.tourData!.address!, style: TextStyle(fontSize: 20), ), ), getGoogleMap(), _disableWidget == false ? setDisableWidget() : showDisableWidget(), // reviewWidget() ], ), ), ), ])), SliverPersistentHeader( delegate: _HeaderDelegate( minHeight: 50, maxHeight: 100, child: Container( color: Colors.lightBlueAccent, child: Center( child: Column( children: <Widget>[ Text( '후기', style: TextStyle(fontSize: 30, color: Colors.white), ), ], mainAxisAlignment: MainAxisAlignment.center, ), ), )), pinned: true, ), SliverList( delegate: SliverChildBuilderDelegate((context, index) { return Card( child: InkWell( child: Padding( padding: EdgeInsets.only(top: 10, bottom: 10, left: 10), child: Text( '${reviews[index].id} : ${reviews[index].review}', style: TextStyle(fontSize: 15), ), ), onDoubleTap: () { if (reviews[index].id == widget.id) { widget.databaseReference! .child('tour') .child(widget.tourData!.id.toString()) .child('review') .child(widget.id!) .remove(); setState(() { reviews.removeAt(index); }); } }, ), ); }, childCount: reviews.length)), SliverList( delegate: SliverChildListDelegate([ MaterialButton( onPressed: () { showDialog( context: context, builder: (context) { return AlertDialog( title: Text('후기 쓰기'), content: TextField( controller: _reviewTextController, ), actions: <Widget>[ MaterialButton( onPressed: () { Review review = Review( widget.id!, _reviewTextController!.value.text, DateTime.now().toIso8601String()); widget.databaseReference! .child('tour') .child(widget.tourData!.id.toString()) .child('review') .child(widget.id!) .set(review.toJson()); }, child: Text('후기 쓰기')), MaterialButton( onPressed: () { Navigator.of(context).pop(); }, child: Text('종료하기')), ], ); }); }, child: Text('댓글 쓰기'), ) ])) ], ), ); } getDisableInfo() { widget.databaseReference! .child('tour') .child(widget.tourData!.id.toString()) .onValue .listen((event) { _disableInfo = DisableInfo.fromSnapshot(event.snapshot); if (_disableInfo!.id == null) { setState(() { _disableWidget = false; }); } else { setState(() { _disableWidget = true; }); } }); } ImageProvider getImage(String? imagePath) { if (imagePath != null) { return NetworkImage(imagePath); } else { return AssetImage('repo/images/map_location.png'); } } Widget setDisableWidget() { return Container( child: Center( child: Column( children: <Widget>[ Text('데이터가 없습니다. 추가해주세요'), Text('시각 장애인 이용 점수 : ${disableCheck1.floor()}'), Padding( padding: EdgeInsets.all(20), child: Slider( value: disableCheck1, min: 0, max: 10, onChanged: (value) { setState(() { disableCheck1 = value; }); }), ), Text('지체 장애인 이용 점수 : ${disableCheck2.floor()}'), Padding( padding: EdgeInsets.all(20), child: Slider( value: disableCheck2, min: 0, max: 10, onChanged: (value) { setState(() { disableCheck2 = value; }); }), ), MaterialButton( onPressed: () { DisableInfo info = DisableInfo(widget.id, disableCheck1.floor(), disableCheck2.floor(), DateTime.now().toIso8601String()); widget.databaseReference! .child("tour") .child(widget.tourData!.id.toString()) .set(info.toJson()) .then((value) { setState(() { _disableWidget = true; }); }); }, child: Text('데이터 저장하기'), ) ], ), ), ); } getGoogleMap() { return SizedBox( height: 400, width: MediaQuery.of(context).size.width - 50, // ---------------------------------------------------------- // 지도를 표시하는 함수 child: GoogleMap( // <MapType 설정값> // none : 지도를 표시하지 않음 // normal : 일반적인 지도 표시 // satellite : 위성 지도 표시 // terrain : 3D 지도 표시 // hybrid : 위성 지도와 일반적인 지도를 함께 표시 mapType: MapType.normal, // initialCameraPosition는 지도에 표시할 위치 설정 initialCameraPosition: _GoogleMapCamera!, onMapCreated: (GoogleMapController controller) { _controller.complete(controller); }, markers: Set<Marker>.of(markers.values)), // ---------------------------------------------------------- ); } showDisableWidget() { return Center( child: Column( children: <Widget>[ Row( children: <Widget>[ Icon(Icons.accessible, size: 40, color: Colors.orange), Text( '지체 장애 이용 점수 : ${_disableInfo!.disable2}', style: TextStyle(fontSize: 20), ) ], mainAxisAlignment: MainAxisAlignment.spaceEvenly, ), SizedBox( height: 20, ), Row( children: <Widget>[ Icon( Icons.remove_red_eye, size: 40, color: Colors.orange, ), Text('시각 장애 이용 점수 : ${_disableInfo!.disable1}', style: TextStyle(fontSize: 20)) ], mainAxisAlignment: MainAxisAlignment.spaceEvenly, ), SizedBox( height: 20, ), Text('작성자 : ${_disableInfo!.id}'), SizedBox( height: 20, ), MaterialButton( onPressed: () { setState(() { _disableWidget = false; }); }, child: Text('새로 작성하기'), ) ], ), ); } } class _HeaderDelegate extends SliverPersistentHeaderDelegate { final double? minHeight; final double? maxHeight; final Widget? child; _HeaderDelegate({ @required this.minHeight, @required this.maxHeight, @required this.child, }); @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { return SizedBox.expand(child: child); } @override double get maxExtent => math.max(maxHeight!, minHeight!); @override double get minExtent => minHeight!; @override bool shouldRebuild(_HeaderDelegate oldDelegate) { return maxHeight != oldDelegate.maxHeight || minHeight != oldDelegate.minHeight || child != oldDelegate.child; } }
// mainPage.dart import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/material.dart'; import 'main/favoritePage.dart'; import 'main/settingPage.dart'; import 'main/mapPage.dart'; // 메인 화면 class MainPage extends StatefulWidget { @override State<StatefulWidget> createState() => _MainPage(); } class _MainPage extends State<MainPage> with SingleTickerProviderStateMixin { TabController? controller; FirebaseDatabase? _database; DatabaseReference? reference; String _databaseURL = '### 데이터베이스 URL ###'; String? id; @override void initState() { super.initState(); controller = TabController(length: 3, vsync: this); _database = FirebaseDatabase(databaseURL: _databaseURL); reference = _database!.reference().child('tour'); } @override void dispose() { controller!.dispose(); super.dispose(); } @override Widget build(BuildContext context) { id = ModalRoute.of(context)!.settings.arguments as String?; // id 변수는 String형이므로 형변환한다. return Scaffold( body: TabBarView( children: <Widget>[ // TabBarView에 채울 위젯들 MapPage( // ---------------------------------- // MapPage 함수를 호출하는 부분에 추가 databaseReference: reference, id: id, // ---------------------------------- ), // 관광 정보 FavoritePage(), // 즐겨찾기 SettingPage() // 설정 ], controller: controller, ), // bottomNavigationBar를 이용해 메인 페이지를 만든다. bottomNavigationBar: TabBar( tabs: <Tab>[ Tab( icon: Icon(Icons.map), ), Tab( icon: Icon(Icons.star), ), Tab( icon: Icon(Icons.settings), ) ], labelColor: Colors.amber, indicatorColor: Colors.deepOrangeAccent, controller: controller, )); } }
즐겨찾기 화면 만들기
즐겨찾기 설정하고 해제하기
// main.dart import 'package:flutter/material.dart'; import 'signPage.dart'; import 'login.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; import 'mainPage.dart'; // 로그인 후 이동할 메인 화면 // ------------------------------------ // 즐겨찾기 import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; // ------------------------------------ void main() { WidgetsFlutterBinding.ensureInitialized(); MobileAds.instance.initialize(); runApp(MyApp()); } class MyApp extends StatelessWidget { // ------------------------------------------------------------------------- // 즐겨찾기 Future<Database> initDatabase() async { return openDatabase( join(await getDatabasesPath(), 'tour_database.db'), onCreate: (db, version) { return db.execute( "CREATE TABLE place(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, tel TEXT , zipcode TEXT , address TEXT , mapx Number , mapy Number , imagePath TEXT)", ); }, version: 1, ); } // ------------------------------------------------------------------------- @override Widget build(BuildContext context) { // --------------------------------------------------------------- // 즐겨찾기 Future<Database> database = initDatabase(); // build 할때 initDatabase() 함수를 호출합니다 // --------------------------------------------------------------- return MaterialApp( title: '부버의 여행', theme: ThemeData( primarySwatch: Colors.blue, ), initialRoute: '/', routes: { '/': (context) => LoginPage(), '/sign': (context) => SignPage(), '/main': (context) => MainPage(database), // 로그인 후 메인 화면으로 이동 }, ); } }
// mainPage.dart import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/material.dart'; import 'main/favoritePage.dart'; import 'main/settingPage.dart'; import 'main/mapPage.dart'; // ------------------------------------ // 즐겨찾기 import 'package:sqflite/sqflite.dart'; // ------------------------------------ // 메인 화면 class MainPage extends StatefulWidget { // ------------------------------ // 즐겨찾기 final Future<Database> database; MainPage(this.database); // ------------------------------ @override State<StatefulWidget> createState() => _MainPage(); } class _MainPage extends State<MainPage> with SingleTickerProviderStateMixin { TabController? controller; FirebaseDatabase? _database; DatabaseReference? reference; String _databaseURL = '### 데이터베이스 URL ###'; String? id; @override void initState() { super.initState(); controller = TabController(length: 3, vsync: this); _database = FirebaseDatabase(databaseURL: _databaseURL); reference = _database!.reference().child('tour'); } @override void dispose() { controller!.dispose(); super.dispose(); } @override Widget build(BuildContext context) { id = ModalRoute.of(context)!.settings.arguments as String?; // id 변수는 String형이므로 형변환한다. return Scaffold( body: TabBarView( children: <Widget>[ // TabBarView에 채울 위젯들 MapPage( // ---------------------------------- // MapPage 함수를 호출하는 부분에 추가 databaseReference: reference!, // ----------------- // 즐겨찾기 db: widget.database, // ----------------- id: id, // ---------------------------------- ), // 관광 정보 FavoritePage( // --------------------------- // 즐겨찾기 databaseReference: reference!, db: widget.database, id: id!, // --------------------------- ), // 즐겨찾기 SettingPage() // 설정 ], controller: controller, ), // bottomNavigationBar를 이용해 메인 페이지를 만든다. bottomNavigationBar: TabBar( tabs: <Tab>[ Tab( icon: Icon(Icons.map), ), Tab( icon: Icon(Icons.star), ), Tab( icon: Icon(Icons.settings), ) ], labelColor: Colors.amber, indicatorColor: Colors.deepOrangeAccent, controller: controller, )); } }
// mapPage.dart import 'package:flutter/material.dart'; import 'dart:convert'; import 'package:firebase_database/firebase_database.dart'; import 'package:http/http.dart' as http; import '/data/tour.dart'; import '/data/listData.dart'; import 'package:sqflite/sqflite.dart'; // -------------------------- // 구글맵 API 연동 import 'tourDetailPage.dart'; // -------------------------- // 메인 화면에서 관광지 목록 구성(관광 정보 오픈 API 연동) class MapPage extends StatefulWidget { final DatabaseReference? databaseReference; // 실시간 데이터베이스 변수 final Future<Database>? db; // 내부에 저장되는 데이터베이스 final String? id; // 로그인한 아이디 MapPage({this.databaseReference, this.db, this.id}); @override State<StatefulWidget> createState() => _MapPage(); } class _MapPage extends State<MapPage> { List<DropdownMenuItem<Item>> list = List.empty(growable: true); List<DropdownMenuItem<Item>> sublist = List.empty(growable: true); List<TourData> tourData = List.empty(growable: true); ScrollController? _scrollController; // 오픈 API 인증키 String authKey = '### 오픈 API 키(일반 인증키) ###'; Item? area; Item? kind; int page = 1; @override void initState() { super.initState(); list = Area().seoulArea; // 지역 목록을 list에 저장 sublist = Kind().kinds; // 관광지 종류를 sublist에 저장 area = list[0].value; kind = sublist[0].value; _scrollController = new ScrollController(); _scrollController!.addListener(() { if (_scrollController!.offset >= _scrollController!.position.maxScrollExtent && !_scrollController!.position.outOfRange) { page++; getAreaList(area: area!.value, contentTypeId: kind!.value, page: page); } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('검색하기'), ), body: Container( child: Center( child: Column( children: <Widget>[ Row( children: <Widget>[ DropdownButton<Item>( value: area, onChanged: (value) { Item selectedItem = value!; setState(() { area = selectedItem; }); }, items: list, ), SizedBox( width: 10, ), DropdownButton<Item>( items: sublist, onChanged: (value) { Item selectedItem = value!; setState(() { kind = selectedItem; }); }, value: kind, ), SizedBox( width: 10, ), MaterialButton( onPressed: () { page = 1; tourData.clear(); getAreaList( area: area!.value, contentTypeId: kind!.value, page: page); }, child: Text( '검색하기', style: TextStyle(color: Colors.white), ), color: Colors.blueAccent, ) ], mainAxisAlignment: MainAxisAlignment.spaceAround, ), Expanded( child: ListView.builder( itemBuilder: (context, index) { return Card( child: InkWell( child: Row( children: <Widget>[ // 아이템을 클릭했을 때 애니메이션 효과로 이어지면서 관광지 상세보기 페이지로 넘어간다. Hero( tag: 'tourinfo$index', child: Container( margin: EdgeInsets.all(10), width: 100.0, height: 100.0, // 원형, 검은색 테두리 decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: Colors.black, width: 1), image: DecorationImage( fit: BoxFit.fill, image: getImage( tourData[index].imagePath))))), SizedBox( width: 20, ), Container( child: Column( children: <Widget>[ Text( tourData[index].title!, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold), ), Text('주소 : ${tourData[index].address}'), tourData[index].tel != null ? Text('전화 번호 : ${tourData[index].tel}') : Container(), ], mainAxisAlignment: MainAxisAlignment.spaceEvenly, ), width: MediaQuery.of(context).size.width - 150, ) ], ), // ----------------------------------------------------- // 구글 맵 API 연동 onTap: () { Navigator.of(context).push(MaterialPageRoute( builder: (context) => TourDetailPage( id: widget.id, tourData: tourData[index], index: index, databaseReference: widget.databaseReference, ))); // ----------------------------------------------------- }, // --------------------------------------- // 즐겨찾기 // 관광지를 더블클릭하면 즐겨찾기 설정 onDoubleTap: () { insertTour(widget.db!, tourData[index]); }, // --------------------------------------- ), ); }, itemCount: tourData.length, controller: _scrollController, )) ], mainAxisAlignment: MainAxisAlignment.start, ), ), ), ); } // ----------------------------------------------------------- // 즐겨찾기 void insertTour(Future<Database> db, TourData info) async { final Database database = await db; await database .insert('place', info.toMap(), conflictAlgorithm: ConflictAlgorithm.replace) .then((value) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('즐겨찾기에 추가되었습니다'))); }); } // ----------------------------------------------------------- ImageProvider getImage(String? imagePath) { if (imagePath != null) { return NetworkImage(imagePath); } else { return AssetImage('repo/images/map_location.png'); } } void getAreaList( {required int area, required int contentTypeId, required int page}) async { var url = 'http://api.visitkorea.or.kr/openapi/service/rest/KorService/areaBasedList?ServiceKey=$authKey&MobileOS=AND&MobileApp=ModuTour&_type=json&areaCode=1&numOfRows=10&sigunguCode=$area&pageNo=$page'; if (contentTypeId != 0) { url = url + '&contentTypeId=$contentTypeId'; } var response = await http.get(Uri.parse(url)); String body = utf8.decode(response.bodyBytes); print(body); var json = jsonDecode(body); if (json['response']['header']['resultCode'] == "0000") { if (json['response']['body']['items'] == '') { showDialog( context: context, builder: (context) { return AlertDialog( content: Text('마지막 데이터 입니다'), ); }); } else { List jsonArray = json['response']['body']['items']['item']; for (var s in jsonArray) { setState(() { tourData.add(TourData.fromJson(s)); }); } } } else { print('error'); } } }
// main/favoritePage.dart import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/material.dart'; import '/data/tour.dart'; import '/main/tourDetailPage.dart'; import 'package:sqflite/sqflite.dart'; // 즐겨찾기 화면 class FavoritePage extends StatefulWidget { final DatabaseReference? databaseReference; final Future<Database>? db; final String? id; FavoritePage({this.databaseReference, this.db, this.id}); @override State<StatefulWidget> createState() => _FavoritePage(); } class _FavoritePage extends State<FavoritePage> { Future<List<TourData>>? _tourList; @override void initState() { super.initState(); _tourList = getTodos(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('즐겨찾기'), ), body: Container( child: Center( child: FutureBuilder( builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: return CircularProgressIndicator(); case ConnectionState.waiting: return CircularProgressIndicator(); case ConnectionState.active: return CircularProgressIndicator(); case ConnectionState.done: if (snapshot.hasData) { return ListView.builder( itemBuilder: (context, index) { List<TourData> tourList = snapshot.data as List<TourData>; TourData info = tourList[index]; return Card( child: InkWell( child: Row( children: <Widget>[ Hero( tag: 'tourinfo$index', child: Container( margin: EdgeInsets.all(10), width: 100.0, height: 100.0, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: Colors.black, width: 1), image: DecorationImage( fit: BoxFit.fill, image: getImage( info.imagePath))))), SizedBox( width: 20, ), Container( child: Column( children: <Widget>[ Text( info.title!, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold), ), Text('주소 : ${info.address}'), info.tel != 'null' ? Text('전화 번호 : ${info.tel}') : Container(), ], mainAxisAlignment: MainAxisAlignment.spaceEvenly, ), width: MediaQuery.of(context).size.width - 150, ) ], ), onTap: () { // 상세페이지 이동은 TourDetailPage를 재사용하도록 합니다 Navigator.of(context).push(MaterialPageRoute( builder: (context) => TourDetailPage( id: widget.id, tourData: info, index: index, databaseReference: widget.databaseReference, ))); }, onDoubleTap: () { deleteTour(widget.db!, info); }, ), ); }, itemCount: (snapshot.data! as List<TourData>).length, ); } else { return Text('No data'); } } return CircularProgressIndicator(); }, future: _tourList, ), ), ), ); } ImageProvider getImage(String? imagePath) { if (imagePath != null) { return NetworkImage(imagePath); } else { return AssetImage('repo/images/map_location.png'); } } void deleteTour(Future<Database> db, TourData info) async { final Database database = await db; await database.delete('place', where: 'title=?', whereArgs: [info.title]).then((value) { setState(() { _tourList = getTodos(); }); ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('즐겨찾기를 해제합니다'))); }); } Future<List<TourData>> getTodos() async { final Database database = await widget.db!; final List<Map<String, dynamic>> maps = await database.query('place'); return List.generate(maps.length, (i) { return TourData( title: maps[i]['title'].toString(), tel: maps[i]['tel'].toString(), address: maps[i]['address'].toString(), zipcode: maps[i]['zipcode'].toString(), mapy: maps[i]['mapy'].toString(), mapx: maps[i]['mapx'].toString(), imagePath: maps[i]['imagePath'].toString()); }); } }
설정 화면 만들기
푸시 알림 수신 여부 설정하기
푸시 알림 수신 여부를 SharedPreferences에 저장하고 로그아웃과 회원 탈퇴 기능을 구현한다.
// main/settingPage.dart import 'dart:io'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; // 설정화면 class SettingPage extends StatefulWidget { final DatabaseReference? databaseReference; final String? id; SettingPage({this.databaseReference, this.id}); @override State<StatefulWidget> createState() => _SettingPage(); } class _SettingPage extends State<SettingPage> { bool pushCheck = true; @override void initState() { super.initState(); _loadData(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('설정하기'), ), body: Container( child: Center( child: Column( children: <Widget>[ Row( children: <Widget>[ Text( '푸시 알림', style: TextStyle(fontSize: 20), ), Switch( value: pushCheck, onChanged: (value) { setState(() { pushCheck = value; }); _setData(value); }) ], mainAxisAlignment: MainAxisAlignment.spaceAround, ), SizedBox( height: 50, ), ElevatedButton( onPressed: () { // 로그아웃은 보통 서버와 통신하는 코드로 구현하지만 지금은 서버가 없으므로 네비게이터의 모든 스택을 지우고 홈으로 이동한다. Navigator.of(context).pushNamedAndRemoveUntil( '/', (Route<dynamic> route) => false); }, child: Text('로그아웃', style: TextStyle(fontSize: 20)), ), SizedBox( height: 50, ), ElevatedButton( onPressed: () { AlertDialog dialog = new AlertDialog( title: Text('아이디 삭제'), content: Text('아이디를 삭제하시겠습니까?'), actions: <Widget>[ TextButton( onPressed: () { print(widget.id); widget.databaseReference! .child('user') .child(widget.id!) .remove(); Navigator.of(context).pushNamedAndRemoveUntil( '/', (Route<dynamic> route) => false); }, child: Text('예')), TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text('아니요')), ], ); showDialog( context: context, builder: (context) { return dialog; }); }, child: Text('회원 탈퇴', style: TextStyle(fontSize: 20)), ), ], mainAxisAlignment: MainAxisAlignment.center, ), ), ), ); } void _setData(bool value) async { var key = "push"; SharedPreferences pref = await SharedPreferences.getInstance(); pref.setBool(key, value); } void _loadData() async { var key = "push"; SharedPreferences pref = await SharedPreferences.getInstance(); setState(() { var value = pref.getBool(key); if (value == null) { setState(() { pushCheck = true; }); } else { setState(() { pushCheck = value; }); } }); } }
// mainPage.dart import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/material.dart'; import 'main/favoritePage.dart'; import 'main/settingPage.dart'; import 'main/mapPage.dart'; // ------------------------------------ // 즐겨찾기 import 'package:sqflite/sqflite.dart'; // ------------------------------------ // 메인 화면 class MainPage extends StatefulWidget { // ------------------------------ // 즐겨찾기 final Future<Database> database; MainPage(this.database); // ------------------------------ @override State<StatefulWidget> createState() => _MainPage(); } class _MainPage extends State<MainPage> with SingleTickerProviderStateMixin { TabController? controller; FirebaseDatabase? _database; DatabaseReference? reference; String _databaseURL = '### 데이터베이스 URL ###'; String? id; @override void initState() { super.initState(); controller = TabController(length: 3, vsync: this); _database = FirebaseDatabase(databaseURL: _databaseURL); reference = _database!.reference().child('tour'); } @override void dispose() { controller!.dispose(); super.dispose(); } @override Widget build(BuildContext context) { id = ModalRoute.of(context)!.settings.arguments as String?; // id 변수는 String형이므로 형변환한다. return Scaffold( body: TabBarView( children: <Widget>[ // TabBarView에 채울 위젯들 MapPage( // ---------------------------------- // MapPage 함수를 호출하는 부분에 추가 databaseReference: reference!, // ----------------- // 즐겨찾기 db: widget.database, // ----------------- id: id, // ---------------------------------- ), // 관광 정보 FavoritePage( // --------------------------- // 즐겨찾기 databaseReference: reference!, db: widget.database, id: id!, // --------------------------- ), // 즐겨찾기 SettingPage( // --------------------------- // 설정화면 databaseReference: reference!, id: id!, // --------------------------- ) // 설정 ], controller: controller, ), // bottomNavigationBar를 이용해 메인 페이지를 만든다. bottomNavigationBar: TabBar( tabs: <Tab>[ Tab( icon: Icon(Icons.map), ), Tab( icon: Icon(Icons.star), ), Tab( icon: Icon(Icons.settings), ) ], labelColor: Colors.amber, indicatorColor: Colors.deepOrangeAccent, controller: controller, )); } }
푸시 알림 처리
파이어베이스에서 메시지를 보내면 알림으로 표시하는 기능을 추가한다.
// main.dart import 'package:flutter/material.dart'; import 'signPage.dart'; import 'login.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; import 'mainPage.dart'; // 로그인 후 이동할 메인 화면 // ------------------------------------ // 즐겨찾기 import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; // ------------------------------------ // --------------------------------------------------------- // 푸시 알림 처리 import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:shared_preferences/shared_preferences.dart'; // --------------------------------------------------------- void main() { WidgetsFlutterBinding.ensureInitialized(); MobileAds.instance.initialize(); runApp(MyApp()); } class MyApp extends StatelessWidget { // ------------------------------------------------------------------------ // 즐겨찾기 Future<Database> initDatabase() async { return openDatabase( join(await getDatabasesPath(), 'tour_database.db'), onCreate: (db, version) { return db.execute( "CREATE TABLE place(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, tel TEXT , zipcode TEXT , address TEXT , mapx Number , mapy Number , imagePath TEXT)", ); }, version: 1, ); } // ------------------------------------------------------------------------ @override Widget build(BuildContext context) { // --------------------------------------------------------------- // 즐겨찾기 Future<Database> database = initDatabase(); // build 할때 initDatabase() 함수를 호출합니다 // --------------------------------------------------------------- return MaterialApp( title: '부버의 여행', theme: ThemeData( primarySwatch: Colors.blue, ), initialRoute: '/', // ------------------------------------------------------------- // 푸시 알림 처리 routes: { '/': (context) { return FutureBuilder( // Initialize FlutterFire future: Firebase.initializeApp(), builder: (context, snapshot) { // Check for errors if (snapshot.hasError) { return Center( child: Text('Error'), ); } // Once complete, show your application if (snapshot.connectionState == ConnectionState.done) { _initFirebaseMessaging(context); _getToken(); return LoginPage(); } // ------------------------------------------------------------- // Otherwise, show something whilst waiting for initialization to complete return Center( child: CircularProgressIndicator(), ); }, ); }, '/sign': (context) => SignPage(), '/main': (context) => MainPage(database), // 로그인 후 메인 화면으로 이동 }, ); } // ------------------------------------------------------------------- // 푸시 알림 설정 _initFirebaseMessaging(BuildContext context) { FirebaseMessaging.onMessage.listen((RemoteMessage event) async { bool? pushCheck = await _loadData(); if (pushCheck!) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(event.notification!.title!), // Text 내 파라미터를 유의해서 보자! 다른 값으로 인해 에러발생, 애 먹었다! content: Text(event.notification!.body!), actions: [ TextButton( child: Text("Ok"), onPressed: () { Navigator.of(context).pop(); }, ) ], ); }); } }); FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {}); } Future<bool?> _loadData() async { var key = "push"; SharedPreferences pref = await SharedPreferences.getInstance(); var value = pref.getBool(key); return value; } _getToken() async { FirebaseMessaging messaging = FirebaseMessaging.instance; print("messaging.getToken() , ${await messaging.getToken()}"); } // ------------------------------------------------------------------- }
배너 광고 넣기
// main.dart import 'package:flutter/material.dart'; import 'signPage.dart'; import 'login.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; import 'mainPage.dart'; // 로그인 후 이동할 메인 화면 // ------------------------------------ // 즐겨찾기 import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; // ------------------------------------ // --------------------------------------------------------- // 푸시 알림 처리 import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:shared_preferences/shared_preferences.dart'; // --------------------------------------------------------- // ------------------------------------------------------- // 배너 광고 넣기 import 'package:google_mobile_ads/google_mobile_ads.dart'; // ------------------------------------------------------- void main() { WidgetsFlutterBinding.ensureInitialized(); MobileAds.instance.initialize(); runApp(MyApp()); } class MyApp extends StatelessWidget { // -------------------------------------------------------------------------- // 즐겨찾기 Future<Database> initDatabase() async { return openDatabase( join(await getDatabasesPath(), 'tour_database.db'), onCreate: (db, version) { return db.execute( "CREATE TABLE place(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, tel TEXT , zipcode TEXT , address TEXT , mapx Number , mapy Number , imagePath TEXT)", ); }, version: 1, ); } // -------------------------------------------------------------------------- @override Widget build(BuildContext context) { // --------------------------------------------------------------- // 즐겨찾기 Future<Database> database = initDatabase(); // build 할때 initDatabase() 함수를 호출합니다 // --------------------------------------------------------------- return MaterialApp( title: '부버의 여행', theme: ThemeData( primarySwatch: Colors.blue, ), initialRoute: '/', // ------------------------------------------------------------- // 푸시 알림 처리 routes: { '/': (context) { return FutureBuilder( // Initialize FlutterFire future: Firebase.initializeApp(), builder: (context, snapshot) { // Check for errors if (snapshot.hasError) { return Center( child: Text('Error'), ); } // Once complete, show your application if (snapshot.connectionState == ConnectionState.done) { _initFirebaseMessaging(context); _getToken(); return LoginPage(); } // ------------------------------------------------------------- // Otherwise, show something whilst waiting for initialization to complete return Center( child: CircularProgressIndicator(), ); }, ); }, '/sign': (context) => SignPage(), '/main': (context) => MainPage(database), // 로그인 후 메인 화면으로 이동 }, ); } // ------------------------------------------------------------------- // 푸시 알림 설정 _initFirebaseMessaging(BuildContext context) { FirebaseMessaging.onMessage.listen((RemoteMessage event) async { bool? pushCheck = await _loadData(); if (pushCheck!) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(event.notification! .title!), // Text 내 파라미터를 유의해서 보자! 다른 값으로 인해 에러발생, 애 먹었다! content: Text(event.notification!.body!), actions: [ TextButton( child: Text("Ok"), onPressed: () { Navigator.of(context).pop(); }, ) ], ); }); } }); FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {}); } Future<bool?> _loadData() async { var key = "push"; SharedPreferences pref = await SharedPreferences.getInstance(); var value = pref.getBool(key); return value; } _getToken() async { FirebaseMessaging messaging = FirebaseMessaging.instance; print("messaging.getToken() , ${await messaging.getToken()}"); } // ------------------------------------------------------------------- }
// settingPage.dart import 'dart:io'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; // 설정화면 class SettingPage extends StatefulWidget { final DatabaseReference? databaseReference; final String? id; SettingPage({this.databaseReference, this.id}); @override State<StatefulWidget> createState() => _SettingPage(); } class _SettingPage extends State<SettingPage> { bool pushCheck = true; // --------------------------------------------------------------- // 배너 광고 넣기 BannerAd? _banner; bool _loadingBanner = false; Future<void> _createAnchoredBanner(BuildContext context) async { final AnchoredAdaptiveBannerAdSize? size = await AdSize.getAnchoredAdaptiveBannerAdSize( Orientation.portrait, MediaQuery.of(context).size.width.truncate(), ); if (size == null) { print('Unable to get height of anchored banner.'); 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 dispose() { super.dispose(); _banner!.dispose(); } // --------------------------------------------------------------- @override void initState() { super.initState(); _loadData(); } @override Widget build(BuildContext context) { // ------------------------------- // 배너 광고 넣기 if (!_loadingBanner) { _loadingBanner = true; _createAnchoredBanner(context); } // ------------------------------- return Scaffold( appBar: AppBar( title: Text('설정하기'), ), // --------------------------------------------- // 배너 광고 넣기 body: Stack( alignment: AlignmentDirectional.bottomCenter, children: <Widget>[ // --------------------------------------------- Container( child: Center( child: Column( children: <Widget>[ Row( children: <Widget>[ Text( '푸시 알림', style: TextStyle(fontSize: 20), ), Switch( value: pushCheck, onChanged: (value) { setState(() { pushCheck = value; }); _setData(value); }) ], mainAxisAlignment: MainAxisAlignment.spaceAround, ), SizedBox( height: 50, ), ElevatedButton( onPressed: () { // 로그아웃은 보통 서버와 통신하는 코드로 구현하지만 지금은 서버가 없으므로 네비게이터의 모든 스택을 지우고 홈으로 이동한다. Navigator.of(context).pushNamedAndRemoveUntil( '/', (Route<dynamic> route) => false); }, child: Text('로그아웃', style: TextStyle(fontSize: 20)), ), SizedBox( height: 50, ), ElevatedButton( onPressed: () { AlertDialog dialog = new AlertDialog( title: Text('아이디 삭제'), content: Text('아이디를 삭제하시겠습니까?'), actions: <Widget>[ TextButton( onPressed: () { print(widget.id); widget.databaseReference! .child('user') .child(widget.id!) .remove(); Navigator.of(context).pushNamedAndRemoveUntil( '/', (Route<dynamic> route) => false); }, child: Text('예')), TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text('아니요')), ], ); showDialog( context: context, builder: (context) { return dialog; }); }, child: Text('회원 탈퇴', style: TextStyle(fontSize: 20)), ), ], mainAxisAlignment: MainAxisAlignment.center, ), ), ), if (_banner != null) Container( color: Colors.green, width: _banner!.size.width.toDouble(), height: _banner!.size.height.toDouble(), child: AdWidget(ad: _banner!), ), ], ), ); } void _setData(bool value) async { var key = "push"; SharedPreferences pref = await SharedPreferences.getInstance(); pref.setBool(key, value); } void _loadData() async { var key = "push"; SharedPreferences pref = await SharedPreferences.getInstance(); setState(() { var value = pref.getBool(key); if (value == null) { setState(() { pushCheck = true; }); } else { setState(() { pushCheck = value; }); } }); } }
구글 플레이에 앱 출시하기
앱 이름 수정
AndroidManifest.xml의 android:label의 값을 앱 이름으로 변경한다. 이 이름은 스마트폰이 앱이 설치되었을 때 아이콘 아래에 표시된다.
<!-- android/app/src/main/AndroidManifest.xml --> <application android:label="부버의 여행" android:name="${applicationName}" android:icon="@mipmap/ic_launcher">
아이콘 만들기
아이콘 만드는 방법은 책의 내용으로 진행할 수 없었다. VSCode의 문제일지도 몰라서 안드로이드 스튜디오에서도 진행했는데 [New → Image Asset]을 선택할 수 없었다. 해결한 방법은 다음과 같다.
터미널에서 flutter_launcher_icons를 설치한다.
flutter pub add flutter_launcher_icons
pubspec.yaml의 가장 아래에 다음의 코드를 추가한다. 당연히 image_path에 이미지 파일이 존재해야 한다.
# pubspec.yaml flutter_icons: android: "launcher_icon" ios: true image_path: "repo/images/buber_tour.png"
터미널에서 다음의 명령을 실행하여 아이콘을 생성한다.
flutter pub run flutter_launcher_icons:main
AndroidManifest.xml에서 android:icon의 아이콘 명을 변경한다(이 부분은 책에 나오는 내용이다).
<!-- android/app/src/main/AndroidManifest.xml --> <application android:label="부버의 여행" android:name="${applicationName}" android:icon="@mipmap/launcher_icon">
728x90반응형'언어·프레임워크 > Flutter' 카테고리의 다른 글
다음글이 없습니다.이전글이 없습니다.댓글