You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android开发求助:实现点击开关按钮后后台定时获取位置并通过Retrofit发送至服务器

Complete Implementation for Background Location Updates with WorkManager

Hey there! Let's tackle this problem step by step. I've put together a clean, working solution that meets all your requirements: it uses WorkManager to bypass device background restrictions, fetches location updates at your desired interval, sends the address to your server via Retrofit, and updates your MainActivity's TextView with the latest address. You can start/stop the whole process with a single toggle button.


Step 1: Set Up Permissions & Dependencies

AndroidManifest.xml

First, add the required permissions to your manifest (these are critical for location access and network calls):

<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.INTERNET" />

<application ...>
    <!-- Optional: Explicitly disable WorkManager's auto-initialization for better control -->
    <provider
        android:name="androidx.startup.InitializationProvider"
        android:authorities="${applicationId}.androidx-startup"
        android:exported="false"
        tools:node="merge">
        <meta-data
            android:name="androidx.work.WorkManagerInitializer"
            android:value="androidx.startup"
            tools:node="remove" />
    </provider>
</application>

build.gradle (Module Level)

Add these dependencies to your module's build file:

dependencies {
    // WorkManager for reliable background tasks
    implementation "androidx.work:work-runtime-ktx:2.8.1"
    // Retrofit + Gson for server communication
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation "com.squareup.retrofit2:converter-gson:2.9.0"
    // Google's Fused Location Provider for accurate location data
    implementation "com.google.android.gms:play-services-location:21.0.1"
    // Coroutines for safe async operations
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
}

Step 2: Core Implementation

1. Location & Geocoding Utility Class

This class wraps location fetching and converts coordinates to a human-readable address:

import android.content.Context
import android.location.Address
import android.location.Geocoder
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.Locale
import kotlin.coroutines.resume

class LocationUtils(private val context: Context) {
    private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)

    // Fetch the last known location (suspend function for coroutine support)
    suspend fun getLastKnownLocation(): android.location.Location? {
        return suspendCancellableCoroutine { continuation ->
            fusedLocationClient.lastLocation.addOnSuccessListener { location ->
                continuation.resume(location)
            }.addOnFailureListener {
                continuation.resume(null)
            }
        }
    }

    // Convert location coordinates to a full address string
    suspend fun getAddressFromLocation(location: android.location.Location): String? {
        return try {
            val geocoder = Geocoder(context, Locale.getDefault())
            val addresses: List<Address>? = geocoder.getFromLocation(location.latitude, location.longitude, 1)
            addresses?.firstOrNull()?.getAddressLine(0)
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }
}

2. Retrofit API Service

Define your server endpoint for sending the address:

import retrofit2.http.Body
import retrofit2.http.POST

// Data class to match your server's request format
data class AddressRequest(val address: String)

interface ApiService {
    @POST("your-server-endpoint") // Replace with your actual API endpoint
    suspend fun sendAddress(@Body request: AddressRequest)
}

3. WorkManager Worker Class

This is the background task that handles location fetching, server calls, and UI updates:

import android.content.Context
import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class LocationWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    companion object {
        // Constants for broadcast communication with MainActivity
        const val ACTION_ADDRESS_UPDATED = "com.yourpackage.ACTION_ADDRESS_UPDATED"
        const val EXTRA_ADDRESS = "address"
        const val WORK_TAG = "LOCATION_WORK_TAG"
    }

    private val locationUtils = LocationUtils(applicationContext)
    private val apiService: ApiService by lazy {
        Retrofit.Builder()
            .baseUrl("https://your-base-url.com/") // Replace with your server's base URL
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }

    override suspend fun doWork(): Result {
        return try {
            // 1. Get the latest location
            val location = locationUtils.getLastKnownLocation() ?: return Result.retry()

            // 2. Convert location to an address
            val address = locationUtils.getAddressFromLocation(location) ?: return Result.retry()

            // 3. Send address to your server
            apiService.sendAddress(AddressRequest(address))

            // 4. Broadcast the address to update MainActivity's TextView
            val intent = Intent(ACTION_ADDRESS_UPDATED).apply {
                putExtra(EXTRA_ADDRESS, address)
            }
            LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)

            Result.success()
        } catch (e: Exception) {
            e.printStackTrace()
            // Retry the task if something fails (e.g., network error, location unavailable)
            Result.retry()
        }
    }
}

4. MainActivity Code

This handles the toggle button, starts/stops background tasks, and updates the UI:

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.widget.TextView
import android.widget.ToggleButton
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import java.util.concurrent.TimeUnit

class MainActivity : AppCompatActivity() {

    private lateinit var tvAddress: TextView
    private lateinit var toggleButton: ToggleButton
    private lateinit var workManager: WorkManager

    // Broadcast receiver to receive address updates from the Worker
    private val addressReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            intent?.getStringExtra(LocationWorker.EXTRA_ADDRESS)?.let { address ->
                tvAddress.text = "Current Address: $address"
            }
        }
    }

    // Permission request launcher for location access
    private val requestPermissionsLauncher = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { permissions ->
        val fineLocationGranted = permissions[android.Manifest.permission.ACCESS_FINE_LOCATION] ?: false
        val backgroundLocationGranted = permissions[android.Manifest.permission.ACCESS_BACKGROUND_LOCATION] ?: false

        if (!fineLocationGranted || !backgroundLocationGranted) {
            Toast.makeText(this, "Permissions are required to get location updates", Toast.LENGTH_LONG).show()
            toggleButton.isChecked = false
        } else {
            startLocationUpdates()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        tvAddress = findViewById(R.id.tv_address)
        toggleButton = findViewById(R.id.toggle_button)
        workManager = WorkManager.getInstance(this)

        // Register the broadcast receiver
        LocalBroadcastManager.getInstance(this).registerReceiver(
            addressReceiver,
            IntentFilter(LocationWorker.ACTION_ADDRESS_UPDATED)
        )

        // Toggle button logic to start/stop updates
        toggleButton.setOnCheckedChangeListener { _, isChecked ->
            if (isChecked) {
                toggleButton.text = "Stop Updates"
                // Request permissions before starting background tasks
                requestPermissionsLauncher.launch(
                    arrayOf(
                        android.Manifest.permission.ACCESS_FINE_LOCATION,
                        android.Manifest.permission.ACCESS_COARSE_LOCATION,
                        android.Manifest.permission.ACCESS_BACKGROUND_LOCATION
                    )
                )
            } else {
                toggleButton.text = "Start Updates"
                stopLocationUpdates()
                tvAddress.text = "Location updates stopped"
            }
        }
    }

    private fun startLocationUpdates() {
        // Note: Android enforces a 15-minute minimum interval for periodic work on API 26+
        // Use 1 minute for testing, but switch to 15+ minutes for production
        val workRequest = PeriodicWorkRequestBuilder<LocationWorker>(1, TimeUnit.MINUTES)
            .addTag(LocationWorker.WORK_TAG)
            .build()

        workManager.enqueue(workRequest)
    }

    private fun stopLocationUpdates() {
        // Cancel all background tasks with our unique tag
        workManager.cancelAllWorkByTag(LocationWorker.WORK_TAG)
    }

    override fun onDestroy() {
        super.onDestroy()
        // Unregister the broadcast receiver to avoid memory leaks
        LocalBroadcastManager.getInstance(this).unregisterReceiver(addressReceiver)
    }
}

5. Activity Layout (activity_main.xml)

Add the toggle button and TextView to your layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    android:gravity="center">

    <ToggleButton
        android:id="@+id/toggle_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textOff="Start Updates"
        android:textOn="Stop Updates"/>

    <TextView
        android:id="@+id/tv_address"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:textSize="16sp"
        android:text="No address yet"/>

</LinearLayout>

Key Tips for Testing & Production

  • Background Restrictions: On devices like Xiaomi, Samsung, or Huawei, you'll need to manually enable "Background Activity" for your app in Settings > Apps > [Your App] > Battery > Background Activity. This ensures WorkManager runs reliably.
  • Periodic Interval: Android requires periodic WorkManager tasks to have a minimum interval of 15 minutes on API 26+. The 1-minute interval works for testing, but adjust it to 15+ minutes for production.
  • Foreground Worker (Optional): For even more reliable execution on Android 12+, convert the LocationWorker to a ForegroundWorker to show a persistent notification (required for long-running background tasks).

内容的提问来源于stack exchange,提问作者Dioo B

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 21:02:46