리저브콜드브루 2025. 3. 10. 16:47
728x90
반응형
 

5. 위치 정보 연동하기

앱의 메인홈화면에 사용될 현 위치 정보를 가져오고 표시하도록 작업을 진행하려고 한다.아래와 같이 단계별로 진행할 예정위치 정보 연동을 위한 권한 설정FusedLocationProvider를 이용한 위치 정

joyshu93.tistory.com

이전 프로젝트에서는 LocationProvider와, LocationDataStore 클래스를 만들어 사용했었다

Compose 프로젝트에 사용할 수 있도록 수정한다


LocationProvider.kt

package com.example.composeweatherapp

import ...

/***
 * Activity 의존성 제거
 * 권한 요청 방식 변경
 */
class LocationProvider(private val context: Context, private val locationDataStore: LocationDataStore) {
    private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)

    // MutableStateFlow<Location?>로 변경하여 Compose UI와 연동 가능
    private val _locationState = MutableStateFlow<Location?>(null)
    val locationState: StateFlow<Location?> = _locationState

    private var locationCallback: LocationCallback? = null

    // 위치를 가져오는 함수 (콜백을 통해 결과를 전달)
    fun fetchLocation() {
        if (ContextCompat.checkSelfPermission(
                context, Manifest.permission.ACCESS_FINE_LOCATION
            ) == PackageManager.PERMISSION_GRANTED
        ) {
            val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 0L)
                .setMaxUpdates(1)
                .build()

            // 기존 콜백이 있으면 제거 후 새로 등록
            locationCallback?.let {
                fusedLocationClient.removeLocationUpdates(it)
            }

            locationCallback = object : LocationCallback() {
                override fun onLocationResult(locationResult: LocationResult) {
                    val location = locationResult.lastLocation
                    if (location != null) {
                        _locationState.value = location // UI에서 즉시 반영되도록 상태 업데이트

                        // 위치 정보를 DataStore에 저장
                        CoroutineScope(Dispatchers.IO).launch {
                            locationDataStore.saveCurLatitude(location.latitude)
                            locationDataStore.saveCurLongitude(location.longitude)
                            locationDataStore.updateLocationKey(location.latitude, location.longitude)
                        }
                    }else {
                        Log.e("LocationProvider", "새로 요청한 위치도 null입니다.")
                    }

                    // 요청이 완료되었으므로 콜백 제거
                    locationCallback?.let {
                        fusedLocationClient.removeLocationUpdates(it)
                    }
                }
            }

            fusedLocationClient.requestLocationUpdates(
                locationRequest,
                locationCallback!!,
                Looper.getMainLooper()
            )
        }
        else {
            Log.e("LocationProvider", "위치 권한이 없습니다.")
        }
    }
}

// Composition에서 LocationProvider를 유지
@Composable
fun rememberLocationProvider(locationDataStore: LocationDataStore): LocationProvider {
    val context = LocalContext.current
    return remember { LocationProvider(context, locationDataStore) }
}

@Composable
fun LocationPermissionHandler(locationProvider: LocationProvider) {
    val context = LocalContext.current
    val permissionGranted = remember {
        mutableStateOf(
            ContextCompat.checkSelfPermission(
                context, Manifest.permission.ACCESS_FINE_LOCATION
            ) == PackageManager.PERMISSION_GRANTED
        )
    }

    // 권한 요청을 rememberLauncherForActivityResult()로 처리하여 Compose 환경에 맞춤
    val permissionLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission()
    ) { isGranted ->
        permissionGranted.value = isGranted
        if (isGranted) {
            locationProvider.fetchLocation()
        }
    }

    // 권한 변경 감지를 LaunchedEffect(permissionGranted.value)로 최적화
    // LaunchedEffect(Unit)을 활용해 앱 실행 시 자동 권한 요청
    // Unit 대신 permissionGranted.value를 감지하도록 변경
    // 권한이 이미 허용된 경우 불필요한 실행을 방지
    LaunchedEffect(permissionGranted.value) {
        if (!permissionGranted.value) {
            permissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
        }
    }
}

 

 

MainScreen.kt

@Composable
fun MainScreen() {

    val context = LocalContext.current
    val locationDataStore = remember { LocationDataStore(context) }
    val locationProvider = rememberLocationProvider(locationDataStore)

    // 위치 권한 요청 및 위치 업데이트
    LocationPermissionHandler(locationProvider)

    // 앱 실행 시마다 최신 위치 요청
    LaunchedEffect(Unit) { // Unit을 Key로 사용하면 앱이 실행될 때마다 fetchLocation()이 호출
        locationProvider.fetchLocation()
    }

    // 위치 데이터를 State로 관리
    var cityName by remember { mutableStateOf("위치 불명") }
    val coroutineScope = rememberCoroutineScope()

    val location = locationProvider.locationState.collectAsState().value

    // 위치가 변경될 때마다 지역명을 업데이트
    LaunchedEffect(location) {
        location?.let {
            coroutineScope.launch {
                val geocoder = Geocoder(context, Locale.getDefault())
                val addresses = geocoder.getFromLocation(it.latitude, it.longitude, 1)

                if (addresses != null) {
                    if (addresses.isNotEmpty()) {
                        cityName = addresses[0].locality ?: addresses[0].adminArea ?: "위치 불명"
                    } else {
                        Log.e("MainScreen", "Geocoder가 주소를 찾지 못했습니다.")
                    }
                }
            }
        }
    }

    TopWeatherInfo(cityName)
}

@Composable
fun TopWeatherInfo(cityName: String)
{
    Column(modifier = Modifier.fillMaxSize().padding(64.dp)) {
        Text(
            text = cityName
        )

        TopWeatherInfoSection(weatherIcon = R.drawable.sunny_128, temperature = "10")
    }

}

 

 

 

결과

  • 앱 실행 시 최신 위치 요청 (LaunchedEffect(Unit)
  • 위치가 변경될 때마다 Geocoder를 사용하여 지역명 업데이트 (LauncedEffect(location))
  • Compose의 StateFlow와 collectAsState()를 사용하여 UI 자동 반영
  • 권한 요청도 rememberLauncherForActivityResult()로 처리하여 더 간결하고 안전한 코드 구성
728x90
반응형