Solution 1 :
I use class LocationLiveData in my application, which extends MutableLiveData and it works perfect. See code bellow:
class LocationLiveData private constructor(context: Context) : MutableLiveData<Location>() {
private val client: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)
private var minAccuracy: Float = 0.toFloat()
private var stopWhenCatch: Boolean = false
private var stopped: Boolean = false
private val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult?) {
locationResult?.lastLocation?.let { location ->
if (location.accuracy <= minAccuracy) {
checkLocationAccuracy()
value = location
if (stopWhenCatch) {
stopped(true)
stopWatch()
}
}
}
}
}
override fun onActive() {
if (value == null || stopped) {
startWatch()
}
}
override fun onInactive() {
stopWatch()
}
fun stopped(stopped: Boolean) {
this.stopped = stopped
}
@SuppressLint("MissingPermission")
private fun checkLocationAccuracy() {
if (minAccuracy == DEFAULT_LOW_ACCURACY) {
minAccuracy = DEFAULT_HIGH_ACCURACY
}
}
@SuppressLint("MissingPermission")
private fun startWatch() {
client.requestLocationUpdates(createLocationHighAccuracyRequest(), locationCallback, Looper.myLooper())
}
private fun stopWatch() {
client.removeLocationUpdates(locationCallback)
}
private fun createLocationHighAccuracyRequest() =
LocationRequest
.create()
.setInterval(INTERVAL)
.setFastestInterval(FASTEST_INTERVAL)
.setSmallestDisplacement(DISTANCE)
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
class Builder(private val mContext: Context) {
private var minAccuracy: Float = DEFAULT_LOW_ACCURACY
private var stopWhenCatch: Boolean = false
fun minAccuracy(minAccuracy: Float): Builder {
this.minAccuracy = minAccuracy
return this
}
fun stopWhenCatch(stopWhenCatch: Boolean): Builder {
this.stopWhenCatch = stopWhenCatch
return this
}
fun build() = LocationLiveData(mContext).apply {
minAccuracy = [email protected]
stopWhenCatch = [email protected]
}
}
companion object {
private const val DEFAULT_HIGH_ACCURACY = 50f
private const val DEFAULT_LOW_ACCURACY = 10_000f // low accuracy for fast finding location on start application, 10km
private const val INTERVAL = 10_000L
private const val FASTEST_INTERVAL = 5_000L
private const val DISTANCE = 5f
}
}
Code from ViewModel:
private var locationLiveData: LocationLiveData? = null
fun subscribeLocationLiveData(context: Context): LocationLiveData {
locationLiveData = LocationLiveData.Builder(context)
.build()
return locationLiveData!!
}
// call where you need to stop getting location
fun unsubscribeLocationLiveData() {
locationLiveData?.stopped(true)
}
Code from fragment:
private fun subscribeLocationUpdate() {
viewModel.subscribeLocationLiveData(requireContext()).observe(
viewLifecycleOwner,
Observer { location ->
// here will be user location
}
)
}
Problem :
To sum up the issue, I was implementing the google maps API to make a run tracker and was also implementing MVVM architecture at the same time. However, when I managed to configure my code to place the LocationCallback object inside my viewModel and my fusedLocationProviderClient, my LocationCallback object is not able to be called at all ever after I visited the fragment I was trying to implement it. I tried to Log.d anything inside the object but nothing would show up. Was wondering if I implemented it wrong. I have already requested for permissions in my main activity.
RunSessionViewModel.kt
package com.example.myfit_exercisecompanion.ui.viewModels
import android.Manifest
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Build
import android.os.Looper
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.myfit_exercisecompanion.repository.RunSessionRepository
import com.example.myfit_exercisecompanion.ui.MainActivity
import com.google.android.gms.location.*
import com.google.android.gms.location.Priority.PRIORITY_HIGH_ACCURACY
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.SphericalUtil
import dagger.hilt.android.lifecycle.HiltViewModel
import timber.log.Timber
import javax.inject.Inject
import kotlin.math.roundToInt
@HiltViewModel
class RunSessionViewModel @Inject constructor(
private val runSessionRepository: RunSessionRepository,
application: Application
): AndroidViewModel(application) {
private val _locationResults = MutableLiveData<LocationResult>()
val locationResults: LiveData<LocationResult>
get() = _locationResults
val context = application
val mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context.applicationContext)
val ui = MutableLiveData(Ui.EMPTY)
private val _locations = mutableListOf<LatLng>()
val locations: List<LatLng>
get() = _locations
private var distance = 0
private val _liveLocations = MutableLiveData<List<LatLng>>()
val liveLocations: MutableLiveData<List<LatLng>>
get() = _liveLocations
private val _liveDistance = MutableLiveData<Int>()
val liveDistance: MutableLiveData<Int>
get() = _liveDistance
private val _liveLocation = MutableLiveData<LatLng>()
val liveLocation: MutableLiveData<LatLng>
get() = _liveLocation
private val locationCallback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
super.onLocationResult(result)
val currentLocation = result.lastLocation
val latLng = LatLng(currentLocation!!.latitude, currentLocation.longitude)
val lastLocation = _locations.lastOrNull()
if (lastLocation != null) {
distance += SphericalUtil.computeDistanceBetween(lastLocation, latLng).roundToInt()
_liveDistance.value = distance
}
_locations.add(latLng)
_liveLocations.value = locations
}
}
@SuppressLint("MissingPermission")
fun getUserLocation() {
mFusedLocationProviderClient.lastLocation.addOnSuccessListener { location ->
val latLng = LatLng(location.latitude, location.longitude)
_locations.add(latLng)
_liveLocation.value = latLng
}
.addOnFailureListener { failure ->
Timber.d("lastLocation failure ${failure.message}")
}
}
@SuppressLint("MissingPermission")
private fun trackUser() {
val locationRequest = LocationRequest.create().apply {
priority = PRIORITY_HIGH_ACCURACY
interval = 5000L
fastestInterval = 2000L
}
mFusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
}
fun stopTracking() {
mFusedLocationProviderClient.removeLocationUpdates(locationCallback)
_locations.clear()
distance = 0
RPMLiveData().unloadStepCounter()
}
fun startTracking() {
trackUser()
val currentUi = ui.value
ui.value = currentUi?.copy(
formattedPace = Ui.EMPTY.formattedPace,
formattedDistance = Ui.EMPTY.formattedDistance
)
}
private val _liveSteps = MutableLiveData<Int>()
val liveSteps:MutableLiveData<Int>
get() = _liveSteps
val rpmLiveData = RPMLiveData()
// inner class just to have access to application
inner class RPMLiveData : LiveData<String>(), SensorEventListener {
private val sensorManager by lazy {
context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
}
private val stepCounterSensor: Sensor? by lazy {
sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
}
private var initialSteps = -1
fun setupStepCounter() {
if (stepCounterSensor != null) {
sensorManager.registerListener(this, stepCounterSensor,
SensorManager.SENSOR_DELAY_FASTEST
)
}
}
override fun onSensorChanged(event: SensorEvent) {
event.values.firstOrNull()?.toInt()?.let { newSteps ->
if (initialSteps == -1) {
initialSteps = newSteps
}
val currentSteps = newSteps - initialSteps
_liveSteps.value = currentSteps
}
}
fun unloadStepCounter() {
if (stepCounterSensor != null) {
sensorManager.unregisterListener(this)
}
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) = Unit
}
}
data class Ui(
val formattedPace: String,
val formattedDistance: String,
val currentLocation: LatLng?,
val userPath: List<LatLng>
) {
companion object {
val EMPTY = Ui(
formattedPace = "",
formattedDistance = "",
currentLocation = null,
userPath = emptyList()
)
}
}
RunTrackerFragment.kt
package com.example.myfit_exercisecompanion.ui.fragments
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Build
import android.os.Bundle
import android.os.SystemClock
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.MutableLiveData
import com.example.myfit_exercisecompanion.R
import com.example.myfit_exercisecompanion.databinding.FragmentRunTrackerBinding
import com.example.myfit_exercisecompanion.ui.MainActivity
import com.example.myfit_exercisecompanion.ui.viewModels.RunSessionViewModel
import com.example.myfit_exercisecompanion.ui.viewModels.Ui
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.PolylineOptions
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class RunTrackerFragment : Fragment(R.layout.fragment_run_tracker), OnMapReadyCallback {
private lateinit var map: GoogleMap
private var _binding: FragmentRunTrackerBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private val viewModel: RunSessionViewModel by viewModels()
private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
var initialSteps = -1
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentRunTrackerBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Timber.d("fusedLocationProviderClient not being a bitch ${viewModel.mFusedLocationProviderClient}")
val mapFragment = childFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
binding.btnStartStop.setOnClickListener {
if (binding.btnStartStop.text == getString(R.string.start_label)) {
startTracking()
binding.btnStartStop.setText(R.string.stop_label)
} else {
stopTracking()
binding.btnStartStop.setText(R.string.start_label)
}
}
viewModel.liveLocations.observe(viewLifecycleOwner) { locations ->
val current = viewModel.ui.value
viewModel.ui.value = current?.copy(userPath = locations)
}
viewModel.liveLocation.observe(viewLifecycleOwner) { currentLocation ->
val current = viewModel.ui.value
viewModel.ui.value = current?.copy(currentLocation = currentLocation)
}
viewModel.liveDistance.observe(viewLifecycleOwner) { distance ->
val current = viewModel.ui.value
val formattedDistance = requireContext().getString(R.string.distance_value, distance)
viewModel.ui.value = current?.copy(formattedDistance = formattedDistance)
}
viewModel.liveSteps.observe(viewLifecycleOwner) { steps ->
val current = viewModel.ui.value
viewModel.ui.value = current?.copy(formattedPace = "$steps")
}
}
private val locationPermissionProviderContract = activity?.registerForActivityResult(
ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
viewModel.getUserLocation()
Timber.d("Pass 1")
}
}
private val activityRecognitionPermissionProviderContract = activity?.registerForActivityResult(
ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
viewModel.RPMLiveData().setupStepCounter()
Timber.d("Pass 2")
}
}
fun requestUserLocation() {
locationPermissionProviderContract?.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
fun requestActivityRecognition() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
activityRecognitionPermissionProviderContract?.launch(Manifest.permission.ACTIVITY_RECOGNITION)
} else {
viewModel.RPMLiveData().setupStepCounter()
}
}
fun onMapLoaded(){
requestUserLocation()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onMapReady(googleMap: GoogleMap) {
map = googleMap
// presenter = (activity as MainActivity).presenter
viewModel.ui.observe(viewLifecycleOwner) { ui ->
Log.d("ui instantiated", viewModel.ui.value.toString())
updateUi(ui)
}
onMapLoaded()
map.uiSettings.isZoomControlsEnabled = true
}
private fun startTracking() {
binding.container.txtPace.text = ""
binding.container.txtDistance.text = ""
binding.container.txtTime.base = SystemClock.elapsedRealtime()
binding.container.txtTime.start()
map.clear()
Log.d("type:", "${binding.container.txtTime.base}")
viewModel.startTracking()
requestActivityRecognition()
}
private fun stopTracking() {
viewModel.stopTracking()
binding.container.txtTime.stop()
}
@SuppressLint("MissingPermission")
private fun updateUi(ui: Ui) {
if (ui.currentLocation != null && ui.currentLocation != map.cameraPosition.target) {
map.isMyLocationEnabled = true
map.animateCamera(CameraUpdateFactory.newLatLngZoom(ui.currentLocation, 14f))
}
binding.container.txtDistance.text = ui.formattedDistance
binding.container.txtPace.text = ui.formattedPace
drawRoute(ui.userPath)
}
private fun drawRoute(locations: List<LatLng>) {
val polylineOptions = PolylineOptions()
map.clear()
val points = polylineOptions.points
points.addAll(locations)
map.addPolyline(polylineOptions)
}
}
because of the way my code is structured, the map appears on my fragment but not at my current location.
Problem
The guide I was trying to follow was actually this link here and I wanted to convert it into a MVVM architecture.
https://www.raywenderlich.com/28767779-how-to-make-an-android-run-tracking-app
The first question on this platform and relatively new to android development so pardon me if I had posted this wrongly or if my code is extremely inconsistent.
Comments
Comment posted by Isaac Chen
Do you put the LocationLiveData class as a parameter to your viewModel? I am using Dagger Hilt so when I did that there was an error that popped up saying I need to use
Comment posted by Roman Chekashov
@IsaacChen, no I just create by using builder