Flutter Background Geolocation -Android
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)
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
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:
Ainda no mesmo arquivo:
Antes:
Depois:
Agora vamos editar o arquivo Utils.java .
Antes:
Depois:
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:
Vamos criar algumas variáveis no topo do nosso arquivo LocationUpdatesService.java:
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é.