사용자의 위치 위도와 경도를 찾고 데이터 저장하기
위치 데이터 얻기
1.구글에서 제공하는 위도 경도 정보 얻어주는 것 그래이들에 추가
2.위도 경도 데이터 클래스와 voewmodel작성
3.LocationUtils에 위도 경도 받아오는 함수 작성
4.display할 곳에서 위도 경도 지켜보는 뷰모델 매개변수로 받기 및 퍼미션 허용 했을 때 3번에서 작성한 함수 실행 하도록 호출
5.위도 경도 화면에 표시해주기 위해 viewModel에 있는 자료형이 State인 location의 value값 가져오기
package com.lululalal.locationapp
data class LocationData(
val latitude : Double,
val longitude : Double
)
package com.lululalal.locationapp
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
class LocationViewModel : ViewModel() {
private val _location = mutableStateOf<LocationData?>(null)
val location: State<LocationData?> = _location
//위치 업데이트 하는 함수
fun updateLocation(newLocation:LocationData) {
_location.value = newLocation
}
}
화면을 그리는 곳에서 권한이 있을 경우 찾은 위치를 화면에 표시해주기 위해 만들어 놓은 viewModel을 가져온다
//뷰모델
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0")
package com.lululalal.locationapp
import android.Manifest
import android.content.Context
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.app.ActivityCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.lululalal.locationapp.ui.theme.LocationAppTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val viewModel:LocationViewModel = viewModel()
LocationAppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MyApp(viewModel)
}
}
}
}
}
@Composable
fun MyApp(viewModel: LocationViewModel) {//부모 컴프저블 만들기
val context = LocalContext.current //지금 내가 있는 액티비티의 context 요청
val locationUtils = LocationUtils(context)
LocationDisplay(locationUtils = locationUtils, viewModel,context = context)
}
@Composable
fun LocationDisplay(
locationUtils: LocationUtils,
viewModel: LocationViewModel,
context: Context
) {
//권한 받기
//1. 권한 런처 표시
// rememberLauncherForActivityResult 사용
// 결과를 위해 액티비티를 시작 요청을 등록하는 것, 보이지 않는 곳에서 자동으로 관리 요청 코드 및 변환과 관련된 레코드가 레지스트리에 생성됨
//2. 권한이 왜 필요한지 알려줘야함
// rationaleRequired의 shouldShowRequestPermissionRationale() 메소드를 사용하여 왜 이 퍼미션이 필요한지에 대한 이유를 표시한다.
val requestPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestMultiplePermissions(), //contract는 permissions를 리턴함
onResult = {permissions ->
if (permissions[Manifest.permission.ACCESS_COARSE_LOCATION]==true
&& permissions[Manifest.permission.ACCESS_FINE_LOCATION]==true ) {
//위치에 권한 있다
//위도 -경도 얻오기
} else {
//권한 요청 필요
val rationaleRequired = ActivityCompat.shouldShowRequestPermissionRationale(
context as MainActivity, //메인 액티비치 말곤 다른창에선 이 팝업 열지말라는 등록
Manifest.permission.ACCESS_FINE_LOCATION
) || ActivityCompat.shouldShowRequestPermissionRationale(
context as MainActivity,
Manifest.permission.ACCESS_COARSE_LOCATION
)
//이 퍼미션이 필요한 이유를 설명해주기
if (rationaleRequired) {
Toast.makeText(context, "이 기능을 사용하려면 위치 권한이 필요합니다.", Toast.LENGTH_SHORT).show()
} else {
//rationaleRequired이 거부 상황일경우 사용자의 설정이나 안드로이드 폰 설정으로 이동
Toast.makeText(context, "설정에 들어가 위치권한을 활성화 해주세요.", Toast.LENGTH_SHORT).show()
}
}
}) //contract에서 리턴받은 permissions가 모두 허용됐는지 체크하는 부분
Column(modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Location not avaliable")
Button(onClick = {
if (locationUtils.hasLocationPermission(context)) {
//이미 권한이 있으면 엡데이트
} else {
//위치 권한 요청
//여기서 실제로 포미션 요청을 런칭함!
requestPermissionLauncher.launch(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
}
}) {
Text(text = "get location")
}
}
}
FusedLocaionProviderClient 매소드 사용하기 위해 추가 구글에서 제공하는 것 중 위도/경도 찾을 수 있는 기능 디펜던시
//Gms:play-services-location 추가
implementation ("com.google.android.gms:play-services-location:21.3.0")
Callback이란?
무언가를 요청한 후 해당 작업이 완료될 때까지 기다린 다음 반환해주는 것
LocationRequest.Builder
LocationRequest.Builder(위치 정확도, 얼마나 자주 탐색할것인지).build()
- Priority엔 4가지 속성 있음
정확도가 높을 수록 배터리 많이 사용
1.PRIORITY_HIGH_ACCURACY : 높은 정확도
2.PRIORITY_LOW_POWER : 배터리 덜 사용
3.PRIORITY_BALANCED_POWER_ACCURACY : 배터리와 정확도 균형
4.PRIORITY_PASSIVE : 배터리 아주 적게 사용
requestLocationUpdates
해당 기능을 사용하고 싶을 떄마다 권한을 요청해야됨
하지만 실제로 권한이 있을 떄만 이 함수를 실행 할 것이기 때문에 퍼미션을 무시하라는 어노테이션 붙여주기 : @SuppressLint("MissingPermission")Looper.getMainLooper() : 위치 업데이트를 위한 threading과 메세지 처리하는 데 사용함
특정 looper를 제공해 어떤 스레드 위치 업데이트가 전달 될 지 정할 수 있음 지금은 메인 루퍼 전달함
package com.lululalal.locationapp
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.os.Looper
import androidx.core.content.ContextCompat
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
class LocationUtils(val context: Context) {
//FusedLocationProviderClient -> 위도 경도 찾을 수 있음
private val _fusedLocationClient : FusedLocationProviderClient
= LocationServices.getFusedLocationProviderClient(context)
//실제 위치를 탐색할 함수
@SuppressLint("MissingPermission")
fun requestLocationUpdate(viewModel: LocationViewModel) {
//Callback : 무언가를 요청한 후 해당 작업이 완료될 때까지 기다린 다음 반환해주는 것
//시간이 걸릴 것 같은 경우 이 콜백 써준다
val locationCallback = object : LocationCallback(){
//클래스에서 상속받아 오버라이드 하는 대신 객체를 직접 만들고 그 내에서 바로 오버라이드 가능
override fun onLocationResult(locationResult: LocationResult) {
super.onLocationResult(locationResult)
//locationResult.lastLocation : 제일 최근 위치 반환
locationResult.lastLocation?.let {
//let 함수 안에서 it이 정보를 보여하고 있음
val location = LocationData(latitude = it.latitude, longitude = it.longitude) //it함수 안에서 정보 추출
viewModel.updateLocation(location) //뷰 모델에 위도경도 업데이트
}
}
}
//위치 요청 빌더 만들기
//LocationRequest.Builder(위치 정확도, 얼마나 자주 탐색할것인지).build()
//Priority엔 4가지 속성 있음
//정확도가 높을 수록 배터리 많이 사용
//1.PRIORITY_HIGH_ACCURACY : 높은 정확도
//2.PRIORITY_LOW_POWER : 배터리 덜 사용
//3.PRIORITY_BALANCED_POWER_ACCURACY : 배터리와 정확도 균형
//4.PRIORITY_PASSIVE : 배터리 아주 적게 사용
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000).build()
//_fusedLocationClient의 requestLocationUpdates 매소드 사용하여 모든것을 하나로 묶기
_fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
//-> 해당 기능을 사용하고 싶을 떄마다 권한을 요청해야됨
//하지만 실제로 권한이 있을 떄만 이 함수를 실행할것이기떄문에 퍼미션을 무시하라는 어노테이션 붙여주기 : @SuppressLint("MissingPermission")
//Looper.getMainLooper() : 위치 업데이트를 위한 threading과 메세지 처리하는 데 사용함
//특정 looper를 제공해 어떤 스레드 위치 업데이트가 전달 될 지 정할 수 있음 지금은 메인 루퍼 전달함
}
//위치 권한에 액세스가 있는지 없는지 확인하는 함수
fun hasLocationPermission(context: Context):Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED //PERMISSION_GRANTED문자열이 ACCESS_FINE_LOCATION에 있는지 체크하는 부분
&&
ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
}
//checkSelfPermission는 리턴 값이 int임 우린 Boolean이 필요 떄문에 PERMISSION_GRANTED와 같은 지 체크하여 액세스 승인했는지 체크함
}
package com.lululalal.locationapp
import android.Manifest
import android.content.Context
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.ActivityCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.lululalal.locationapp.ui.theme.LocationAppTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val viewModel:LocationViewModel = viewModel()
LocationAppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MyApp(viewModel)
}
}
}
}
}
@Composable
fun MyApp(viewModel: LocationViewModel) {//부모 컴프저블 만들기
val context = LocalContext.current //지금 내가 있는 액티비티의 context 요청
val locationUtils = LocationUtils(context)
LocationDisplay(locationUtils = locationUtils, viewModel,context = context)
}
@Composable
fun LocationDisplay(
locationUtils: LocationUtils,
viewModel: LocationViewModel,
context: Context
) {
//[1]권한 받기
//1. 권한 런처 표시
// rememberLauncherForActivityResult 사용
// 결과를 위해 액티비티를 시작 요청을 등록하는 것, 보이지 않는 곳에서 자동으로 관리 요청 코드 및 변환과 관련된 레코드가 레지스트리에 생성됨
//2. 권한이 왜 필요한지 알려줘야함
// rationaleRequired의 shouldShowRequestPermissionRationale() 메소드를 사용하여 왜 이 퍼미션이 필요한지에 대한 이유를 표시한다.
//[2]위치 데이터 얻기
//1.구글에서 제공하는 위도 경도 정보 얻어주는 것 그래이들에 추가
//2.위도 경도 데이터 클래스와 voewmodel작성
//3.LocationUtils에 위도 경도 받아오는 함수 작성
//4.display할 곳에서 위도 경도 지켜보는 뷰모델 매개변수로 받기 및 퍼미션 허용 했을 때 3번에서 작성한 함수 실행 하도록 호출
//5.위도 경도 화면에 표시해주기 위해 viewModel에 있는 자료형이 State인 location의 value값 가져오기
val location = viewModel.location.value
val requestPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestMultiplePermissions(), //contract는 permissions를 리턴함
onResult = {permissions ->
if (permissions[Manifest.permission.ACCESS_COARSE_LOCATION]==true
&& permissions[Manifest.permission.ACCESS_FINE_LOCATION]==true ) {
//위치에 권한 있다
//위도 -경도 셋팅하는 함수 호출!!
locationUtils.requestLocationUpdate(viewModel)
} else {
//권한 요청 필요
val rationaleRequired = ActivityCompat.shouldShowRequestPermissionRationale(
context as MainActivity, //메인 액티비치 말곤 다른창에선 이 팝업 열지말라는 등록
Manifest.permission.ACCESS_FINE_LOCATION
) || ActivityCompat.shouldShowRequestPermissionRationale(
context as MainActivity,
Manifest.permission.ACCESS_COARSE_LOCATION
)
//이 퍼미션이 필요한 이유를 설명해주기
if (rationaleRequired) {
Toast.makeText(context, "이 기능을 사용하려면 위치 권한이 필요합니다.", Toast.LENGTH_SHORT).show()
} else {
//rationaleRequired이 거부 상황일경우 사용자의 설정이나 안드로이드 폰 설정으로 이동
Toast.makeText(context, "설정에 들어가 위치권한을 활성화 해주세요.", Toast.LENGTH_SHORT).show()
}
}
}) //contract에서 리턴받은 permissions가 모두 허용됐는지 체크하는 부분
Column(modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
if (location != null) {
Text(text = "Address = ${location.longitude} : ${location.latitude}")
} else {
Text(text = "Location not available")
}
Button(onClick = {
if (locationUtils.hasLocationPermission(context)) {
//이미 권한이 있으면 엡데이트
} else {
//위치 권한 요청
//여기서 실제로 포미션 요청을 런칭함!
requestPermissionLauncher.launch(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
}
}) {
Text(text = "get location")
}
}
}