안드로이드 개발을 하면서 사용자의 위치 정보가 필요한 상황이 발생한다.
그렇다면, 위치정보를 가지고 오는 방법은 여러가지가 있다.
- Fused Location Provider API (Google Play Services)
- Location Manager (Legacy)
대표적으로 두가지 방법이 있는 데,
첫번째 방식은 Google Play 서비스의 Fused Location Provider API를 사용하여 위치 정보를 가져오는 방식 이다.
두번째 방식은 LocationManager 클래스를 사용하여 위치 정보를 가져오는 방식이다. 이 방법은 오래된 방법이고, GPS나 NETWORK를 통해 위치값을 가져온다고 한다.
오늘 포스팅 할 위치 정보를 가져오는 방법은 Fused Location Provider API 이다.
들어가기 앞서 아래의 공식 문서를 참고하여 만들었다.
https://developer.android.com/training/location/retrieve-current?hl=ko
마지막으로 알려진 위치 가져오기 | Android 개발자 | Android Developers
마지막으로 알려진 위치 가져오기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Google Play 서비스 Location API를 사용하여 앱에서 마지막으로 알려진 사용자
developer.android.com
최근에 Hilt에 대해 공부했으므로, Hilt와 MVVM 패턴을 적용하여 위치 정보를 가지고 오는 방법을 구현해보도록 하겠다.
가장 먼저 모듈을 정의하였다.
모듈 (LocationModule.kt)
@Module
@InstallIn(SingletonComponent::class)
object LocationModule {
@Provides
@Singleton
fun provideFusedLocationProviderClient(@ApplicationContext context: Context): FusedLocationProviderClient {
return LocationServices.getFusedLocationProviderClient(context)
}
@Provides
@Singleton
fun provideLocationRepository(fusedLocationProviderClient: FusedLocationProviderClient): LocationRepository {
return LocationRepository(fusedLocationProviderClient)
}
}
코드는 다음과 같다.
'@Provides' 어노테이션을 통해 'FusedLocationProviderClient'와 'LocationRepository' 를 제공하는 메서드를 설정하였다.
레포지토리(LocationRepository.kt)
@Singleton
class LocationRepository @Inject constructor(
private val fusedLocationProviderClient: FusedLocationProviderClient
) {
@SuppressLint("MissingPermission")
suspend fun getCurrentLocation() : Flow<Status<Location?>> = flow{
emit(Status.Loading())
try {
val locationResult = fusedLocationProviderClient.getCurrentLocation(
Priority.PRIORITY_HIGH_ACCURACY,
CancellationTokenSource().token
).await()
locationResult?.let {
emit(Status.Success(data = it))
}
} catch (e: Exception) {
// 위치 정보 가져오기 실패 시 예외 처리
emit(Status.Error(e.localizedMessage ?: ""))
}
}
}
@Inject 를 통해 의존성 주입을 한다.
getCurrentLocation()은 현재 위치 좌표를 가져오는 함수로, 'suspend'를 이용하였다.
이때 반환 결과로 Flow를 사용하는데, Flow는 비동기 작업의 결과를 데이터 스트림으로 내보내기 위한, 코루틴 기반의 리액티브 스트림이다.
Status의 경우에는 다음과 같다. 제너릭 클래스로 정의하였으며, sealed class로 정의하였다.
sealed class Status<T>(val data : T? = null ,val message: String? = null) {
class Success<T>(data : T?) :Status<T>(data = data)
class Error<T>(message: String) : Status<T>(message = message)
class Loading<T>() : Status<T>()
}
다시 위의 코드를 설명하면, fusedLocationProviderClient.getCurrentLocation() 의 경우에는 위치 정보를 가져오기 위해 인자로 'Priority.PRIORITY_HIGH_ACCURACY' 및 'CancellationTokenSource().token' 을 사용하여, 높은 정확도로 위치를 가지고 왔다. 그리고 await() 를 통해 비동기적으로 위치정보를 가져왔다.
함수가 실행되면 Status.Loading을 통해 스트림에 로딩 상태임을 전달하고,
위치 정보가 성공적으로 가져오는 경우 위치정보와 함께 성공 상태를 스트림에 전달한다.
실패시 예외정보를 포함하여 전달한다.
뷰모델 (LocationViewModel.kt)
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.shino72.location.repository.LocationRepository
import com.shino72.location.utils.LocationState
import com.shino72.location.utils.Status
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class LocationViewModel
@Inject
constructor(
private val locationRepository: LocationRepository
) : ViewModel()
{
private val _location = MutableStateFlow(LocationState())
val location : StateFlow<LocationState> = _location
suspend fun getLocation() {
locationRepository.getCurrentLocation().onEach {state ->
when(state)
{
is Status.Loading -> {
_location.value = LocationState(isLoading = true)
}
is Status.Error -> {
_location.value = LocationState(error = state.message ?: "")
}
is Status.Success -> {
_location.value = LocationState(data = state.data)
}
}
}.launchIn(viewModelScope)
}
}
data class LocationState(
val data: Location? = null,
val error: String = "",
val isLoading: Boolean = false
)
해당 뷰모델은 의존성을 주입받고, 위치 정보를 관리한다.
'onEach' 를 통해 스트림 상태를 관리한다.
각 상태에 따라 _location의 값을 업데이트 해준다.
메인 엑티비티
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val locationViewModel : LocationViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.apply {
btn.setOnClickListener {
requestPermission {
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
locationViewModel.getLocation()
}
}
}
}
lifecycle.coroutineScope.launchWhenCreated {
locationViewModel.location.collect {
if(it.isLoading) {
binding.progress.visibility = View.VISIBLE
binding.text.text = "Loading..."
}
if(it.error.isNotBlank()) {
binding.progress.visibility = View.GONE
binding.text.text = "${it.error}"
}
it.data?.let {
binding.progress.visibility = View.GONE
binding.text.text = "Success : latitude => ${it.latitude} / longitude ${it.longitude}"
}
}
}
}
private fun requestPermission(logic : () -> Unit) {
TedPermission.create()
.setPermissionListener(object : PermissionListener {
override fun onPermissionGranted() {
logic()
}
override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
Toast.makeText(this@MainActivity, "권한을 허가해주세요", Toast.LENGTH_SHORT).show()
}
}).setDeniedMessage("위치 권한을 허용해주세요.").setPermissions(android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACCESS_COARSE_LOCATION).check()
}
}
메인 엑티비티는 간단하게 설명하면, TedPermission을 통해 권한 체크를 해주었다.
위치 정보를 얻기 위해 각 퍼미션을 요청해주었다.
locationViewModel.location.collect를 통해 위치 정보 상태 변화를 관찰하여 UI를 업데이트를 해준다.
메인 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn"
app:layout_constraintBottom_toBottomOf="parent"
android:text="위치 얻기"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<ProgressBar
android:visibility="gone"
android:id="@+id/progress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="100dp"
android:layout_height="100dp"/>
<TextView
android:id="@+id/text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
실행 영상
깃허브
'안드로이드 > 앱 개발' 카테고리의 다른 글
[Android App] 명언 제조기 (0) | 2023.07.17 |
---|---|
[Retrofit] MultiPart / PartMap 사용하기 (0) | 2023.05.22 |
[Android][Kotlin][길찾기 기능 구현하기] Part1. 기능 구상 (0) | 2023.04.03 |
[kotlin] RecyclerView를 이용하여 지하철 노선도 만들기 (0) | 2023.01.30 |
[Android] Retrofit2를 이용하여 환율 데이터 받아오기 (0) | 2023.01.24 |