Android开发求助:实现点击开关按钮后后台定时获取位置并通过Retrofit发送至服务器
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
LocationWorkerto aForegroundWorkerto show a persistent notification (required for long-running background tasks).
内容的提问来源于stack exchange,提问作者Dioo B

