Dandy Now!
  • [Flutter] "Do it! 플러터 앱 프로그래밍" - 오픈 API를 활용한 여행 정보 앱 프로젝트 | 메인, 상세보기, 즐겨찾기, 설정 화면 만들기
    2022년 02월 27일 00시 57분 41초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    "조준수. (2021). Do it! 플러터 앱 프로그래밍. 이지스퍼블리싱", 15장 오픈 API를 활용한 여행 정보 앱 만들기를 실습했다. 메인, 상세보기, 즐겨찾기, 설정 화면을 각각 만들었다. 이 과정에서 관광 정보 오픈 API와 구글 맵 API를 연동하였다. 그리고 푸시 알림과 배너 광고 기능도 적용하였다.

     

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

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

    book.naver.com

     

    메인 화면 만들기

    메인 화면 기본 골격

    // 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(), // 로그인 후 메인 화면으로 이동
          },
        );
      }
    }

     

    [그림 1] 로그인 성공 후 메인 화면

     


     

    관광 정보 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),
        ));
      }
    }

     

    [그림 2] 관광 정보 API 이용해 검색하기

     

    에러 처리

    첫 빌드시 로그인 화면은 정상적으로 작동했다. 하지만 "검색하기" 버튼을 눌렀을 때 앱이 멈췄다. 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,
            ));
      }
    }

     

    [그림 3] 상세 페이지 및 구글맵 API 연동

     


     

    즐겨찾기 화면 만들기

    즐겨찾기 설정하고 해제하기

    // 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());
        });
      }
    }

     

    [그림 4] 즐겨찾기 화면

     


     

    설정 화면 만들기

    푸시 알림 수신 여부 설정하기

    푸시 알림 수신 여부를 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,
            ));
      }
    }

     

    [그림 5] 설정 화면 기능 구현

     


     

    푸시 알림 처리

    파이어베이스에서 메시지를 보내면 알림으로 표시하는 기능을 추가한다.

    // 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()}");
      }
      // -------------------------------------------------------------------
    }

     

    [그림 6] 푸시 알림 처리

     


     

    배너 광고 넣기

    // 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;
            });
          }
        });
      }
    }

     

    [그림 7] 설정화면의 배너 광고

     


     

    구글 플레이에 앱 출시하기

    앱 이름 수정

    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">

     

    [그림 8] 변경된 아이콘

    728x90
    반응형
    댓글