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


LocationViewModel.kt
1package com.example.locationapp2<div></div>3import androidx.compose.runtime.State4import androidx.compose.runtime.mutableStateOf5import androidx.lifecycle.ViewModel6<div></div>7class LocationViewModel: ViewModel() {8 private val _location = mutableStateOf<LocationData?>(null)9 val location: State<LocationData?> = _location10<div></div>11 fun updateLocation(newLocation: LocationData) {12 _location.value = newLocation13 }14<div></div>15}1package com.example.locationapp2
3import androidx.compose.runtime.State4import androidx.compose.runtime.mutableStateOf5import androidx.lifecycle.ViewModel6
7class LocationViewModel: ViewModel() {8 private val _location = mutableStateOf<LocationData?>(null)9 val location: State<LocationData?> = _location10
11 fun updateLocation(newLocation: LocationData) {12 _location.value = newLocation13 }14
15}1private val _location = mutableStateOf<LocationData?>(null)1private val _location = mutableStateOf<LocationData?>(null)mutableStateOf 它包裝了一個變數,當值改變時,Compose UI 會自動重新組合(recompose)。
LocationData? → 泛型裡面的 ? 代表可以是 null,表示「可能還沒取得位置」。
_location 是私有的可變,location 是公開的唯讀。
1fun updateLocation(newLocation: LocationData) {2 _location.value = newLocation3}1fun updateLocation(newLocation: LocationData) {2 _location.value = newLocation3}當 LocationUtils 拿到新座標後,會呼叫updateLocation更新座標。
部分資訊可能已經過時