這是一個練習定位權限的App,我將重要的部分記錄下來。
1 2 3 <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
1 2 3 <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
android.permission.INTERNET : 允許應用程式開啟網路連線
android.permission.ACCESS_COARSE_LOCATION : 允許應用程式存取使用粗略的位置
android.permission.ACCESS_FINE_LOCATION : 允許應用程式存取使用精確的位置
LocationUtils.kt hasLocationPermission function 回傳一個 Boolean→ true 或 false,代表有沒有定位權限。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class LocationUtils(val context: Context) { private val _fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) <div></div> @SuppressLint("MissingPermission") fun requestLocationUpdates(viewModel: LocationViewModel) { val locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { super.onLocationResult(locationResult) locationResult.lastLocation?.let { val location = LocationData(latitude = it.latitude, longitude = it.longitude) viewModel.updateLocation(location) } } } val locationRequest = LocationRequest.Builder( Priority.PRIORITY_HIGH_ACCURACY, 1000 ).build() _fusedLocationClient.requestLocationUpdates( locationRequest, locationCallback, Looper.getMainLooper() ) } <div></div> fun hasLocationPermission(context: Context): Boolean { return ContextCompat.checkSelfPermission( context, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission( context, Manifest.permission.ACCESS_COARSE_LOCATION ) == PackageManager.PERMISSION_GRANTED } <div></div> fun reverseGeocodeLocation(location: LocationData): String { // The Geocoder constructor expects a java.util.Locale val geocoder = Geocoder(context, Locale.getDefault()) val coordinate = LatLng(location.latitude, location.longitude) val addresses: MutableList<Address>? = geocoder.getFromLocation(coordinate.latitude, coordinate.longitude, 1) <div></div> return if (addresses?.isNotEmpty() == true) { addresses[0]?.getAddressLine(0) ?: "Address line not found" } else { "Address not found" } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class LocationUtils(val context: Context) { private val _fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) @SuppressLint("MissingPermission") fun requestLocationUpdates(viewModel: LocationViewModel) { val locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { super.onLocationResult(locationResult) locationResult.lastLocation?.let { val location = LocationData(latitude = it.latitude, longitude = it.longitude) viewModel.updateLocation(location) } } } val locationRequest = LocationRequest.Builder( Priority.PRIORITY_HIGH_ACCURACY, 1000 ).build() _fusedLocationClient.requestLocationUpdates( locationRequest, locationCallback, Looper.getMainLooper() ) } fun hasLocationPermission(context: Context): Boolean { return ContextCompat.checkSelfPermission( context, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission( context, Manifest.permission.ACCESS_COARSE_LOCATION ) == PackageManager.PERMISSION_GRANTED } fun reverseGeocodeLocation(location: LocationData): String { // The Geocoder constructor expects a java.util.Locale val geocoder = Geocoder(context, Locale.getDefault()) val coordinate = LatLng(location.latitude, location.longitude) val addresses: MutableList<Address>? = geocoder.getFromLocation(coordinate.latitude, coordinate.longitude, 1) return if (addresses?.isNotEmpty() == true) { addresses[0]?.getAddressLine(0) ?: "Address line not found" } else { "Address not found" } } }
1 2 private val _fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)
1 2 private val _fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)
這是 Google 提供的 Fused Location Provider API ,能整合 GPS、Wi-Fi、藍牙與行動網路的定位來源,讓定位更準確且省電。_fusedLocationClient 用來執行「請求位置更新」、「停止更新」、「取得最後位置」等操作。
requestLocationUpdates 1 2 @SuppressLint("MissingPermission") fun requestLocationUpdates(viewModel: LocationViewModel) {
1 2 @SuppressLint("MissingPermission") fun requestLocationUpdates(viewModel: LocationViewModel) {
⚠️ 注意:這裡用了 @SuppressLint("MissingPermission"),代表你必須在呼叫這個函式前,確認已取得權限 (也就是前面 LocationDisplay 檢查的部分)。
意思是保證外層一定會先檢查 hasLocationPermission() 才會進來,所以不用再提醒我 MissingPermission。
這個函式會每秒更新一次位置 (因為下面設定了 1000 毫秒),然後將新位置傳給你的 LocationViewModel。
1 2 3 4 5 6 7 8 9 val locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { super.onLocationResult(locationResult) locationResult.lastLocation?.let { val location = LocationData(latitude = it.latitude, longitude = it.longitude) viewModel.updateLocation(location) } } }
1 2 3 4 5 6 7 8 9 val locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { super.onLocationResult(locationResult) locationResult.lastLocation?.let { val location = LocationData(latitude = it.latitude, longitude = it.longitude) viewModel.updateLocation(location) } } }
當系統取得新的定位資訊時,會呼叫 onLocationResult。
lastLocation 是最新的 Location 物件(包含緯度與經度)。
1 2 3 4 5 6 package com.example.locationapp <div></div> data class LocationData( val latitude: Double, val longitude: Double )
1 2 3 4 5 6 package com.example.locationapp data class LocationData( val latitude: Double, val longitude: Double )
LocationData 是我們定義的資料類別,用來包裝座標。
呼叫 viewModel.updateLocation(location) → 把資料傳給 ViewModel 更新 UI。
1 2 3 4 5 _fusedLocationClient.requestLocationUpdates( locationRequest, locationCallback, Looper.getMainLooper() )
1 2 3 4 5 _fusedLocationClient.requestLocationUpdates( locationRequest, locationCallback, Looper.getMainLooper() )
這行讓 app 持續監聽位置變化。
hasLocationPermission LocationUtils 要持有 Context,才能呼叫系統 API(如 ContextCompat.checkSelfPermission)來檢查權限。
return 這段是重點:ContextCompat.checkSelfPermission() 是Android官方提供的工具,用來檢查 App 是否擁有某個權限。它會回傳一個整數值(PERMISSION_GRANTED 或 PERMISSION_DENIED)。
這段程式碼的功能就像是你在檢查一個房間的門:
「門有沒有上鎖?如果鎖是開著的,就代表我可以進去。」
ContextCompat (檢查員): 你可以把它想像成 Android 系統中的一位通用檢查員 或萬用工具箱 。它的工作就是確保你的程式碼在所有不同型號和版本的 Android 手機上都能正常運作(這就是 Compat 兼容性的意思)。
.checkSelfPermission() (檢查方法): 這是檢查員專門用來檢查特定權限狀態 的方法。
為什麼需要 ContextCompat.checkSelfPermission()? 我們不直接使用 PackageManager.PERMISSION_GRANTED 進行判斷,是因為 PackageManager.PERMISSION_GRANTED 只是一個「結果代碼」(密碼) ,而 ContextCompat.checkSelfPermission() 才是「執行檢查的動作」 。
reverseGeocodeLocation 1 2 3 4 5 fun reverseGeocodeLocation(location: LocationData): String { val geocoder = Geocoder(context, Locale.getDefault()) val coordinate = LatLng(location.latitude, location.longitude) val addresses: MutableList<Address>? = geocoder.getFromLocation(coordinate.latitude, coordinate.longitude, 1)
1 2 3 4 5 fun reverseGeocodeLocation(location: LocationData): String { val geocoder = Geocoder(context, Locale.getDefault()) val coordinate = LatLng(location.latitude, location.longitude) val addresses: MutableList<Address>? = geocoder.getFromLocation(coordinate.latitude, coordinate.longitude, 1)
用 Geocoder 根據經緯度查詢地點名稱。getFromLocation() 會回傳一個地址列表(可能含國家、城市、街道)。
MainActivity.kt 1 2 3 4 5 6 @Composable fun MyApp(modifier: Modifier) { val context = LocalContext.current val locationUtils = LocationUtils(context) LocationDisplay(locationUtils = locationUtils, context = context) }
1 2 3 4 5 6 @Composable fun MyApp(modifier: Modifier) { val context = LocalContext.current val locationUtils = LocationUtils(context) LocationDisplay(locationUtils = locationUtils, context = context) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { val viewModel: LocationViewModel = viewModel() LocationAppTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> MyApp(viewModel = viewModel,modifier = Modifier.padding(innerPadding)) } } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { val viewModel: LocationViewModel = viewModel() LocationAppTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> MyApp(viewModel = viewModel,modifier = Modifier.padding(innerPadding)) } } } } }
Context: 是一個核心的 Android 系統類別,它提供了應用程式全域資訊,例如資源、資料庫、文件系統路徑,以及存取系統服務(如定位服務)的管道。
LocalContext.current: 這是 Jetpack Compose 框架提供的一個 CompositionLocal 。它用於在可組合函數中存取 Android 應用程式的 Context 物件。
LocationDisplay(locationUtils = locationUtils, context = context) 這行程式碼的作用是調用 一個名為 LocationDisplay 的 Compose UI 元件 ,向它傳遞所需的資料和環境依賴。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 @Composable fun LocationDisplay( locationUtils: LocationUtils, viewModel: LocationViewModel, context: Context ) { val location = viewModel.location.value val address = location?.let { locationUtils.reverseGeocodeLocation(location) } <div></div> val requestPermissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestMultiplePermissions(), onResult = { permissions -> if (permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true && permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true ) { // 擁有 location 權限 locationUtils.requestLocationUpdates(viewModel = viewModel) <div></div> } else { // 要求權限 val rationaleRequired = ActivityCompat.shouldShowRequestPermissionRationale( context as MainActivity, Manifest.permission.ACCESS_FINE_LOCATION ) || ActivityCompat.shouldShowRequestPermissionRationale( context as MainActivity, Manifest.permission.ACCESS_COARSE_LOCATION ) <div></div> if (rationaleRequired) { Toast.makeText( context, "Location Permission is required for this feature to work", Toast.LENGTH_LONG ).show() } else { Toast.makeText( context, "Location Permission is required, Please enable from settings", Toast.LENGTH_LONG ).show() } } } ) <div></div> Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { if(location != null) { Text("Address: ${location.latitude} ${location.longitude} \n $address ") } Text(text = "Location not available") <div></div> Button(onClick = { if (locationUtils.hasLocationPermission(context)) { locationUtils.requestLocationUpdates(viewModel) } else { requestPermissionLauncher.launch( arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION ) ) } }) { Text(text = "Get Location") } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 @Composable fun LocationDisplay( locationUtils: LocationUtils, viewModel: LocationViewModel, context: Context ) { val location = viewModel.location.value val address = location?.let { locationUtils.reverseGeocodeLocation(location) } val requestPermissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestMultiplePermissions(), onResult = { permissions -> if (permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true && permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true ) { // 擁有 location 權限 locationUtils.requestLocationUpdates(viewModel = viewModel) } else { // 要求權限 val rationaleRequired = ActivityCompat.shouldShowRequestPermissionRationale( context as MainActivity, Manifest.permission.ACCESS_FINE_LOCATION ) || ActivityCompat.shouldShowRequestPermissionRationale( context as MainActivity, Manifest.permission.ACCESS_COARSE_LOCATION ) if (rationaleRequired) { Toast.makeText( context, "Location Permission is required for this feature to work", Toast.LENGTH_LONG ).show() } else { Toast.makeText( context, "Location Permission is required, Please enable from settings", Toast.LENGTH_LONG ).show() } } } ) Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { if(location != null) { Text("Address: ${location.latitude} ${location.longitude} \n $address ") } Text(text = "Location not available") Button(onClick = { if (locationUtils.hasLocationPermission(context)) { locationUtils.requestLocationUpdates(viewModel) } else { requestPermissionLauncher.launch( arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION ) ) } }) { Text(text = "Get Location") } } }
主要公能:顯示一個文字 + 按鈕,並處理按下按鈕後檢查或請求定位權限。
contract = ActivityResultContracts.RequestMultiplePermissions()
表示這個 launcher 可以一次請求多個權限(這裡是 coarse + fine)。
onResult = { permissions -> ... }
系統對話框關閉後,會回傳使用者是否授權。
1 2 3 4 5 6 7 if (permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true && permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true ) { // 已擁有定位權限 } else { // 尚未取得權限,顯示提示 }
1 2 3 4 5 6 7 if (permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true && permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true ) { // 已擁有定位權限 } else { // 尚未取得權限,顯示提示 }
若兩個權限都有允許,目前就什麼都不做;否則,進入「沒有權限」的分支。
如果沒權限 → 顯示提示 Toast
1 2 3 4 5 6 7 val rationaleRequired = ActivityCompat.shouldShowRequestPermissionRationale( context as MainActivity, Manifest.permission.ACCESS_FINE_LOCATION ) || ActivityCompat.shouldShowRequestPermissionRationale( context as MainActivity, Manifest.permission.ACCESS_COARSE_LOCATION )
1 2 3 4 5 6 7 val rationaleRequired = ActivityCompat.shouldShowRequestPermissionRationale( context as MainActivity, Manifest.permission.ACCESS_FINE_LOCATION ) || ActivityCompat.shouldShowRequestPermissionRationale( context as MainActivity, Manifest.permission.ACCESS_COARSE_LOCATION )
shouldShowRequestPermissionRationale(…)
這是 Android 的 API,用來判斷:
1 2 3 4 5 if (rationaleRequired) { Toast.makeText(context, "Location Permission is required for this feature to work", Toast.LENGTH_LONG).show() } else { Toast.makeText(context, "Location Permission is required, Please enable from settings", Toast.LENGTH_LONG).show() }
1 2 3 4 5 if (rationaleRequired) { Toast.makeText(context, "Location Permission is required for this feature to work", Toast.LENGTH_LONG).show() } else { Toast.makeText(context, "Location Permission is required, Please enable from settings", Toast.LENGTH_LONG).show() }
LocationViewModel.kt 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.example.locationapp <div></div> import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel <div></div> class LocationViewModel: ViewModel() { private val _location = mutableStateOf<LocationData?>(null) val location: State<LocationData?> = _location <div></div> fun updateLocation(newLocation: LocationData) { _location.value = newLocation } <div></div> }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.example.locationapp import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel class LocationViewModel: ViewModel() { private val _location = mutableStateOf<LocationData?>(null) val location: State<LocationData?> = _location fun updateLocation(newLocation: LocationData) { _location.value = newLocation } }
1 private val _location = mutableStateOf<LocationData?>(null)
1 private val _location = mutableStateOf<LocationData?>(null)
mutableStateOf 它包裝了一個變數,當值改變時,Compose UI 會自動重新組合(recompose) 。
LocationData? → 泛型裡面的 ? 代表可以是 null,表示「可能還沒取得位置」。
_location 是私有的可變,location 是公開的唯讀。
1 2 3 fun updateLocation(newLocation: LocationData) { _location.value = newLocation }
1 2 3 fun updateLocation(newLocation: LocationData) { _location.value = newLocation }
當 LocationUtils 拿到新座標後,會呼叫updateLocation更新座標。