이번시간에는 주소와 위도/경도 사이의 데이터 변환에 대해서 구현하겠습니다.
개발환경 : 윈도우11, 안드로이드 스튜디오, flutter 3.0.1
원래 도로명 검색과 현재 위치 찾기에 사용하는 API 서버는 vWorld 였으나
해외에서 접근이 안되는 관계로 2개의 API 서버를 사용해서 데체할 예정입니다.
도로명 검색은 이전에 설명한대로 api 서버는 "https://www.juso.go.kr/openIndexPage.do" 사이트를 이용했습니다.
주소와 위도/경도 사이의 변환은 구글 api 서버를 사용할 예정입니다.
vWorld 는 도로명 검색결과에 위도/경도 정보가 포함되어 있으나 juso 서버에는 도로명 정보만 있어서
juso 서버의 결과를 한번 더 구글 api 서버를 이용하여 위도/경도 정보를 취득할 예정입니다.
(구글 API)현재위치 찾기의 결과는 지번주소로 도로명 주소 검색 결과와 형식이 다른것을 알수 있다.
화면 구성은 아래와 같다.
1번째 캡쳐는 도로명 검색란에 입력후 키보드에서 엔터를 치면 관련 주소 리스트가 보이고 원하는 주소를 클릭하면 주소명과 위도/경도 정보가 출력되는것을 볼수 있다.
3번째 캡쳐는 현재 디바이스의 GPS 위치를 가져와서 해당 주소 및 주변의 4개의 주소를 추가로 보여주고 원하는 주소를 클릭하면 해당 주소명과 위도/경도 정보가 출력되는것을 볼수 있다.
추가된 패키지는 아래와 같습니다.
uuid: ^3.0.6 // 유니크 키값 추출용 패키지
geolocator: ^9.0.1 // 디바이스의 GPS 정보 추출용 패키지
패키지가 추가로 환경 파일을 변경이 필요했습니다. 변경 내용은 아래와 같습니다.
android/app/build.gradle
android {
compileSdkVersion 33//최소 33으로 처리
ndkVersion flutter.ndkVersion
android/app/src/main/AndroidManifest.xml - location & apiKEY 추가
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.apple_market3">
<!-- geolocator 패키지, 위치정보를 위해 필요함 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- location 패키지를 사용할 경우(현재는 사용하지 않으므로 참고용임) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<application
android:label="apple_market3"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- 구글을 사용하기 위한 API_KEY 필수 -->
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="개인이 신청한 키 입력" />
<activity
./src/screens/start/address_page.dart - 검색 결과를 화면으로 구현(추출한 데이터들을 화면에 표시)
import 'package:apple_market3/src/models/address_from_location_model.dart';
import 'package:apple_market3/src/models/location_from_address_model.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:uuid/uuid.dart';
import '../../constants/common_size.dart';
import '../../models/address_model.dart';
import 'address_service.dart';
import 'google_map_service.dart';
class AddressPage extends StatefulWidget {
const AddressPage({Key? key}) : super(key: key);
@override
State<AddressPage> createState() => _AddressPageState();
}
class _AddressPageState extends State<AddressPage> {
final TextEditingController _addressController = TextEditingController();
var uuid = const Uuid(); // sessionToken 에 할당할 예정
late String sessionToken;
var googleMapServices = GoogleMapServices(sessionToken: const Uuid().v4());
// 더미 객체 생성로 생성하지 않고 nullable 로 처리
Position? position;
// 국내 임의의 위도/경도 정보를 할당
Position fakePosition = Position(
latitude: 37.5377469,
longitude: 126.9643189,
timestamp: DateTime.now(),
accuracy: 20.0,
altitude: 0.0,
heading: 0.0,
speed: 0.0,
speedAccuracy: 0.0);
// geocoding, 좌표에 주소로 변경시 사용하는 모델
LocationFromAddress? myLocation;
// reverse geocoding, 좌표에 주소로 변경시 사용하는 모델, 리스트 타입
final List<AddressFromLocation> _addressModelXYList = [];
// 도로명 주소 모델, 내부 배열에 해당 주소들 포함됨
AddressModel? _addressModel;
// 현재위치 찾기 로딩중인지 확인 flag
bool _isGettingLocation = false;
@override
void initState() {
super.initState();
sessionToken = uuid.v4();
_checkGPSAvailability();
}
@override
void dispose() {
_addressController.dispose();
super.dispose();
}
void _checkGPSAvailability() async {
// gps 사용 허가 확인
LocationPermission geolocationStatus = await Geolocator.checkPermission();
// debugPrint('geolocationStatus: [$geolocationStatus]');
// GeolocationStatus.granted
if (geolocationStatus == LocationPermission.denied ||
geolocationStatus == LocationPermission.deniedForever) {
showDialog(
// 다이얼로그 밖 영역 클릭시 사라지지 않게 처리
barrierDismissible: false,
context: context,
builder: (ctx) {
return AlertDialog(
title: const Text('GPS 사용 불가'),
content: const Text('GPS 사용 불가로 앱을 사용할 수 없습니다'),
actions: <Widget>[
ElevatedButton(
child: const Text('OK'),
onPressed: () {
Navigator.pop(ctx);
},
),
],
);
},
).then((_) => Navigator.pop(context));
} else {
// 디바이스의 현재 GPS 위치 리턴, 현재는 가짜 위치를 사용중
await _getGPSLocation();
}
}
// 디바이스의 현재 GPS 위치 리턴, 현재는 가짜 위치를 사용중
Future<void> _getGPSLocation() async {
position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
// fake GPS, 현재는 가짜 위치를 사용중
position = fakePosition;
debugPrint('myPosition(_getGPSLocation) : $position');
}
@override
Widget build(BuildContext context) {
debugPrint(">>> build from AddressPage");
return SafeArea(
// padding 대신에 minimum 으로 설정 가능, 위/아래 글씨가 잘리는것도 방지하자
minimum: const EdgeInsets.symmetric(horizontal: padding_16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
//도로명 검색하기
TextFormField(
controller: _addressController,
// 키보드의 엔터키를 터치하면 실행, 입력된 단어를 포함한 주소를 리턴
onFieldSubmitted: onClickTextField,
decoration: InputDecoration(
// icon 으로도 가능하지만 여기서는 prefixIcon 으로 설정함
prefixIcon: const Icon(
Icons.search,
color: Colors.grey,
),
// 아이콘 주변 공간을 조절 가능
prefixIconConstraints:
const BoxConstraints(minWidth: 24, maxHeight: 24),
// 문자 입력 박스의 아웃라인 설정
border: const UnderlineInputBorder(
borderSide: BorderSide(color: Colors.grey)),
// focusedBorder: const UnderlineInputBorder(borderSide: BorderSide(color: Colors.grey)),
hintText: '도로명으로 검색...',
hintStyle: TextStyle(color: Theme.of(context).hintColor),
),
),
const SizedBox(height: padding_08),
// 현재 위치 찾기
TextButton.icon(
label: Text(
// 로딩중일때는 다른 문자를 표시함
_isGettingLocation ? '위치 찾는중 ~~' : '현재위치 찾기',
style: Theme.of(context).textTheme.button,
),
// 디바이스의 GPS 좌표에 해당 하는 주소와 주변 주소를 리트스 형식으로 리턴
onPressed: myAddresses, //myLocation,
// 로딩중일때는 다른 아이콘을 표시함
icon: _isGettingLocation
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(color: Colors.white))
: const Icon(CupertinoIcons.compass,
color: Colors.white, size: 20),
),
//도로명 검색 결과 표시,
if (_addressModel != null)
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: padding_16),
itemCount: (_addressModel == null)
? 0
: _addressModel!.results.juso.length,
itemBuilder: (context, index) {
if (_addressModel == null) {
return Container();
}
var subAddress =
_addressModel!.results.juso[index].jibunAddr.split(' ');
return ListTile(
onTap: () async {
// 주소를 좌표로 변환하는 함수
myLocation =
await GoogleMapServices.getLocationFromAddress(
_addressModel!.results.juso[index].roadAddrPart1);
debugPrint(myLocation!.results[0].formattedAddress);
debugPrint(myLocation!.results[0].geometry.location.toString());
},
title:
Text(_addressModel!.results.juso[index].roadAddrPart1),
subtitle: Text('${subAddress[2]} ${subAddress[3]}'),
);
},
),
),
//현재위치 검색 결과 표시
if (_addressModelXYList.isNotEmpty)
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: padding_16),
// shrinkWrap: true,
itemCount: _addressModelXYList.length,
itemBuilder: (context, index) {
if (_addressModelXYList[index].results.isEmpty) {
//_addressModelXYList[index].results == null ||
return Container();
}
return ListTile(
onTap: () {
debugPrint(_addressModelXYList[index].results[0].formattedAddress);
debugPrint(_addressModelXYList[index].results[0].geometry.location.toString());
// _saveAddressAndGoToNextPage(_addressModelXYList[index].result![0].text ?? '',
// num.parse(_addressModelXYList[index].input!.point!.y ?? '0') ,
// num.parse(_addressModelXYList[index].input!.point!.x ?? '0') ,);
},
// leading: ExtendedImage.asset('assets/imgs/apple.png'),
title: Text(
_addressModelXYList[index].results[0].formattedAddress),
// subtitle: Text(_addressModelXYList[index].results![0]. ?? ''),
);
},
),
),
],
),
);
}
// 디바이스의 GPS 좌표에 해당 하는 주소와 주변 주소를 리트스 형식으로 리턴
void myAddresses() async {
_addressModel = null;
_addressModelXYList.clear();
_addressController.text = '';
setState(() {
_isGettingLocation = true;
});
List<AddressFromLocation> addressModelXY =
await GoogleMapServices.getAddressFromLocation5(
position!.latitude, position!.longitude);
_addressModelXYList.addAll(addressModelXY);
setState(() {
_isGettingLocation = false;
});
}
void onClickTextField(text) async {
_addressModelXYList.clear();
_addressModel = await AddressService().searchAddressByStr(text);
setState(() {});
}
}
./src/screens/start/google_map_service.dart - google API
import 'package:apple_market3/src/models/address_from_location_model.dart';
import 'package:apple_market3/src/models/location_from_address_model.dart';
import 'package:dio/dio.dart';
import '../../../keys.dart';
import '../../utils/logger.dart';
class GoogleMapServices {
final String sessionToken;
GoogleMapServices({required this.sessionToken});
// 주소정보를 입력해서 위도 경도 정보를 가져온다
static Future<LocationFromAddress> getLocationFromAddress(String address) async {
const String baseUrl =
'https://maps.googleapis.com/maps/api/geocode/json';
String url =
'$baseUrl?key=$googleApiKey&address=$address&language=ko';//&sessiontoken=$token';
var response = await Dio().get(url).catchError((e) {
logger.e(e.message);
});
final LocationFromAddress locationFromAddress = LocationFromAddress.fromJson(response.data);
return locationFromAddress;
}
// 위도 경도 정보를 이용해서 주소 정보를 찾는 함수
static Future<AddressFromLocation> getAddressFromLocation(double lat, double lng) async {
const String baseUrl = 'https://maps.googleapis.com/maps/api/geocode/json';
String url = '$baseUrl?latlng=$lat,$lng&key=$googleApiKey&language=ko';
var response = await Dio().get(url).catchError((e) {
logger.e(e.message);
});
final AddressFromLocation addressFromLocation = AddressFromLocation.fromJson(response.data);
return addressFromLocation;
}
// 위도 경도 정보를 이용해서 주소 정보를 찾는 함수
static Future<List<AddressFromLocation>> getAddressFromLocation5(double lat, double lng) async {
List<AddressFromLocation> addressFromLocation5 = <AddressFromLocation>[];
const String baseUrl = 'https://maps.googleapis.com/maps/api/geocode/json';
for (var i=0; i<5; i++) {
int x;
int y;
if (i == 1) {
x = 0;
y = 1;
}
else if (i == 2) {
x = 1;
y = 0;
}
else if (i == 3) {
x = 0;
y = -1;
}
else if (i == 4) {
x = -1;
y = 0;
}
else {
x = 0;
y = 0;
}
String url = '$baseUrl?latlng=${lat+0.01*y},${lng+0.01*x}&key=$googleApiKey&language=ko';
var response = await Dio().get(url).catchError((e) {
logger.e(e.message);
});
final AddressFromLocation addressFromLocation = AddressFromLocation.fromJson(response.data);
if(addressFromLocation.status == 'OK'){
addressFromLocation5.add(addressFromLocation);
}
}
// final formattedAddress = response.data['results'][0]['formatted_address'];
// final formattedAddress = addressFromLocation.results[0].formattedAddress;
return addressFromLocation5;
}
}
./src/models/address_from_location_model.dart
// To parse this JSON data, do
//
// final addressFromLocation = addressFromLocationFromJson(jsonString);
import 'dart:convert';
AddressFromLocation addressFromLocationFromJson(String str) =>
AddressFromLocation.fromJson(json.decode(str));
String addressFromLocationToJson(AddressFromLocation data) =>
json.encode(data.toJson());
class AddressFromLocation {
PlusCode plusCode;
List<Result> results;
String status;
AddressFromLocation({
required this.plusCode,
required this.results,
required this.status,
});
factory AddressFromLocation.fromJson(Map<String, dynamic> json) =>
AddressFromLocation(
plusCode: PlusCode.fromJson(json["plus_code"]),
results:
List<Result>.from(json["results"].map((x) => Result.fromJson(x))),
status: json["status"],
);
Map<String, dynamic> toJson() => {
"plus_code": plusCode.toJson(),
"results": List<dynamic>.from(results.map((x) => x.toJson())),
"status": status,
};
@override
String toString() {
return 'AddressFromLocation{plusCode: $plusCode, results: $results, status: $status}';
}
}
class PlusCode {
String? compoundCode;
String? globalCode;
PlusCode({
this.compoundCode = '',
this.globalCode = '',
});
factory PlusCode.fromJson(Map<String, dynamic> json) => PlusCode(
compoundCode: json["compound_code"] ?? '',
globalCode: json["global_code"] ?? '',
);
Map<String, dynamic> toJson() => {
"compound_code": compoundCode,
"global_code": globalCode,
};
@override
String toString() {
return 'PlusCode{compoundCode: $compoundCode, globalCode: $globalCode}';
}
}
class Result {
List<AddressComponent> addressComponents;
String formattedAddress;
Geometry geometry;
String placeId;
PlusCode? plusCode;
List<String> types;
Result({
required this.addressComponents,
required this.formattedAddress,
required this.geometry,
required this.placeId,
this.plusCode,
required this.types,
});
factory Result.fromJson(Map<String, dynamic> json) => Result(
addressComponents: List<AddressComponent>.from(
json["address_components"]
.map((x) => AddressComponent.fromJson(x))),
formattedAddress: json["formatted_address"] ?? '',
geometry: Geometry.fromJson(json["geometry"]),
placeId: json["place_id"] ?? '',
plusCode: json["plus_code"] != null
? PlusCode.fromJson(json["plus_code"])
: null,
types: List<String>.from(json["types"].map((x) => x)),
);
Map<String, dynamic> toJson() => {
"address_components":
List<dynamic>.from(addressComponents.map((x) => x.toJson())),
"formatted_address": formattedAddress,
"geometry": geometry.toJson(),
"place_id": placeId,
"plus_code": plusCode?.toJson(),
"types": List<dynamic>.from(types.map((x) => x)),
};
@override
String toString() {
return 'Result{addressComponents: $addressComponents, formattedAddress: $formattedAddress, geometry: $geometry, placeId: $placeId, plusCode: $plusCode, types: $types}';
}
}
class AddressComponent {
String longName;
String shortName;
List<String> types;
AddressComponent({
required this.longName,
required this.shortName,
required this.types,
});
factory AddressComponent.fromJson(Map<String, dynamic> json) =>
AddressComponent(
longName: json["long_name"] ?? '',
shortName: json["short_name"] ?? '',
types: List<String>.from(json["types"].map((x) => x)),
);
Map<String, dynamic> toJson() => {
"long_name": longName,
"short_name": shortName,
"types": List<dynamic>.from(types.map((x) => x)),
};
@override
String toString() {
return 'AddressComponent{longName: $longName, shortName: $shortName, types: $types}';
}
}
class Geometry {
Location location;
String locationType;
Viewport viewport;
Geometry({
required this.location,
required this.locationType,
required this.viewport,
});
factory Geometry.fromJson(Map<String, dynamic> json) => Geometry(
location: Location.fromJson(json["location"]),
locationType: json["location_type"] ?? '',
viewport: Viewport.fromJson(json["viewport"]),
);
Map<String, dynamic> toJson() => {
"location": location.toJson(),
"location_type": locationType,
"viewport": viewport.toJson(),
};
@override
String toString() {
return 'Geometry{location: $location, locationType: $locationType, viewport: $viewport}';
}
}
class Location {
double lat;
double lng;
Location({
required this.lat,
required this.lng,
});
factory Location.fromJson(Map<String, dynamic> json) => Location(
lat: json["lat"].toDouble() ?? 0.0,
lng: json["lng"].toDouble() ?? 0.0,
);
Map<String, dynamic> toJson() => {
"lat": lat,
"lng": lng,
};
@override
String toString() {
return 'Location{lat: $lat, lng: $lng}';
}
}
class Viewport {
Location northeast;
Location southwest;
Viewport({
required this.northeast,
required this.southwest,
});
factory Viewport.fromJson(Map<String, dynamic> json) => Viewport(
northeast: Location.fromJson(json["northeast"]),
southwest: Location.fromJson(json["southwest"]),
);
Map<String, dynamic> toJson() => {
"northeast": northeast.toJson(),
"southwest": southwest.toJson(),
};
@override
String toString() {
return 'Viewport{northeast: $northeast, southwest: $southwest}';
}
}
./src/models/location_from_address_model.dart
// To parse this JSON data, do
//
// final locationFromAddress = locationFromAddressFromJson(jsonString);
import 'dart:convert';
LocationFromAddress locationFromAddressFromJson(String str) =>
LocationFromAddress.fromJson(json.decode(str));
String locationFromAddressToJson(LocationFromAddress data) =>
json.encode(data.toJson());
class LocationFromAddress {
List<Result> results;
String status;
LocationFromAddress({
required this.results,
required this.status,
});
factory LocationFromAddress.fromJson(Map<String, dynamic> json) =>
LocationFromAddress(
results:
List<Result>.from(json["results"].map((x) => Result.fromJson(x))),
status: json["status"],
);
Map<String, dynamic> toJson() => {
"results": List<dynamic>.from(results.map((x) => x.toJson())),
"status": status,
};
@override
String toString() {
return 'LocationFromAddress{results: $results, status: $status}';
}
}
class Result {
List<AddressComponent> addressComponents;
String formattedAddress;
Geometry geometry;
String placeId;
PlusCode? plusCode;
List<String> types;
Result({
required this.addressComponents,
required this.formattedAddress,
required this.geometry,
required this.placeId,
this.plusCode,
required this.types,
});
factory Result.fromJson(Map<String, dynamic> json) => Result(
addressComponents: List<AddressComponent>.from(
json["address_components"]
.map((x) => AddressComponent.fromJson(x))),
formattedAddress: json["formatted_address"],
geometry: Geometry.fromJson(json["geometry"]),
placeId: json["place_id"],
plusCode: json["plus_code"] != null
? PlusCode.fromJson(json["plus_code"])
: null,
types: List<String>.from(json["types"].map((x) => x)),
);
Map<String, dynamic> toJson() => {
"address_components":
List<dynamic>.from(addressComponents.map((x) => x.toJson())),
"formatted_address": formattedAddress,
"geometry": geometry.toJson(),
"place_id": placeId,
"plus_code": plusCode?.toJson(),
"types": List<dynamic>.from(types.map((x) => x)),
};
@override
String toString() {
return 'Result{addressComponents: $addressComponents, formattedAddress: $formattedAddress, geometry: $geometry, placeId: $placeId, plusCode: $plusCode, types: $types}';
}
}
class AddressComponent {
String longName;
String shortName;
List<String> types;
AddressComponent({
required this.longName,
required this.shortName,
required this.types,
});
factory AddressComponent.fromJson(Map<String, dynamic> json) =>
AddressComponent(
longName: json["long_name"],
shortName: json["short_name"],
types: List<String>.from(json["types"].map((x) => x)),
);
Map<String, dynamic> toJson() => {
"long_name": longName,
"short_name": shortName,
"types": List<dynamic>.from(types.map((x) => x)),
};
@override
String toString() {
return 'AddressComponent{longName: $longName, shortName: $shortName, types: $types}';
}
}
class Geometry {
Location location;
String locationType;
Viewport viewport;
Geometry({
required this.location,
required this.locationType,
required this.viewport,
});
factory Geometry.fromJson(Map<String, dynamic> json) => Geometry(
location: Location.fromJson(json["location"]),
locationType: json["location_type"],
viewport: Viewport.fromJson(json["viewport"]),
);
Map<String, dynamic> toJson() => {
"location": location.toJson(),
"location_type": locationType,
"viewport": viewport.toJson(),
};
@override
String toString() {
return 'Geometry{location: $location, locationType: $locationType, viewport: $viewport}';
}
}
class Location {
double lat;
double lng;
Location({
required this.lat,
required this.lng,
});
factory Location.fromJson(Map<String, dynamic> json) => Location(
lat: json["lat"].toDouble(),
lng: json["lng"].toDouble(),
);
Map<String, dynamic> toJson() => {
"lat": lat,
"lng": lng,
};
@override
String toString() {
return 'Location{lat: $lat, lng: $lng}';
}
}
class Viewport {
Location northeast;
Location southwest;
Viewport({
required this.northeast,
required this.southwest,
});
factory Viewport.fromJson(Map<String, dynamic> json) => Viewport(
northeast: Location.fromJson(json["northeast"]),
southwest: Location.fromJson(json["southwest"]),
);
Map<String, dynamic> toJson() => {
"northeast": northeast.toJson(),
"southwest": southwest.toJson(),
};
@override
String toString() {
return 'Viewport{northeast: $northeast, southwest: $southwest}';
}
}
class PlusCode {
String compoundCode;
String globalCode;
PlusCode({
required this.compoundCode,
required this.globalCode,
});
factory PlusCode.fromJson(Map<String, dynamic> json) => PlusCode(
compoundCode: json["compound_code"],
globalCode: json["global_code"],
);
Map<String, dynamic> toJson() => {
"compound_code": compoundCode,
"global_code": globalCode,
};
@override
String toString() {
return 'PlusCode{compoundCode: $compoundCode, globalCode: $globalCode}';
}
}
'Flutter > 12 Clone 'Used Goods app'' 카테고리의 다른 글
[Flutter] Clone - 당근마켓14(pageController with provider) (0) | 2022.07.26 |
---|---|
[Flutter] Clone - 당근마켓13(Shared reference) (0) | 2022.07.25 |
[Flutter] Clone - 당근마켓11(Address - ListView) (0) | 2022.07.22 |
[Flutter] Clone - 당근마켓10(Address Model) (0) | 2022.07.22 |
[Flutter] Clone - 당근마켓9(logout) (0) | 2022.07.21 |