[Android] - 실시간 위치 정보 받아오기 (with. Fused Location Provider API + MVVM + Hilt)

안드로이드 개발을 하면서 사용자의 위치 정보가 필요한 상황이 발생한다.

 

그렇다면, 위치정보를 가지고 오는 방법은 여러가지가 있다.

  • 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>

 

실행 영상

 

 

깃허브

https://github.com/Myeongcheol-shin/location-information