Este fin de semana mientras intento aprender Flutter Flame y ademas que una persona pregunto en un grupo de flutter sobre cómo obtener la ubicación cada cierto tiempo en flutter, me puse manos a la obra para crear un código que hace exactamente eso, pero sin usar ningún paquete, simplemente usando channels y código nativo, aunque solo para Android, ya que iOS tiene sus particularidades.
Obviaré todo el código necesario para solicitar los permisos, ya que me centraré únicamente en la lógica para obtener la ubicación y enviarla mediante un channel hacia flutter.
Permisos necesarios
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
Estos son los permisos necesarios para que el código funcione, estos deben ir en el archivo AndroidManifest.xml del proyecto.
LocationService
“Lo primero que hay que hacer es crear un Foreground Service. En Android, para operaciones de larga duración y continuas (como la obtención constante de la ubicación) que se ejecutan en segundo plano, es obligatorio usar un Foreground Service, el cual requiere de una notificación visible para el usuario y así garantizar que el sistema no lo detenga fácilmente
Para ello hay que extender una clase desde la clase android.app.Service de Android, despues hay dos clases que hay que usar:
private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var locationCallback: LocationCallback
fusedLocationClient
: el cliente que interactúa con los servicios de ubicación de Google.locationCallback
: recibe las actualizaciones de ubicación en tiempo real.
El código quedara así:
override fun onCreate() {
super.onCreate()
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
locationCallback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.locations.forEach { location ->
Log.d("LocationService", "Lat: ${location.latitude}, Lon: ${location.longitude}")
LocationStreamHandler.emit(...)
}
}
}
startLocationUpdates()
}
El código para obtener las ubicaciones cada X tiempo llegara en el callback de LocationCallback dentro de este, lo que hice fue usar un StreamHandler, especificamente un método dentro de mi clase LocationStreamHandler que hereda de EventChannel.StreamHandler esta clase es importante ya que es la que comunicara los datos mediante el método emit hacia el código de flutter.
Tambien hay un metodo startLocationUpdates, su funcion es iniciar la petición de ubicaciones, de la siguiente manera:
val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1 * 60 * 1000L)
.setMinUpdateIntervalMillis(1 * 60 * 1000L)
.build()
fusedLocationClient.requestLocationUpdates(request, locationCallback, Looper.getMainLooper())
Para que todo esto funcione se necesita implementar Looper.getMainLooper(), esta función en Android retorna el objeto Looper
asociado al hilo principal (UI Thread) de la aplicación.
Looper
: Es un mecanismo que se encarga de extraer (dequeue) mensajes y eventos de una cola de mensajes (MessageQueue
) y de enviarlos al correspondienteHandler
para su procesamiento. En esencia, es lo que permite que un hilo (thread) se ejecute de manera cíclica o en un “bucle”.getMainLooper()
: Esta función estática garantiza que obtienes elLooper
de un hilo muy específico: el hilo principal o hilo de la interfaz de usuario (UI Thread).
Por último, esta clase hay que agregarla en AndroidManifest.xml de la siguiente manera:
<service
android:name=".LocationService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location"
/>
LocationStreamHandler
la clase LocationStreamHandler es un singleton que hereda de EventChannel.StreamHandler. Esta clase nos servirá para pasar los datos usando el método emit hacia la parte flutter usando los channels.
Esta clase sirve como un puente para el Stream de datos. Además, la clase se asegura de que el envío de datos a Flutter se realice de forma segura, garantizando que la información se transmita al hilo principal (UI Thread) de la aplicación Flutter.
object LocationStreamHandler : EventChannel.StreamHandler {
private val main = Handler(Looper.getMainLooper())
@Volatile private var sink: EventChannel.EventSink? = null
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { sink = events }
override fun onCancel(arguments: Any?) { sink = null }
fun emit(lat: Double, lon: Double, acc: Float?, bearing: Float?, speed: Float?, timeMillis: Long) {
val payload = mapOf(
"latitude" to lat,
"longitude" to lon,
"accuracy" to acc,
"bearing" to bearing,
"speed" to speed,
"timestamp" to timeMillis
)
main.post { sink?.success(payload) }
}
fun emitError(code: String, message: String) {
main.post { sink?.error(code, message, null) }
}
}
Por ultimo, en main hay que “conectar” este StreamHandler de la siguiente manera:
EventChannel(
flutterEngine.dartExecutor.binaryMessenger,
'com.g80bits/location_updates'
).setStreamHandler(LocationStreamHandler)
Una vez teniendo esto, en el lado flutter el código simplemente debe implementar un Stream:
EventChannel(
'com.g80bits/location_updates',
).receiveBroadcastStream().listen((event){
print("Location update: $event");
})
Con el código anterior, se obtendrá cada X tiempo la ubicación desde Android
Y eso sería todo, dejare el código completo así se comprende mejor la lógica.