- 주요 스택
- Google Maps 지도
- 지도 마커
- 지도 동그라미 표시
- 현재 위치 표시 및 위도/경도 구하기
- 위도, 경도간 거리 구하기
지도 띄워보기
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// latitude 위도 , longitude 경도
static final LatLng companyLatLng = LatLng(10.800715, 106.7120223);
static final CameraPosition initialPosition = CameraPosition(
target: companyLatLng, // 최초 포지션
zoom: 15, // zoom level
);
@override
Widget build(BuildContext context) {
return Scaffold(
body: GoogleMap(
initialCameraPosition: initialPosition,
),
);
}
}
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// latitude 위도 , longitude 경도
static final LatLng companyLatLng = LatLng(10.800715, 106.7120223);
static final CameraPosition initialPosition = CameraPosition(
target: companyLatLng, // 최초 포지션
zoom: 15, // zoom level
);
@override
Widget build(BuildContext context) {
return Scaffold(
body: GoogleMap(
initialCameraPosition: initialPosition,
),
);
}
}
import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { // latitude 위도 , longitude 경도 static final LatLng companyLatLng = LatLng(10.800715, 106.7120223); static final CameraPosition initialPosition = CameraPosition( target: companyLatLng, // 최초 포지션 zoom: 15, // zoom level ); @override Widget build(BuildContext context) { return Scaffold( body: GoogleMap( initialCameraPosition: initialPosition, ), ); } }
AppBar 추가, 상 하단 분리
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// latitude 위도 , longitude 경도
static final LatLng companyLatLng = LatLng(10.800715, 106.7120223);
static final CameraPosition initialPosition = CameraPosition(
target: companyLatLng, // 최초 포지션
zoom: 15, // zoom level
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: Column(
children: [
_CustomGoogleMap(initialPosition: initialPosition),
_ChoolCheckButton(),
],
),
);
}
AppBar renderAppBar() {
return AppBar(
title: Text(
'오늘도 출근',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w700,
),
),
backgroundColor: Colors.white,
);
}
}
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
const _CustomGoogleMap({
required this.initialPosition,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
// hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
initialCameraPosition: initialPosition,
),
);
}
}
class _ChoolCheckButton extends StatelessWidget {
const _ChoolCheckButton({super.key});
@override
Widget build(BuildContext context) {
return Expanded(
child: Text(
'출근',
),
);
}
}
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// latitude 위도 , longitude 경도
static final LatLng companyLatLng = LatLng(10.800715, 106.7120223);
static final CameraPosition initialPosition = CameraPosition(
target: companyLatLng, // 최초 포지션
zoom: 15, // zoom level
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: Column(
children: [
_CustomGoogleMap(initialPosition: initialPosition),
_ChoolCheckButton(),
],
),
);
}
AppBar renderAppBar() {
return AppBar(
title: Text(
'오늘도 출근',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w700,
),
),
backgroundColor: Colors.white,
);
}
}
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
const _CustomGoogleMap({
required this.initialPosition,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
// hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
initialCameraPosition: initialPosition,
),
);
}
}
class _ChoolCheckButton extends StatelessWidget {
const _ChoolCheckButton({super.key});
@override
Widget build(BuildContext context) {
return Expanded(
child: Text(
'출근',
),
);
}
}
import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { // latitude 위도 , longitude 경도 static final LatLng companyLatLng = LatLng(10.800715, 106.7120223); static final CameraPosition initialPosition = CameraPosition( target: companyLatLng, // 최초 포지션 zoom: 15, // zoom level ); @override Widget build(BuildContext context) { return Scaffold( appBar: renderAppBar(), body: Column( children: [ _CustomGoogleMap(initialPosition: initialPosition), _ChoolCheckButton(), ], ), ); } AppBar renderAppBar() { return AppBar( title: Text( '오늘도 출근', style: TextStyle( color: Colors.blue, fontWeight: FontWeight.w700, ), ), backgroundColor: Colors.white, ); } } class _CustomGoogleMap extends StatelessWidget { final CameraPosition initialPosition; const _CustomGoogleMap({ required this.initialPosition, super.key, }); @override Widget build(BuildContext context) { return Expanded( flex: 2, child: GoogleMap( mapType: MapType.normal, // hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등 initialCameraPosition: initialPosition, ), ); } } class _ChoolCheckButton extends StatelessWidget { const _ChoolCheckButton({super.key}); @override Widget build(BuildContext context) { return Expanded( child: Text( '출근', ), ); } }
Geolocator 앱 권한 체크 및 허용 요청
Future<String> checkPermission() async {
// 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
final isLocationEnabled = await Geolocator
.isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)
if(!isLocationEnabled) {
return '위치 서비스를 활성화 해주세요.';
}
/*
* LocationPermission 타입
* denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
* deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
* whileInUse : 앱 실행중에만 권한 허가
* always : 항상 허가
* */
LocationPermission checkedPermission = await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
if(checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
if(checkedPermission == LocationPermission.denied) {
return '위치 권한을 허가해주세요.';
}
}
if(checkedPermission == LocationPermission.deniedForever){
return '앱의 위치 권한을 세팅에서 허가해주세요.';
}
return '위치 권한이 허가되었습니다.';
}
Future<String> checkPermission() async {
// 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
final isLocationEnabled = await Geolocator
.isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)
if(!isLocationEnabled) {
return '위치 서비스를 활성화 해주세요.';
}
/*
* LocationPermission 타입
* denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
* deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
* whileInUse : 앱 실행중에만 권한 허가
* always : 항상 허가
* */
LocationPermission checkedPermission = await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
if(checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
if(checkedPermission == LocationPermission.denied) {
return '위치 권한을 허가해주세요.';
}
}
if(checkedPermission == LocationPermission.deniedForever){
return '앱의 위치 권한을 세팅에서 허가해주세요.';
}
return '위치 권한이 허가되었습니다.';
}
Future<String> checkPermission() async { // 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문 final isLocationEnabled = await Geolocator .isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스) if(!isLocationEnabled) { return '위치 서비스를 활성화 해주세요.'; } /* * LocationPermission 타입 * denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨 * deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함) * whileInUse : 앱 실행중에만 권한 허가 * always : 항상 허가 * */ LocationPermission checkedPermission = await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인 if(checkedPermission == LocationPermission.denied) { checkedPermission = await Geolocator.requestPermission(); if(checkedPermission == LocationPermission.denied) { return '위치 권한을 허가해주세요.'; } } if(checkedPermission == LocationPermission.deniedForever){ return '앱의 위치 권한을 세팅에서 허가해주세요.'; } return '위치 권한이 허가되었습니다.'; }
FutureBuilder 를 통한 Future 상태 관리
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// latitude 위도 , longitude 경도
static final LatLng companyLatLng = LatLng(10.800715, 106.7120223);
static final CameraPosition initialPosition = CameraPosition(
target: companyLatLng, // 최초 포지션
zoom: 15, // zoom level
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: FutureBuilder( // FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다.
future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다.
builder: (BuildContext context, AsyncSnapshot snapshot) {
// future 에 넣은 함수의 리턴값은 snapshot에 들어간다.
print(snapshot.connectionState); // none, waiting, active, done (future 함수의 진행상태)
print(snapshot.data); // '위치 권한이 허가되었습니다'
if (snapshot.connectionState == ConnectionState.waiting) {
// 아직 사용자의 응답을 기다리고 있을때는 로딩바
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data == '위치 권한이 허가되었습니다.') {
return Column(
children: [
_CustomGoogleMap(
initialPosition: initialPosition,
),
_ChoolCheckButton(),
],
);
}
return Center(
child: Text(snapshot.data),
);
},
),
);
}
Future<String> checkPermission() async {
// 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
final isLocationEnabled = await Geolocator
.isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)
if (!isLocationEnabled) {
return '위치 서비스를 활성화 해주세요.';
}
/*
* LocationPermission 타입
* denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
* deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
* whileInUse : 앱 실행중에만 권한 허가
* always : 항상 허가
* */
LocationPermission checkedPermission =
await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
if (checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
if (checkedPermission == LocationPermission.denied) {
return '위치 권한을 허가해주세요.';
}
}
if (checkedPermission == LocationPermission.deniedForever) {
return '앱의 위치 권한을 세팅에서 허가해주세요.';
}
return '위치 권한이 허가되었습니다.';
}
AppBar renderAppBar() {
return AppBar(
title: Text(
'오늘도 출근',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w700,
),
),
backgroundColor: Colors.white,
);
}
}
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
const _CustomGoogleMap({
required this.initialPosition,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
// hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
initialCameraPosition: initialPosition,
),
);
}
}
class _ChoolCheckButton extends StatelessWidget {
const _ChoolCheckButton({super.key});
@override
Widget build(BuildContext context) {
return Expanded(
child: Text(
'출근',
),
);
}
}
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// latitude 위도 , longitude 경도
static final LatLng companyLatLng = LatLng(10.800715, 106.7120223);
static final CameraPosition initialPosition = CameraPosition(
target: companyLatLng, // 최초 포지션
zoom: 15, // zoom level
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: FutureBuilder( // FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다.
future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다.
builder: (BuildContext context, AsyncSnapshot snapshot) {
// future 에 넣은 함수의 리턴값은 snapshot에 들어간다.
print(snapshot.connectionState); // none, waiting, active, done (future 함수의 진행상태)
print(snapshot.data); // '위치 권한이 허가되었습니다'
if (snapshot.connectionState == ConnectionState.waiting) {
// 아직 사용자의 응답을 기다리고 있을때는 로딩바
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data == '위치 권한이 허가되었습니다.') {
return Column(
children: [
_CustomGoogleMap(
initialPosition: initialPosition,
),
_ChoolCheckButton(),
],
);
}
return Center(
child: Text(snapshot.data),
);
},
),
);
}
Future<String> checkPermission() async {
// 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
final isLocationEnabled = await Geolocator
.isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)
if (!isLocationEnabled) {
return '위치 서비스를 활성화 해주세요.';
}
/*
* LocationPermission 타입
* denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
* deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
* whileInUse : 앱 실행중에만 권한 허가
* always : 항상 허가
* */
LocationPermission checkedPermission =
await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
if (checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
if (checkedPermission == LocationPermission.denied) {
return '위치 권한을 허가해주세요.';
}
}
if (checkedPermission == LocationPermission.deniedForever) {
return '앱의 위치 권한을 세팅에서 허가해주세요.';
}
return '위치 권한이 허가되었습니다.';
}
AppBar renderAppBar() {
return AppBar(
title: Text(
'오늘도 출근',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w700,
),
),
backgroundColor: Colors.white,
);
}
}
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
const _CustomGoogleMap({
required this.initialPosition,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
// hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
initialCameraPosition: initialPosition,
),
);
}
}
class _ChoolCheckButton extends StatelessWidget {
const _ChoolCheckButton({super.key});
@override
Widget build(BuildContext context) {
return Expanded(
child: Text(
'출근',
),
);
}
}
import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { // latitude 위도 , longitude 경도 static final LatLng companyLatLng = LatLng(10.800715, 106.7120223); static final CameraPosition initialPosition = CameraPosition( target: companyLatLng, // 최초 포지션 zoom: 15, // zoom level ); @override Widget build(BuildContext context) { return Scaffold( appBar: renderAppBar(), body: FutureBuilder( // FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다. future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다. builder: (BuildContext context, AsyncSnapshot snapshot) { // future 에 넣은 함수의 리턴값은 snapshot에 들어간다. print(snapshot.connectionState); // none, waiting, active, done (future 함수의 진행상태) print(snapshot.data); // '위치 권한이 허가되었습니다' if (snapshot.connectionState == ConnectionState.waiting) { // 아직 사용자의 응답을 기다리고 있을때는 로딩바 return Center( child: CircularProgressIndicator(), ); } if (snapshot.data == '위치 권한이 허가되었습니다.') { return Column( children: [ _CustomGoogleMap( initialPosition: initialPosition, ), _ChoolCheckButton(), ], ); } return Center( child: Text(snapshot.data), ); }, ), ); } Future<String> checkPermission() async { // 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문 final isLocationEnabled = await Geolocator .isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스) if (!isLocationEnabled) { return '위치 서비스를 활성화 해주세요.'; } /* * LocationPermission 타입 * denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨 * deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함) * whileInUse : 앱 실행중에만 권한 허가 * always : 항상 허가 * */ LocationPermission checkedPermission = await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인 if (checkedPermission == LocationPermission.denied) { checkedPermission = await Geolocator.requestPermission(); if (checkedPermission == LocationPermission.denied) { return '위치 권한을 허가해주세요.'; } } if (checkedPermission == LocationPermission.deniedForever) { return '앱의 위치 권한을 세팅에서 허가해주세요.'; } return '위치 권한이 허가되었습니다.'; } AppBar renderAppBar() { return AppBar( title: Text( '오늘도 출근', style: TextStyle( color: Colors.blue, fontWeight: FontWeight.w700, ), ), backgroundColor: Colors.white, ); } } class _CustomGoogleMap extends StatelessWidget { final CameraPosition initialPosition; const _CustomGoogleMap({ required this.initialPosition, super.key, }); @override Widget build(BuildContext context) { return Expanded( flex: 2, child: GoogleMap( mapType: MapType.normal, // hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등 initialCameraPosition: initialPosition, ), ); } } class _ChoolCheckButton extends StatelessWidget { const _ChoolCheckButton({super.key}); @override Widget build(BuildContext context) { return Expanded( child: Text( '출근', ), ); } }
지도에 원 그리기
static final double distance = 100;
static final Circle circle = Circle(
// circle 생성
circleId: CircleId('circle'),
center: companyLatLng,
fillColor: Colors.blue.withOpacity(0.5), // 원 내부 색깔
radius: distance,
strokeColor: Colors.blue,
strokeWidth: 1,
);
....생략
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
final Circle circle;
const _CustomGoogleMap({
required this.initialPosition,
required this.circle,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
// hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
initialCameraPosition: initialPosition,
myLocationEnabled: true,
myLocationButtonEnabled: false,
circles: Set.from([circle]),
),
);
}
}
static final double distance = 100;
static final Circle circle = Circle(
// circle 생성
circleId: CircleId('circle'),
center: companyLatLng,
fillColor: Colors.blue.withOpacity(0.5), // 원 내부 색깔
radius: distance,
strokeColor: Colors.blue,
strokeWidth: 1,
);
....생략
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
final Circle circle;
const _CustomGoogleMap({
required this.initialPosition,
required this.circle,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
// hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
initialCameraPosition: initialPosition,
myLocationEnabled: true,
myLocationButtonEnabled: false,
circles: Set.from([circle]),
),
);
}
}
static final double distance = 100; static final Circle circle = Circle( // circle 생성 circleId: CircleId('circle'), center: companyLatLng, fillColor: Colors.blue.withOpacity(0.5), // 원 내부 색깔 radius: distance, strokeColor: Colors.blue, strokeWidth: 1, ); ....생략 class _CustomGoogleMap extends StatelessWidget { final CameraPosition initialPosition; final Circle circle; const _CustomGoogleMap({ required this.initialPosition, required this.circle, super.key, }); @override Widget build(BuildContext context) { return Expanded( flex: 2, child: GoogleMap( mapType: MapType.normal, // hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등 initialCameraPosition: initialPosition, myLocationEnabled: true, myLocationButtonEnabled: false, circles: Set.from([circle]), ), ); } }
마커 심기
marker도 circle 과 완전 비슷함
static final Marker marker = Marker(
markerId: MarkerId('marker'),
position: companyLatLng,
);
...
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
final Circle circle;
final Marker marker;
const _CustomGoogleMap({
required this.initialPosition,
required this.circle,
required this.marker,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
// hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
initialCameraPosition: initialPosition,
myLocationEnabled: true,
myLocationButtonEnabled: false,
circles: Set.from([circle]),
markers: Set.from([marker]),
),
);
}
}
static final Marker marker = Marker(
markerId: MarkerId('marker'),
position: companyLatLng,
);
...
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
final Circle circle;
final Marker marker;
const _CustomGoogleMap({
required this.initialPosition,
required this.circle,
required this.marker,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
// hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
initialCameraPosition: initialPosition,
myLocationEnabled: true,
myLocationButtonEnabled: false,
circles: Set.from([circle]),
markers: Set.from([marker]),
),
);
}
}
static final Marker marker = Marker( markerId: MarkerId('marker'), position: companyLatLng, ); ... class _CustomGoogleMap extends StatelessWidget { final CameraPosition initialPosition; final Circle circle; final Marker marker; const _CustomGoogleMap({ required this.initialPosition, required this.circle, required this.marker, super.key, }); @override Widget build(BuildContext context) { return Expanded( flex: 2, child: GoogleMap( mapType: MapType.normal, // hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등 initialCameraPosition: initialPosition, myLocationEnabled: true, myLocationButtonEnabled: false, circles: Set.from([circle]), markers: Set.from([marker]), ), ); } }
현재 위치에 따라 원 색상 변화시키기
static final double okDistance = 100;
static final Circle withInDistanceCircle = Circle(
circleId: CircleId('withInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.blue.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.blue,
strokeWidth: 1,
);
static final Circle notWithInDistanceCircle = Circle(
circleId: CircleId('notWithInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.red.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.red,
strokeWidth: 1,
);
static final Circle checkDoneCircle = Circle(
circleId: CircleId('checkDoneCircle'),
center: companyLatLng,
fillColor: Colors.green.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.green,
strokeWidth: 1,
);
...
if (snapshot.data == '위치 권한이 허가되었습니다.') {
return StreamBuilder<Position>(
// FutureBuilder 와 같음
stream: Geolocator.getPositionStream(),
// position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨
builder: (context, snapshot) {
bool isWithinRange = false;
if (snapshot.hasData) {
final start = snapshot.data!;
final end = companyLatLng;
final distance = Geolocator.distanceBetween(
start.latitude,
start.longitude,
end.latitude,
end.longitude,
); // 현재 거리와 회사 거리가 100m 이내인지 계산
if (distance < okDistance) {
isWithinRange = true;
}
}
return Column(
children: [
_CustomGoogleMap(
initialPosition: initialPosition,
circle: isWithinRange
? withInDistanceCircle
: notWithInDistanceCircle,
marker: marker,
),
_ChoolCheckButton(),
],
);
});
}
static final double okDistance = 100;
static final Circle withInDistanceCircle = Circle(
circleId: CircleId('withInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.blue.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.blue,
strokeWidth: 1,
);
static final Circle notWithInDistanceCircle = Circle(
circleId: CircleId('notWithInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.red.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.red,
strokeWidth: 1,
);
static final Circle checkDoneCircle = Circle(
circleId: CircleId('checkDoneCircle'),
center: companyLatLng,
fillColor: Colors.green.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.green,
strokeWidth: 1,
);
...
if (snapshot.data == '위치 권한이 허가되었습니다.') {
return StreamBuilder<Position>(
// FutureBuilder 와 같음
stream: Geolocator.getPositionStream(),
// position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨
builder: (context, snapshot) {
bool isWithinRange = false;
if (snapshot.hasData) {
final start = snapshot.data!;
final end = companyLatLng;
final distance = Geolocator.distanceBetween(
start.latitude,
start.longitude,
end.latitude,
end.longitude,
); // 현재 거리와 회사 거리가 100m 이내인지 계산
if (distance < okDistance) {
isWithinRange = true;
}
}
return Column(
children: [
_CustomGoogleMap(
initialPosition: initialPosition,
circle: isWithinRange
? withInDistanceCircle
: notWithInDistanceCircle,
marker: marker,
),
_ChoolCheckButton(),
],
);
});
}
static final double okDistance = 100; static final Circle withInDistanceCircle = Circle( circleId: CircleId('withInDistanceCircle'), center: companyLatLng, fillColor: Colors.blue.withOpacity(0.5), radius: okDistance, strokeColor: Colors.blue, strokeWidth: 1, ); static final Circle notWithInDistanceCircle = Circle( circleId: CircleId('notWithInDistanceCircle'), center: companyLatLng, fillColor: Colors.red.withOpacity(0.5), radius: okDistance, strokeColor: Colors.red, strokeWidth: 1, ); static final Circle checkDoneCircle = Circle( circleId: CircleId('checkDoneCircle'), center: companyLatLng, fillColor: Colors.green.withOpacity(0.5), radius: okDistance, strokeColor: Colors.green, strokeWidth: 1, ); ... if (snapshot.data == '위치 권한이 허가되었습니다.') { return StreamBuilder<Position>( // FutureBuilder 와 같음 stream: Geolocator.getPositionStream(), // position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨 builder: (context, snapshot) { bool isWithinRange = false; if (snapshot.hasData) { final start = snapshot.data!; final end = companyLatLng; final distance = Geolocator.distanceBetween( start.latitude, start.longitude, end.latitude, end.longitude, ); // 현재 거리와 회사 거리가 100m 이내인지 계산 if (distance < okDistance) { isWithinRange = true; } } return Column( children: [ _CustomGoogleMap( initialPosition: initialPosition, circle: isWithinRange ? withInDistanceCircle : notWithInDistanceCircle, marker: marker, ), _ChoolCheckButton(), ], ); }); }
Dialog 로 출근 여부 받기 / 상태에 따라 원 변화시키기
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool choolCheckDone = false;
// latitude 위도 , longitude 경도
static final LatLng companyLatLng = LatLng(
10.8007,
106.7146,
);
static final CameraPosition initialPosition = CameraPosition(
target: companyLatLng, // 최초 포지션
zoom: 15, // zoom level
);
static final double okDistance = 100;
static final Circle withInDistanceCircle = Circle(
circleId: CircleId('withInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.blue.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.blue,
strokeWidth: 1,
);
static final Circle notWithInDistanceCircle = Circle(
circleId: CircleId('notWithInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.red.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.red,
strokeWidth: 1,
);
static final Circle checkDoneCircle = Circle(
circleId: CircleId('checkDoneCircle'),
center: companyLatLng,
fillColor: Colors.green.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.green,
strokeWidth: 1,
);
static final Marker marker = Marker(
markerId: MarkerId('marker'),
position: companyLatLng,
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: FutureBuilder<String>(
// FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다.
future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다.
builder: (BuildContext context, AsyncSnapshot snapshot) {
// future 에 넣은 함수의 리턴값은 snapshot에 들어간다.
print(snapshot
.connectionState); // none, waiting, active, done (future 함수의 진행상태)
print(snapshot.data); // '위치 권한이 허가되었습니다'
if (snapshot.connectionState == ConnectionState.waiting) {
// 아직 사용자의 응답을 기다리고 있을때는 로딩바
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data == '위치 권한이 허가되었습니다.') {
return StreamBuilder<Position>(
// FutureBuilder 와 같음
stream: Geolocator.getPositionStream(),
// position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨
builder: (context, snapshot) {
bool isWithinRange = false;
if (snapshot.hasData) {
final start = snapshot.data!;
final end = companyLatLng;
final distance = Geolocator.distanceBetween(
start.latitude,
start.longitude,
end.latitude,
end.longitude,
); // 현재 거리와 회사 거리가 100m 이내인지 계산
if (distance < okDistance) {
isWithinRange = true;
}
}
return Column(
children: [
_CustomGoogleMap(
initialPosition: initialPosition,
circle: choolCheckDone
? checkDoneCircle
: isWithinRange
? withInDistanceCircle
: notWithInDistanceCircle,
marker: marker,
),
_ChoolCheckButton(
isWithinRange: isWithinRange,
choolCheckDone: choolCheckDone,
onPressed: onChoolCheckPressed,
),
],
);
});
}
return Center(
child: Text(snapshot.data),
);
},
),
);
}
onChoolCheckPressed() async {
final result = await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
// alert dialog 를 쉽게 제작할 수 있음
title: Text('출근하기'),
content: Text('출근을 하시겠습니까?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(
false); // dialog 를 하나의 스크린처럼 생각하면됨. 스크린 전환에서 뒤돌아가기 처럼 pop()
},
child: Text('취소'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text('출근하기'),
)
],
);
},
);
if (result) {
setState(() {
choolCheckDone = true;
});
}
}
Future<String> checkPermission() async {
// 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
final isLocationEnabled = await Geolocator
.isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)
if (!isLocationEnabled) {
return '위치 서비스를 활성화 해주세요.';
}
/*
* LocationPermission 타입
* denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
* deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
* whileInUse : 앱 실행중에만 권한 허가
* always : 항상 허가
* */
LocationPermission checkedPermission =
await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
if (checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
if (checkedPermission == LocationPermission.denied) {
return '위치 권한을 허가해주세요.';
}
}
if (checkedPermission == LocationPermission.deniedForever) {
return '앱의 위치 권한을 세팅에서 허가해주세요.';
}
return '위치 권한이 허가되었습니다.';
}
AppBar renderAppBar() {
return AppBar(
title: Center(
child: Text(
'오늘도 출근',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w700,
),
),
),
backgroundColor: Colors.white,
);
}
}
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
final Circle circle;
final Marker marker;
const _CustomGoogleMap({
required this.initialPosition,
required this.circle,
required this.marker,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
initialCameraPosition: initialPosition,
myLocationEnabled: true,
myLocationButtonEnabled: false,
circles: Set.from([circle]),
markers: Set.from([marker]),
),
);
}
}
class _ChoolCheckButton extends StatelessWidget {
final bool isWithinRange;
final VoidCallback onPressed;
final bool choolCheckDone;
const _ChoolCheckButton({
required this.isWithinRange,
required this.onPressed,
required this.choolCheckDone,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.timelapse_outlined,
size: 50.0,
color: choolCheckDone
? Colors.green
: isWithinRange
? Colors.blue
: Colors.red,
),
const SizedBox(height: 20.0),
if (!choolCheckDone && isWithinRange)
TextButton(
onPressed: onPressed,
child: Text('출근하기'),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool choolCheckDone = false;
// latitude 위도 , longitude 경도
static final LatLng companyLatLng = LatLng(
10.8007,
106.7146,
);
static final CameraPosition initialPosition = CameraPosition(
target: companyLatLng, // 최초 포지션
zoom: 15, // zoom level
);
static final double okDistance = 100;
static final Circle withInDistanceCircle = Circle(
circleId: CircleId('withInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.blue.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.blue,
strokeWidth: 1,
);
static final Circle notWithInDistanceCircle = Circle(
circleId: CircleId('notWithInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.red.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.red,
strokeWidth: 1,
);
static final Circle checkDoneCircle = Circle(
circleId: CircleId('checkDoneCircle'),
center: companyLatLng,
fillColor: Colors.green.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.green,
strokeWidth: 1,
);
static final Marker marker = Marker(
markerId: MarkerId('marker'),
position: companyLatLng,
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: FutureBuilder<String>(
// FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다.
future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다.
builder: (BuildContext context, AsyncSnapshot snapshot) {
// future 에 넣은 함수의 리턴값은 snapshot에 들어간다.
print(snapshot
.connectionState); // none, waiting, active, done (future 함수의 진행상태)
print(snapshot.data); // '위치 권한이 허가되었습니다'
if (snapshot.connectionState == ConnectionState.waiting) {
// 아직 사용자의 응답을 기다리고 있을때는 로딩바
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data == '위치 권한이 허가되었습니다.') {
return StreamBuilder<Position>(
// FutureBuilder 와 같음
stream: Geolocator.getPositionStream(),
// position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨
builder: (context, snapshot) {
bool isWithinRange = false;
if (snapshot.hasData) {
final start = snapshot.data!;
final end = companyLatLng;
final distance = Geolocator.distanceBetween(
start.latitude,
start.longitude,
end.latitude,
end.longitude,
); // 현재 거리와 회사 거리가 100m 이내인지 계산
if (distance < okDistance) {
isWithinRange = true;
}
}
return Column(
children: [
_CustomGoogleMap(
initialPosition: initialPosition,
circle: choolCheckDone
? checkDoneCircle
: isWithinRange
? withInDistanceCircle
: notWithInDistanceCircle,
marker: marker,
),
_ChoolCheckButton(
isWithinRange: isWithinRange,
choolCheckDone: choolCheckDone,
onPressed: onChoolCheckPressed,
),
],
);
});
}
return Center(
child: Text(snapshot.data),
);
},
),
);
}
onChoolCheckPressed() async {
final result = await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
// alert dialog 를 쉽게 제작할 수 있음
title: Text('출근하기'),
content: Text('출근을 하시겠습니까?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(
false); // dialog 를 하나의 스크린처럼 생각하면됨. 스크린 전환에서 뒤돌아가기 처럼 pop()
},
child: Text('취소'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text('출근하기'),
)
],
);
},
);
if (result) {
setState(() {
choolCheckDone = true;
});
}
}
Future<String> checkPermission() async {
// 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
final isLocationEnabled = await Geolocator
.isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)
if (!isLocationEnabled) {
return '위치 서비스를 활성화 해주세요.';
}
/*
* LocationPermission 타입
* denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
* deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
* whileInUse : 앱 실행중에만 권한 허가
* always : 항상 허가
* */
LocationPermission checkedPermission =
await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
if (checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
if (checkedPermission == LocationPermission.denied) {
return '위치 권한을 허가해주세요.';
}
}
if (checkedPermission == LocationPermission.deniedForever) {
return '앱의 위치 권한을 세팅에서 허가해주세요.';
}
return '위치 권한이 허가되었습니다.';
}
AppBar renderAppBar() {
return AppBar(
title: Center(
child: Text(
'오늘도 출근',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w700,
),
),
),
backgroundColor: Colors.white,
);
}
}
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
final Circle circle;
final Marker marker;
const _CustomGoogleMap({
required this.initialPosition,
required this.circle,
required this.marker,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
initialCameraPosition: initialPosition,
myLocationEnabled: true,
myLocationButtonEnabled: false,
circles: Set.from([circle]),
markers: Set.from([marker]),
),
);
}
}
class _ChoolCheckButton extends StatelessWidget {
final bool isWithinRange;
final VoidCallback onPressed;
final bool choolCheckDone;
const _ChoolCheckButton({
required this.isWithinRange,
required this.onPressed,
required this.choolCheckDone,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.timelapse_outlined,
size: 50.0,
color: choolCheckDone
? Colors.green
: isWithinRange
? Colors.blue
: Colors.red,
),
const SizedBox(height: 20.0),
if (!choolCheckDone && isWithinRange)
TextButton(
onPressed: onPressed,
child: Text('출근하기'),
),
],
),
);
}
}
import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { bool choolCheckDone = false; // latitude 위도 , longitude 경도 static final LatLng companyLatLng = LatLng( 10.8007, 106.7146, ); static final CameraPosition initialPosition = CameraPosition( target: companyLatLng, // 최초 포지션 zoom: 15, // zoom level ); static final double okDistance = 100; static final Circle withInDistanceCircle = Circle( circleId: CircleId('withInDistanceCircle'), center: companyLatLng, fillColor: Colors.blue.withOpacity(0.5), radius: okDistance, strokeColor: Colors.blue, strokeWidth: 1, ); static final Circle notWithInDistanceCircle = Circle( circleId: CircleId('notWithInDistanceCircle'), center: companyLatLng, fillColor: Colors.red.withOpacity(0.5), radius: okDistance, strokeColor: Colors.red, strokeWidth: 1, ); static final Circle checkDoneCircle = Circle( circleId: CircleId('checkDoneCircle'), center: companyLatLng, fillColor: Colors.green.withOpacity(0.5), radius: okDistance, strokeColor: Colors.green, strokeWidth: 1, ); static final Marker marker = Marker( markerId: MarkerId('marker'), position: companyLatLng, ); @override Widget build(BuildContext context) { return Scaffold( appBar: renderAppBar(), body: FutureBuilder<String>( // FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다. future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다. builder: (BuildContext context, AsyncSnapshot snapshot) { // future 에 넣은 함수의 리턴값은 snapshot에 들어간다. print(snapshot .connectionState); // none, waiting, active, done (future 함수의 진행상태) print(snapshot.data); // '위치 권한이 허가되었습니다' if (snapshot.connectionState == ConnectionState.waiting) { // 아직 사용자의 응답을 기다리고 있을때는 로딩바 return Center( child: CircularProgressIndicator(), ); } if (snapshot.data == '위치 권한이 허가되었습니다.') { return StreamBuilder<Position>( // FutureBuilder 와 같음 stream: Geolocator.getPositionStream(), // position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨 builder: (context, snapshot) { bool isWithinRange = false; if (snapshot.hasData) { final start = snapshot.data!; final end = companyLatLng; final distance = Geolocator.distanceBetween( start.latitude, start.longitude, end.latitude, end.longitude, ); // 현재 거리와 회사 거리가 100m 이내인지 계산 if (distance < okDistance) { isWithinRange = true; } } return Column( children: [ _CustomGoogleMap( initialPosition: initialPosition, circle: choolCheckDone ? checkDoneCircle : isWithinRange ? withInDistanceCircle : notWithInDistanceCircle, marker: marker, ), _ChoolCheckButton( isWithinRange: isWithinRange, choolCheckDone: choolCheckDone, onPressed: onChoolCheckPressed, ), ], ); }); } return Center( child: Text(snapshot.data), ); }, ), ); } onChoolCheckPressed() async { final result = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( // alert dialog 를 쉽게 제작할 수 있음 title: Text('출근하기'), content: Text('출근을 하시겠습니까?'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop( false); // dialog 를 하나의 스크린처럼 생각하면됨. 스크린 전환에서 뒤돌아가기 처럼 pop() }, child: Text('취소'), ), TextButton( onPressed: () { Navigator.of(context).pop(true); }, child: Text('출근하기'), ) ], ); }, ); if (result) { setState(() { choolCheckDone = true; }); } } Future<String> checkPermission() async { // 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문 final isLocationEnabled = await Geolocator .isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스) if (!isLocationEnabled) { return '위치 서비스를 활성화 해주세요.'; } /* * LocationPermission 타입 * denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨 * deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함) * whileInUse : 앱 실행중에만 권한 허가 * always : 항상 허가 * */ LocationPermission checkedPermission = await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인 if (checkedPermission == LocationPermission.denied) { checkedPermission = await Geolocator.requestPermission(); if (checkedPermission == LocationPermission.denied) { return '위치 권한을 허가해주세요.'; } } if (checkedPermission == LocationPermission.deniedForever) { return '앱의 위치 권한을 세팅에서 허가해주세요.'; } return '위치 권한이 허가되었습니다.'; } AppBar renderAppBar() { return AppBar( title: Center( child: Text( '오늘도 출근', style: TextStyle( color: Colors.blue, fontWeight: FontWeight.w700, ), ), ), backgroundColor: Colors.white, ); } } class _CustomGoogleMap extends StatelessWidget { final CameraPosition initialPosition; final Circle circle; final Marker marker; const _CustomGoogleMap({ required this.initialPosition, required this.circle, required this.marker, super.key, }); @override Widget build(BuildContext context) { return Expanded( flex: 2, child: GoogleMap( mapType: MapType.normal, initialCameraPosition: initialPosition, myLocationEnabled: true, myLocationButtonEnabled: false, circles: Set.from([circle]), markers: Set.from([marker]), ), ); } } class _ChoolCheckButton extends StatelessWidget { final bool isWithinRange; final VoidCallback onPressed; final bool choolCheckDone; const _ChoolCheckButton({ required this.isWithinRange, required this.onPressed, required this.choolCheckDone, super.key, }); @override Widget build(BuildContext context) { return Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.timelapse_outlined, size: 50.0, color: choolCheckDone ? Colors.green : isWithinRange ? Colors.blue : Colors.red, ), const SizedBox(height: 20.0), if (!choolCheckDone && isWithinRange) TextButton( onPressed: onPressed, child: Text('출근하기'), ), ], ), ); } }
현재 위치로 이동 기능 추가 + 마지막 정리
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool choolCheckDone = false;
GoogleMapController? mapController;
// latitude 위도 , longitude 경도
static final LatLng companyLatLng = LatLng(
10.8007,
106.7146,
);
static final CameraPosition initialPosition = CameraPosition(
target: companyLatLng, // 최초 포지션
zoom: 15, // zoom level
);
static final double okDistance = 100;
static final Circle withInDistanceCircle = Circle(
circleId: CircleId('withInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.blue.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.blue,
strokeWidth: 1,
);
static final Circle notWithInDistanceCircle = Circle(
circleId: CircleId('notWithInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.red.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.red,
strokeWidth: 1,
);
static final Circle checkDoneCircle = Circle(
circleId: CircleId('checkDoneCircle'),
center: companyLatLng,
fillColor: Colors.green.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.green,
strokeWidth: 1,
);
static final Marker marker = Marker(
markerId: MarkerId('marker'),
position: companyLatLng,
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: FutureBuilder<String>(
// FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다.
future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다.
builder: (BuildContext context, AsyncSnapshot snapshot) {
// future 에 넣은 함수의 리턴값은 snapshot에 들어간다.
print(snapshot
.connectionState); // none, waiting, active, done (future 함수의 진행상태)
print(snapshot.data); // '위치 권한이 허가되었습니다'
if (snapshot.connectionState == ConnectionState.waiting) {
// 아직 사용자의 응답을 기다리고 있을때는 로딩바
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data == '위치 권한이 허가되었습니다.') {
return StreamBuilder<Position>(
// FutureBuilder 와 같음
stream: Geolocator.getPositionStream(),
// position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨
builder: (context, snapshot) {
bool isWithinRange = false;
if (snapshot.hasData) {
final start = snapshot.data!;
final end = companyLatLng;
final distance = Geolocator.distanceBetween(
start.latitude,
start.longitude,
end.latitude,
end.longitude,
); // 현재 거리와 회사 거리가 100m 이내인지 계산
if (distance < okDistance) {
isWithinRange = true;
}
}
return Column(
children: [
_CustomGoogleMap(
initialPosition: initialPosition,
circle: choolCheckDone
? checkDoneCircle
: isWithinRange
? withInDistanceCircle
: notWithInDistanceCircle,
marker: marker,
onMapCreated: onMapCreated,
),
_ChoolCheckButton(
isWithinRange: isWithinRange,
choolCheckDone: choolCheckDone,
onPressed: onChoolCheckPressed,
),
],
);
});
}
return Center(
child: Text(snapshot.data),
);
},
),
);
}
onMapCreated(GoogleMapController controller) {
mapController = controller;
}
onChoolCheckPressed() async {
final result = await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
// alert dialog 를 쉽게 제작할 수 있음
title: Text('출근하기'),
content: Text('출근을 하시겠습니까?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(
false); // dialog 를 하나의 스크린처럼 생각하면됨. 스크린 전환에서 뒤돌아가기 처럼 pop()
},
child: Text('취소'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text('출근하기'),
)
],
);
},
);
if (result) {
setState(() {
choolCheckDone = true;
});
}
}
Future<String> checkPermission() async {
// 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
final isLocationEnabled = await Geolocator
.isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)
if (!isLocationEnabled) {
return '위치 서비스를 활성화 해주세요.';
}
/*
* LocationPermission 타입
* denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
* deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
* whileInUse : 앱 실행중에만 권한 허가
* always : 항상 허가
* */
LocationPermission checkedPermission =
await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
if (checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
if (checkedPermission == LocationPermission.denied) {
return '위치 권한을 허가해주세요.';
}
}
if (checkedPermission == LocationPermission.deniedForever) {
return '앱의 위치 권한을 세팅에서 허가해주세요.';
}
return '위치 권한이 허가되었습니다.';
}
AppBar renderAppBar() {
return AppBar(
title: Text(
'오늘도 출근',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w700,
),
),
backgroundColor: Colors.white,
actions: [
IconButton(
onPressed: () async {
if (mapController == null) {
return;
}
final location = await Geolocator.getCurrentPosition(); // 현재 위치 구하기
mapController!.animateCamera(CameraUpdate.newLatLng(LatLng( // 현재 위치로 카메라 이동
location.latitude,
location.longitude,
)));
},
color: Colors.blue,
icon: Icon(Icons.my_location),
)
],
);
}
}
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
final Circle circle;
final Marker marker;
final MapCreatedCallback onMapCreated;
const _CustomGoogleMap({
required this.initialPosition,
required this.circle,
required this.marker,
required this.onMapCreated,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
// hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
initialCameraPosition: initialPosition,
myLocationEnabled: true,
myLocationButtonEnabled: false,
circles: Set.from([circle]),
markers: Set.from([marker]),
onMapCreated: onMapCreated,
),
);
}
}
class _ChoolCheckButton extends StatelessWidget {
final bool isWithinRange;
final VoidCallback onPressed;
final bool choolCheckDone;
const _ChoolCheckButton({
required this.isWithinRange,
required this.onPressed,
required this.choolCheckDone,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.timelapse_outlined,
size: 50.0,
color: choolCheckDone
? Colors.green
: isWithinRange
? Colors.blue
: Colors.red,
),
const SizedBox(height: 20.0),
if (!choolCheckDone && isWithinRange)
TextButton(
onPressed: onPressed,
child: Text('출근하기'),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool choolCheckDone = false;
GoogleMapController? mapController;
// latitude 위도 , longitude 경도
static final LatLng companyLatLng = LatLng(
10.8007,
106.7146,
);
static final CameraPosition initialPosition = CameraPosition(
target: companyLatLng, // 최초 포지션
zoom: 15, // zoom level
);
static final double okDistance = 100;
static final Circle withInDistanceCircle = Circle(
circleId: CircleId('withInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.blue.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.blue,
strokeWidth: 1,
);
static final Circle notWithInDistanceCircle = Circle(
circleId: CircleId('notWithInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.red.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.red,
strokeWidth: 1,
);
static final Circle checkDoneCircle = Circle(
circleId: CircleId('checkDoneCircle'),
center: companyLatLng,
fillColor: Colors.green.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.green,
strokeWidth: 1,
);
static final Marker marker = Marker(
markerId: MarkerId('marker'),
position: companyLatLng,
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: FutureBuilder<String>(
// FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다.
future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다.
builder: (BuildContext context, AsyncSnapshot snapshot) {
// future 에 넣은 함수의 리턴값은 snapshot에 들어간다.
print(snapshot
.connectionState); // none, waiting, active, done (future 함수의 진행상태)
print(snapshot.data); // '위치 권한이 허가되었습니다'
if (snapshot.connectionState == ConnectionState.waiting) {
// 아직 사용자의 응답을 기다리고 있을때는 로딩바
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data == '위치 권한이 허가되었습니다.') {
return StreamBuilder<Position>(
// FutureBuilder 와 같음
stream: Geolocator.getPositionStream(),
// position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨
builder: (context, snapshot) {
bool isWithinRange = false;
if (snapshot.hasData) {
final start = snapshot.data!;
final end = companyLatLng;
final distance = Geolocator.distanceBetween(
start.latitude,
start.longitude,
end.latitude,
end.longitude,
); // 현재 거리와 회사 거리가 100m 이내인지 계산
if (distance < okDistance) {
isWithinRange = true;
}
}
return Column(
children: [
_CustomGoogleMap(
initialPosition: initialPosition,
circle: choolCheckDone
? checkDoneCircle
: isWithinRange
? withInDistanceCircle
: notWithInDistanceCircle,
marker: marker,
onMapCreated: onMapCreated,
),
_ChoolCheckButton(
isWithinRange: isWithinRange,
choolCheckDone: choolCheckDone,
onPressed: onChoolCheckPressed,
),
],
);
});
}
return Center(
child: Text(snapshot.data),
);
},
),
);
}
onMapCreated(GoogleMapController controller) {
mapController = controller;
}
onChoolCheckPressed() async {
final result = await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
// alert dialog 를 쉽게 제작할 수 있음
title: Text('출근하기'),
content: Text('출근을 하시겠습니까?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(
false); // dialog 를 하나의 스크린처럼 생각하면됨. 스크린 전환에서 뒤돌아가기 처럼 pop()
},
child: Text('취소'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text('출근하기'),
)
],
);
},
);
if (result) {
setState(() {
choolCheckDone = true;
});
}
}
Future<String> checkPermission() async {
// 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
final isLocationEnabled = await Geolocator
.isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)
if (!isLocationEnabled) {
return '위치 서비스를 활성화 해주세요.';
}
/*
* LocationPermission 타입
* denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
* deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
* whileInUse : 앱 실행중에만 권한 허가
* always : 항상 허가
* */
LocationPermission checkedPermission =
await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
if (checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
if (checkedPermission == LocationPermission.denied) {
return '위치 권한을 허가해주세요.';
}
}
if (checkedPermission == LocationPermission.deniedForever) {
return '앱의 위치 권한을 세팅에서 허가해주세요.';
}
return '위치 권한이 허가되었습니다.';
}
AppBar renderAppBar() {
return AppBar(
title: Text(
'오늘도 출근',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w700,
),
),
backgroundColor: Colors.white,
actions: [
IconButton(
onPressed: () async {
if (mapController == null) {
return;
}
final location = await Geolocator.getCurrentPosition(); // 현재 위치 구하기
mapController!.animateCamera(CameraUpdate.newLatLng(LatLng( // 현재 위치로 카메라 이동
location.latitude,
location.longitude,
)));
},
color: Colors.blue,
icon: Icon(Icons.my_location),
)
],
);
}
}
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
final Circle circle;
final Marker marker;
final MapCreatedCallback onMapCreated;
const _CustomGoogleMap({
required this.initialPosition,
required this.circle,
required this.marker,
required this.onMapCreated,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
// hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
initialCameraPosition: initialPosition,
myLocationEnabled: true,
myLocationButtonEnabled: false,
circles: Set.from([circle]),
markers: Set.from([marker]),
onMapCreated: onMapCreated,
),
);
}
}
class _ChoolCheckButton extends StatelessWidget {
final bool isWithinRange;
final VoidCallback onPressed;
final bool choolCheckDone;
const _ChoolCheckButton({
required this.isWithinRange,
required this.onPressed,
required this.choolCheckDone,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.timelapse_outlined,
size: 50.0,
color: choolCheckDone
? Colors.green
: isWithinRange
? Colors.blue
: Colors.red,
),
const SizedBox(height: 20.0),
if (!choolCheckDone && isWithinRange)
TextButton(
onPressed: onPressed,
child: Text('출근하기'),
),
],
),
);
}
}
import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { bool choolCheckDone = false; GoogleMapController? mapController; // latitude 위도 , longitude 경도 static final LatLng companyLatLng = LatLng( 10.8007, 106.7146, ); static final CameraPosition initialPosition = CameraPosition( target: companyLatLng, // 최초 포지션 zoom: 15, // zoom level ); static final double okDistance = 100; static final Circle withInDistanceCircle = Circle( circleId: CircleId('withInDistanceCircle'), center: companyLatLng, fillColor: Colors.blue.withOpacity(0.5), radius: okDistance, strokeColor: Colors.blue, strokeWidth: 1, ); static final Circle notWithInDistanceCircle = Circle( circleId: CircleId('notWithInDistanceCircle'), center: companyLatLng, fillColor: Colors.red.withOpacity(0.5), radius: okDistance, strokeColor: Colors.red, strokeWidth: 1, ); static final Circle checkDoneCircle = Circle( circleId: CircleId('checkDoneCircle'), center: companyLatLng, fillColor: Colors.green.withOpacity(0.5), radius: okDistance, strokeColor: Colors.green, strokeWidth: 1, ); static final Marker marker = Marker( markerId: MarkerId('marker'), position: companyLatLng, ); @override Widget build(BuildContext context) { return Scaffold( appBar: renderAppBar(), body: FutureBuilder<String>( // FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다. future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다. builder: (BuildContext context, AsyncSnapshot snapshot) { // future 에 넣은 함수의 리턴값은 snapshot에 들어간다. print(snapshot .connectionState); // none, waiting, active, done (future 함수의 진행상태) print(snapshot.data); // '위치 권한이 허가되었습니다' if (snapshot.connectionState == ConnectionState.waiting) { // 아직 사용자의 응답을 기다리고 있을때는 로딩바 return Center( child: CircularProgressIndicator(), ); } if (snapshot.data == '위치 권한이 허가되었습니다.') { return StreamBuilder<Position>( // FutureBuilder 와 같음 stream: Geolocator.getPositionStream(), // position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨 builder: (context, snapshot) { bool isWithinRange = false; if (snapshot.hasData) { final start = snapshot.data!; final end = companyLatLng; final distance = Geolocator.distanceBetween( start.latitude, start.longitude, end.latitude, end.longitude, ); // 현재 거리와 회사 거리가 100m 이내인지 계산 if (distance < okDistance) { isWithinRange = true; } } return Column( children: [ _CustomGoogleMap( initialPosition: initialPosition, circle: choolCheckDone ? checkDoneCircle : isWithinRange ? withInDistanceCircle : notWithInDistanceCircle, marker: marker, onMapCreated: onMapCreated, ), _ChoolCheckButton( isWithinRange: isWithinRange, choolCheckDone: choolCheckDone, onPressed: onChoolCheckPressed, ), ], ); }); } return Center( child: Text(snapshot.data), ); }, ), ); } onMapCreated(GoogleMapController controller) { mapController = controller; } onChoolCheckPressed() async { final result = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( // alert dialog 를 쉽게 제작할 수 있음 title: Text('출근하기'), content: Text('출근을 하시겠습니까?'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop( false); // dialog 를 하나의 스크린처럼 생각하면됨. 스크린 전환에서 뒤돌아가기 처럼 pop() }, child: Text('취소'), ), TextButton( onPressed: () { Navigator.of(context).pop(true); }, child: Text('출근하기'), ) ], ); }, ); if (result) { setState(() { choolCheckDone = true; }); } } Future<String> checkPermission() async { // 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문 final isLocationEnabled = await Geolocator .isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스) if (!isLocationEnabled) { return '위치 서비스를 활성화 해주세요.'; } /* * LocationPermission 타입 * denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨 * deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함) * whileInUse : 앱 실행중에만 권한 허가 * always : 항상 허가 * */ LocationPermission checkedPermission = await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인 if (checkedPermission == LocationPermission.denied) { checkedPermission = await Geolocator.requestPermission(); if (checkedPermission == LocationPermission.denied) { return '위치 권한을 허가해주세요.'; } } if (checkedPermission == LocationPermission.deniedForever) { return '앱의 위치 권한을 세팅에서 허가해주세요.'; } return '위치 권한이 허가되었습니다.'; } AppBar renderAppBar() { return AppBar( title: Text( '오늘도 출근', style: TextStyle( color: Colors.blue, fontWeight: FontWeight.w700, ), ), backgroundColor: Colors.white, actions: [ IconButton( onPressed: () async { if (mapController == null) { return; } final location = await Geolocator.getCurrentPosition(); // 현재 위치 구하기 mapController!.animateCamera(CameraUpdate.newLatLng(LatLng( // 현재 위치로 카메라 이동 location.latitude, location.longitude, ))); }, color: Colors.blue, icon: Icon(Icons.my_location), ) ], ); } } class _CustomGoogleMap extends StatelessWidget { final CameraPosition initialPosition; final Circle circle; final Marker marker; final MapCreatedCallback onMapCreated; const _CustomGoogleMap({ required this.initialPosition, required this.circle, required this.marker, required this.onMapCreated, super.key, }); @override Widget build(BuildContext context) { return Expanded( flex: 2, child: GoogleMap( mapType: MapType.normal, // hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등 initialCameraPosition: initialPosition, myLocationEnabled: true, myLocationButtonEnabled: false, circles: Set.from([circle]), markers: Set.from([marker]), onMapCreated: onMapCreated, ), ); } } class _ChoolCheckButton extends StatelessWidget { final bool isWithinRange; final VoidCallback onPressed; final bool choolCheckDone; const _ChoolCheckButton({ required this.isWithinRange, required this.onPressed, required this.choolCheckDone, super.key, }); @override Widget build(BuildContext context) { return Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.timelapse_outlined, size: 50.0, color: choolCheckDone ? Colors.green : isWithinRange ? Colors.blue : Colors.red, ), const SizedBox(height: 20.0), if (!choolCheckDone && isWithinRange) TextButton( onPressed: onPressed, child: Text('출근하기'), ), ], ), ); } }