Flutter Background Geolocation -Android

Bruno Eleodoro Roza
Flutter — Comunidade BR
8 min readMar 25, 2020
Photo by NASA on Unsplash

Já precisou pegar a latitude e longitude do seu usuário constantemente, mesmo que ele feche o seu app em Flutter?

Você deve ter tentado alguns plugins, e quero deixar minha frustração com o plugin da “Transistor Software” (https://pub.dev/packages/flutter_background_geolocation).

ATENÇÃO: Faz tempo que não atualizo o plugin portanto diversas mudanças no código nativo ocorreram durante esse tempo, por favor siga o artigo do Caio que fez um trabalho incrível de persistência e estou muito feliz que tenha conseguido resolver esse problema antigo que sempre tivemos com Flutter mas ninguém teve coragem até então para resolver:

[Video sobre o artigo]

O plugin deles de fato é o melhor que tinha disponível no website de packages para o Flutter, o pub dev. Ele era o único package que respeitava as guide lines do Android que diz para você notificar o usuário quando seu app estiver obtendo a localização em background.

E quando gerei o build pra release do meu app com esse plugin, tive a surpresa e me assustei ao saber que o plugin era pago.(e não é um preço acessível)

GRRR

Mas isso me motivou a desenvolver o meu próprio “plugin”.

Claro que foi um desastre, mas depois de varias tentativas, consegui chegar em um beta.

Eu chamo ele assim, porque na verdade só funciona para Android e ainda com varias limitações.

Meu objetivo aqui é que o trabalho que passei, você não tenha que passar e possa seguir a partir desse ponto para desenvolver algo melhor e não ter que PAGAR 😠 para usar um plugin.

Vamos para o código

Crie um projeto com o flutter create padrão mesmo, sem nada de diferente.

Primeira coisa a ser feita é abrir a pasta “android/” em uma nova janela do Android Studio (use o editor de sua preferência).

Depois edite o arquivo dentro de “app/build.gradle” e preste atenção nos parametros, compileSdkVersion e targetSdkVersion Eles precisam estar com o valor 29.

E você precisa adicionar uma dependência para obter a latitude e longitude usando o Android nativo, basta ver no código abaixo a propriedade implementation.

Api de nível 29 corresponde ao Android 10 .

https://developer.android.com/about/versions/10

def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}

def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
compileSdkVersion 29


sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}

lintOptions {
disable 'InvalidPackage'
}

defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.flutter_background_geolocation_android"
minSdkVersion 19
targetSdkVersion 29
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}

buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}

flutter {
source '../..'
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.android.gms:play-services-location:16.0.0'
}

Feito isso, vamos deletar o arquivo MainActivity.kt dentro da pasta principal do android onde ficam as classes java e as classes em kotlin.

Depois precisamos rodar um Clean, para limpar alguns caches do Android Studio.

No menu de “Build” selecione a opção “Clean Project”.

E vamos criar uma MainActivity.java (Porque o código que eu fiz está em JAVA, se você prefere kotlin, fique a vontade)

Dentro do seu arquivo, cole o seguinte conteúdo.

Ele vai dar alguns erros, mas já vão ser corrigidos.

Agora vamos criar nossa class que vai ser responsável por obter os dados de latitude e longitude nativamente no Android. Vamos fazer isso através do uso de services.

Mas na verdade, quem fez essa classe não fui eu, e sim a google. Nada melhor do que confiar no código vindo do próprio GitHub deles para executar em background e obter a localização do usuário.

O Link para acessar o código fonte da Google está aqui:

E também vamos precisar da classe Utils.java que está presente no mesmo diretório.

Se estiver dando erro ao acessar os links acima, vou colocar o código aqui. (mas eles no vão ficar sempre atualizados).

Utils.java

Utils.java

LocationUpdatesService.java

LocationUpdatesService.java

Depois de criado esses arquivos você deve ter uma estrutura igual a essa na sua pasta Android:

Vamos corrigir os erros:

No arquivo LocationUpdatesService.java vamos editar as seguintes linhas de código:

Antes
Trocar o nome do app
Depois
Trocar o nome do app

Ainda no mesmo arquivo:

Antes:

Depois:

Agora vamos editar o arquivo Utils.java .

Antes:

Location title

Depois:

Message Location

Finalizado as edições, agora vamos adicionar em nosso arquivo Manifest o service que acabamos de criar.

Adicione primeiramente as permissões:

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

Depois vamos declarar nosso service.

....
</activity>
<service
android:name=".LocationUpdatesService"
android:enabled="true"
android:exported="true"
android:foregroundServiceType="location" />
</application>

Feito isso, nosso arquivo vai ficar dessa forma:

AndroidManifest.xml

Vamos criar algumas variáveis no topo do nosso arquivo LocationUpdatesService.java:

Variáveis globais

onStartCommand

Repare que nesse trecho de código, estamos instanciando nosso objeto FlutterEngine e passando como rota inicial uma rota chamada “/callback”, para que quando a localização for enviada do código nativo para o Flutter em background a gente não renderize nenhuma UI, já que vamos estar executando código em background.

flutterEngine = new FlutterEngine(this);
flutterEngine.getNavigationChannel().setInitialRoute("/callback");

Como nesse exemplo, vamos salvar todas as latitudes e longitudes no SharedPreferences, precisamos incluir o plugin para que consigamos utilizar os métodos dele em background. Eu peguei esse trecho de código do próprio arquivo que o Flutter gera em nosso projeto:

flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());

E por fim, mas não menos importante, o código que vai criar o MethodChannel, que vai permitir que o código nativo envie os dados para o Flutter em background:

channel = new MethodChannel(flutterEngine.getDartExecutor(), "geolocation_plugin");

onDestroy

Nesse método, precisamos fechar a conexão do method channel com o flutter caso o Service pare de executar. Assim evitamos consumo de memoria em mantendo uma conexão inativa.

onNewLocation

Agora vamos editar o método que pega as informações de localização do usuário para que ele envie esses dados através do MethodChannel que abrimos no passo anterior.

O que nos importa esta no código abaixo.

Nele especificamos o método “callbackLocation” e os argumentos que queremos enviar para o Flutter, veja que estou enviando as informações separadas por vírgulas. e o último parâmetro é essencial.

channel.invokeMethod("callbackLocation", location.getLatitude() + "," + location.getLongitude() + "," + location.getSpeed() + "," + serviceIsRunningInForeground(this));

No comando abaixo ele retorna true ou false, isso é importante porque se o usuário estiver com o app aberto, não queremos instanciar outra engine de Flutter.

Ou seja, se esse código retornar true para o Flutter, sabemos que o usuário está com o app em background/foreground, e não está com ele aberto.

serviceIsRunningInForeground(this)

Voltando para o Flutter

No Flutter, precisamos iniciar o Method Channel e colocar o código que será executado em background.

Aqui está como vai ficar a classe “MyApp” do nosso projeto, assim como já mencionei no artigo, quando o app estiver rodando em background não precisamos renderizar nenhuma UI para poupar memória.

Agora vamos para o código que recebe a latitude e longitude do código nativo.

Primeiro crie o seguinte método, vamos utilizar ele para facilitar obter os dados de localização que foram salvos.

List getLocations(prefs) {
var storedValue = prefs.getString('locations');
if (storedValue == null) {
storedValue = jsonEncode([]);
}
var locations = jsonDecode(storedValue);
return locations;
}

Agora vamos para o código que vai ficar presente dentro do método main, aqui temos a declaração da variável que vai ser passada por parametro para o nosso MyApp onde se o valor dela for true, não vamos renderizar a UI.

Temos também a instancia do MethodChannel com o mesmo nome que colocamos em nosso arquivo LocationUpdatesService.java “geolocation_plugin”.

bool callbackLocation = false;
WidgetsFlutterBinding.ensureInitialized();
MethodChannel _channel = MethodChannel('geolocation_plugin');

Agora precisamos criar o nosso listener que vai aguardar a latitude e longitude serem passadas pelo código nativo.

Nesse código é feito o split das informações pela virgula “,” para pegar exatamente cada parte da informação. Se caso não se lembrar, em nosso LocationUpdatesService, passamos os parametros separados por virgula e aqui pegamos eles e salvamos em variaveis para trabalhar com eles.

_channel.setMethodCallHandler((MethodCall call) async {
print(call.method);
if (call.method == "callbackLocation") {
callbackLocation = true;

var lat = call.arguments.split(',')[0];
var lon = call.arguments.split(',')[1];
var vel = call.arguments.split(',')[2];
var isRunningInBackground = call.arguments.split(',')[3];

if (isRunningInBackground == "true") {
SharedPreferences prefs = await SharedPreferences.getInstance();
List locations = getLocations(prefs);
locations.add({'lat': lat, 'lon': lon});
prefs.setString('locations', jsonEncode(locations));
}
}
});

Depois disso, é feito uma verificação para ver se realmente o app está rodando em background para que então, eu salve no SharedPreferences esta nova localização do usuário.

Aqui está o código completo.

Feito isso, seu projeto está pronto para executar operações em background baseadas no Service que está rodando por baixo dos panos no Android.

Vídeo mostrando o funcionamento:

Notas Importantes

  • Assim como eu disse e reforço agora, este código só esta funcional no Android.
  • Assim como você acompanhou no passo a passo, para que os plugins fiquem disponíveis para serem executados em background (como o SharedPreferences) é necessario adicionar eles no LocationUpdatesService.java .

Próximos passos

  • Transformar esse código em um plugin para que seja possível a instalação em qualquer projeto e fique disponível para todos da comunidade com uma fácil integração.
  • Tornar disponível para IOS. ( O que eu acredito que vai demorar ).

Acesse o repositório com tudo funcionando:

Me siga nas redes sociais:

Obrigado pela sua paciência, pois o artigo é longo ahahah, mas espero que te ajude no seu projeto e que você possa ter insights a partir desse projeto.

Até.

--

--