Developing Myself Everyday
article thumbnail

사진: Unsplashhenry perks

 

 

개인 프로젝트를 진행하면서 사용자에 위치에 근처에 있는 관광지를 추천해 주는 기능을 추가하면 어떨까? 하고 생각했습니다.

 

이번 게시글에서는 안드로이드에서 사용자의 위치를 가져올 수 있는 방법을 다뤄보고자 합니다. 다만 해당 게시글에서는 방법을 알려주는 것에 그치기에 생략된 부분이 많습니다. 그렇기 때문에 어느 정도 구조와 사용되는 라이브러리에 대해 이해하지 않으면 어려우실 수 있습니다.


 

 

 

Set up


가장 최근에 알려진 사용자의 위치 정보를 가져오기 위해서는 `Google Play services location APIs` 를 사용합니다. 이와 관련된 내용은 아래의 공식문서에 있습니다.

 

마지막으로 알려진 위치 가져오기  |  Sensors and location  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English 마지막으로 알려진 위치 가져오기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Google P

developer.android.com

 

 

Set up과 관련된 내용은 아래의 게시글에서 참고하시면 좋을 것 같습니다. 이 게시글은 Compose와 Hilt 라이브러리를 사용하고 있습니다.

 

Google Maps & Location Jetpack Compose

Today, I would like to show you how to use Google Maps and user location in Jetpack Compose.

medium.com

 

 

 

 

Permissions


사용자의 위치 정보를 가져오기 위해서는, 가장 먼저 권한이 부여되어 있어야 합니다.

 

제가 진행하고 있는 멀티 모듈 프로젝트에서 앱의 진입점은 App 모듈이기에 App 모듈의 `AndroidManifest.xml` 파일에 아래의 Permissions을 선언해 줍니다.

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

 

실제로 권한을 요청하고 부여하는 부분은 아래의 클래스들을 정의한 다음에 다시 살펴보겠습니다.

 

 

 

 

FusedLocationProviderClient의 메서드


`Google Play services location APIs`에서 사용자의 위치를 가져오기 위해서는 `FusedLocationProviderClient`가 사용됩니다

 

FusedLocationProviderClient에는 위치를 제공해주는 다양한 메서드가 있습니다. 이들의 역할은 목적에 따라 다릅니다. 

 

제가 진행하는 프로젝트에서는 위치 정보를 지속해서 제공해줘야 합니다. 그렇기 때문에 제가 설명하는 방법은 Flow를 통해 사용자의 위치 정보를 지속적으로 제공하는 방법입니다.

 

 

 

 

Service


먼저 사용자의 위치를 요청하고 이를 반환해 주는 코드를 작성해야 합니다.

 

 

바로 아래의 Interface가 그 역할을 담당합니다. 

interface LocationService {
    fun requestLocationUpdates(): Flow<LatLng?>
}

 

 

이제 이를 구현해야 합니다. `Google Play services location APIs`에서 사용자의 위치를 가져오기 위해서는 `FusedLocationProviderClient`가 사용됩니다. 또한 권한 설정을 마지막으로 확인하기 위해 Application Context도 필요합니다.

class LocationServiceImpl @Inject constructor(
    private val context: Context,
    private val locationClient: FusedLocationProviderClient
) : LocationService {

    override fun requestLocationUpdates(): Flow<LatLng?> {
        TODO()
    }
}

 

 

 

 

Module


Service에 필요한 파라미터를 주입해주는 방법은 Hilt를 사용하면 매우 간단합니다. Application Context를 주입해주고 `LocationServices.getFusedLocationProviderClient(context)` 를 사용해서 `FusedLocationProviderClient`를 주입해줍니다.

@Module
@InstallIn(SingletonComponent::class)
object LocationModule {

    @Singleton
    @Provides
    fun provideLocationClient(
        @ApplicationContext context: Context
    ): LocationService = LocationServiceImpl(
        context,
        LocationServices.getFusedLocationProviderClient(context)
    )
}

 

 

 

 

ServiceImpl


이제 서비스의 구현체의 내용을 작성해 보겠습니다.

 

사용자의 위치 정보를 일정한 간격으로 가져와야 하기 때문에 이러한 방법은 기본적으로 Callback 기반입니다.

 

다만 저는 Flow를 쓰고 싶습니다. 그렇기 때문에 Callback을 Flow로 변환하기 위한 CallbackFlow 메서드를 사용합니다.

class LocationServiceImpl @Inject constructor(
    private val context: Context,
    private val locationClient: FusedLocationProviderClient
) : LocationService {

    // callbackFlow 추가
    override fun requestLocationUpdates(): Flow<LatLng?> = callbackFlow {
        TODO()
    }
}

 

 

 

CallbackFlow 내부에서 `trySend` 메서드를 사용하면 이를 Flow로 반환할 수 있습니다. 그리고 내부의 코드의 실행이 완료되고 나서 종료를 막기 위해서 `awaitClose`를 정의해야 합니다. 

 

지속해서 특정 작업을 해야 하는 경우에 awaitClose를 사용하면 Callback을 지속적으로 관찰할 수 있고 Flow를 유지할 수 있습니다.

class LocationServiceImpl @Inject constructor(
    private val context: Context,
    private val locationClient: FusedLocationProviderClient
) : LocationService {

    override fun requestLocationUpdates(): Flow<LatLng?> = callbackFlow {
        
        // awaitClose 추가
        awaitClose {
            locationClient.removeLocationUpdates(locationCallback)
        }
    }
}

 

 

 

다음으로 할 작업은 권한이 부여되었는지를 확인하는 것입니다. UI에서 권한을 부여하고 이를 확인하는 절차를 가지지만, 마지막으로 아래와 같이 권한이 부여되었는지를 확인하고 권한이 없는 경우에는 Null을 반환하고 Flow를 종료합니다.

class LocationServiceImpl @Inject constructor(
    private val context: Context,
    private val locationClient: FusedLocationProviderClient
) : LocationService {

    override fun requestLocationUpdates(): Flow<LatLng?> = callbackFlow {
        // 권한 부여 체크
        if (!context.hasLocationPermission()) {
            trySend(null)
            return@callbackFlow
        }
        
         awaitClose {
            locationClient.removeLocationUpdates(locationCallback)
        }
    }
}

// 권한 부여 함수
fun Context.hasLocationPermission(): Boolean {
    return ContextCompat.checkSelfPermission(
        this,
        Manifest.permission.ACCESS_FINE_LOCATION
    ) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(
        this,
        Manifest.permission.ACCESS_COARSE_LOCATION
    ) == PackageManager.PERMISSION_GRANTED
}

 

 

다음은 위치를 요청해야 합니다. `LocationRequest.Builder`를 통해 위치 요청 설정을 하고 `LocationCallback`을 통해 위치가 업데이트되었을 때의 작업을 정의합니다. 해당 Callback에서 trySend를 통해 Flow를 반환합니다.

class LocationServiceImpl @Inject constructor(
    private val context: Context,
    private val locationClient: FusedLocationProviderClient
) : LocationService {

    @SuppressLint("MissingPermission")
    @RequiresApi(Build.VERSION_CODES.S)
    override fun requestLocationUpdates(): Flow<LatLng?> = callbackFlow {
        if (!context.hasLocationPermission()) {
            trySend(null)
            return@callbackFlow
        }

        val request = LocationRequest.Builder(10000L)
            .setIntervalMillis(10000L)
            .setPriority(Priority.PRIORITY_HIGH_ACCURACY)
            .build()

        val locationCallback = object : LocationCallback() {
            override fun onLocationResult(locationResult: LocationResult) {
                locationResult.locations.lastOrNull()?.let {
                    trySend(LatLng(it.latitude, it.longitude))
                }
            }
        }

        locationClient.requestLocationUpdates(
            request,
            locationCallback,
            Looper.getMainLooper()
        )

        awaitClose {
            locationClient.removeLocationUpdates(locationCallback)
        }
    }
}

 

 

이제 해당 Service는 사용자의 위치에 대한 정보를 제공합니다. 원하는 대로 해당 Service를 직접 사용하거나 중간에 Domain 계층을 두고 사용하면 됩니다.

 

 

 

 

권한 요청


사용자의 위치 정보를 가져오기 위한 권한을 체크하고 이를 요청하는 부분은 UI에서 처리합니다.

 

저의 경우에는 Compose를 사용했기에 아래와 같이 함수를 정의하여 권한을 체크하고 요청했습니다.

@RequiresApi(Build.VERSION_CODES.S)
@Composable
fun LocationScreen(
    
    ...
    goBack: () -> Unit,
    viewModel: ViewModel = hiltViewModel()
) {
    
    ...
    val context = LocalContext.current
    InitPermission(context = context, goBack = goBack, viewModel = viewModel)

    ...
    
}

@RequiresApi(Build.VERSION_CODES.S)
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun InitPermission(
    context: Context,
    goBack: () -> Unit,
    viewModel: LocationViewModel
) {
    val permissionState = rememberMultiplePermissionsState(
        permissions = listOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_COARSE_LOCATION
        )
    )

    LaunchedEffect(!context.hasLocationPermission()) {
        permissionState.launchMultiplePermissionRequest()
    }

    when {
        permissionState.allPermissionsGranted -> {
           // 권한이 부여 되어 있으면 현재 위치를 요청
            viewModel.fetchCurrentLocation()
        }

        permissionState.shouldShowRationale -> {
            RationaleAlert(onDismiss = goBack) {
                permissionState.launchMultiplePermissionRequest()
            }
        }

        !permissionState.allPermissionsGranted && !permissionState.shouldShowRationale -> {
            // 권한이 거절되었을 때의 대응
            viewModel.revokedPermissions()
        }
    }
}

 

위의 `hasLocationPermission` 함수는 위에서 Service를 정의할 때 사용했던 함수를 한번 더 사용했습니다.

 

 

 

 

마무리하며


이렇게 해서 사용자의 위치를 요청하는 방법을 간략하게 알아보았습니다.

 

해당 게시글에서는 많은 내용을 다루고 있지는 않습니다. 그렇기 때문에 만약 어려우신 부분이 있다면 댓글로 요청하시면 제가 알고 있는 선에서 설명해 드리겠습니다. 감사합니다.

 

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!