Android应用Mapbox初始化异常:无法显示用户位置及更新相机
问题现象
在Android应用中集成Mapbox后出现以下问题:
- 应用启动后仅显示静态地图
- 无法看到用户位置标记
- 位置更新时相机视角无变化
- 切换地图样式无效果
相关代码
HomeFragment
@AndroidEntryPoint class HomeFragment : Fragment() { private var _binding: FragmentHomeBinding? = null private val binding: FragmentHomeBinding get() = _binding!! private lateinit var onBackPressedCallback: OnBackPressedCallback private val viewModel: HomeViewModel by viewModels() private lateinit var map: GoogleMap private lateinit var mapView: MapView private lateinit var locationComponentPlugin: LocationComponentPlugin private val markersManager = MarkersManager() private val routesManager = RoutesManager() private val onIndicatorBearingChangedListener = OnIndicatorBearingChangedListener { mapView.mapboxMap.setCamera(CameraOptions.Builder().bearing(it).build()) } private val onIndicatorPositionChangedListener = OnIndicatorPositionChangedListener { mapView.mapboxMap.setCamera(CameraOptions.Builder().center(it).build()) mapView.gestures.focalPoint = mapView.mapboxMap.pixelForCoordinate(it) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) MapboxOptions.accessToken = requireContext().getString(R.string.mapbox_access_token) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentHomeBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { setupMap() setupView() setupOnBackPress() setupViewModelSubscriptions() } override fun onResume() { super.onResume() val missingPermissions = PermissionUtils.checkPermissions(requireContext(), neededPermissions) if (missingPermissions.isNotEmpty()) { findNavController() .navigate(R.id.action_homeFragment_to_missingPermissionsFragment) } } override fun onDestroyView() { super.onDestroyView() onBackPressedCallback.remove() _binding = null } private fun setupView() { binding.btnManageCars.setOnClickListener { findNavController().navigate(R.id.action_homeFragment_to_manageCarsFragment) } binding.btnPlaceCar.setOnClickListener { onPlaceClicked() } binding.btnStartNavi.setOnClickListener { viewModel.onEvent(HomeEvent.StartNavigation) } binding.btnCancelNavi.setOnClickListener { viewModel.onEvent(HomeEvent.CancelNavigation) } } private fun setupMap() { mapView = MapView(requireContext()) mapView.mapboxMap.loadStyle(Style.STANDARD) initLocationComponent() } private fun initLocationComponent() { locationComponentPlugin = mapView.location locationComponentPlugin.updateSettings { puckBearing = PuckBearing.COURSE puckBearingEnabled = true enabled = true locationPuck = LocationPuck2D( bearingImage = ImageHolder.from(com.mapbox.maps.R.drawable.mapbox_user_puck_icon), shadowImage = ImageHolder.from(com.mapbox.maps.R.drawable.mapbox_user_icon_shadow), scaleExpression = interpolate { linear() zoom() stop { literal(0.0) literal(0.6) } stop { literal(20.0) literal(1.0) } }.toJson() ) } locationComponentPlugin.addOnIndicatorPositionChangedListener(onIndicatorPositionChangedListener) locationComponentPlugin.addOnIndicatorBearingChangedListener(onIndicatorBearingChangedListener) } private fun setupOnBackPress() { onBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { this.isEnabled = false requireActivity().onBackPressedDispatcher.onBackPressed() } } requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, onBackPressedCallback) } private fun setupViewModelSubscriptions() { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { launch { viewModel.lastLocation.collectLatest { location -> location?.let { updateUserLocation(it) } } } launch { viewModel.oneTimeEvent.collect { event -> manageOneTimeEvent(event) } } } } private fun updateUserLocation(location: Location) { mapView.mapboxMap.setCamera( CameraOptions.Builder() .center(Point.fromLngLat(location.longitude, location.latitude)) .zoom(25.0) .build() ) } }
HomeViewModel
@HiltViewModel class HomeViewModel @Inject constructor( private val startLocationUpdatesUseCase: StartLocationUpdatesUseCase, private val stopLocationUpdatesUseCase: StopLocationUpdatesUseCase, private val getCurrentLocationUseCase: GetCurrentLocationUseCase, private val getAllCarsUseCase: GetAllCarsUseCase, private val getPlacedCarsUseCase: GetPlacedCarsUseCase, private val startNavigationUseCase: StartNavigationUseCase, private val setCarLocationUseCase: SetCarLocationUseCase, private val removeCarLocationUseCase: RemoveCarLocationUseCase, ) : ViewModel() { val lastLocation: StateFlow<Location?> = getCurrentLocationUseCase() private val _uiState = MutableStateFlow(HomeState()) val uiState: StateFlow<HomeState> = _uiState.asStateFlow() private val _oneTimeEvent = MutableSharedFlow<HomeOneTimeEvent>() val oneTimeEvent: SharedFlow<HomeOneTimeEvent> = _oneTimeEvent.asSharedFlow() init { startLocationUpdatesUseCase() onEvent(HomeEvent.GetUserCars) onEvent(HomeEvent.GetPlacedCars) } ... ... ... }
Use Cases
class StartLocationUpdatesUseCase@Inject constructor( private val repository: LocationRepository ) { operator fun invoke() { repository.startLocationUpdates() } } class StopLocationUpdatesUseCase @Inject constructor( private val repository: LocationRepository ) { operator fun invoke() = repository.stopLocationUpdates() } class GetCurrentLocationUseCase @Inject constructor( private val repository: LocationRepository ) { operator fun invoke(): StateFlow<Location?> = repository.currentLocation }
Repository
class LocationRepositoryImpl @Inject constructor( private val deviceLocationProvider: DeviceLocationProvider ) : LocationRepository { private val _currentLocation = MutableStateFlow<Location?>(null) override val currentLocation: StateFlow<Location?> = _currentLocation.asStateFlow() private val locationObserver = LocationObserver { locations -> _currentLocation.value = locations.lastOrNull() } override fun startLocationUpdates() { deviceLocationProvider.addLocationObserver(locationObserver) } override fun stopLocationUpdates() { deviceLocationProvider.removeLocationObserver(locationObserver) } }
Location Module
@Module @InstallIn(SingletonComponent::class) class LocationModule { @Provides fun provideDeviceLocationProvider(): DeviceLocationProvider { val locationService = LocationServiceFactory.getOrCreate() val request = LocationProviderRequest.Builder() .interval(IntervalSettings.Builder().interval(1000L).minimumInterval(1000L).maximumInterval(5000L).build()) .displacement(0f) .accuracy(AccuracyLevel.HIGHEST) .build() val result = locationService.getDeviceLocationProvider(request) if (result.isValue) { return result.value!! } else { throw IllegalStateException("Cannot obtain DeviceLocationProvider: ${result.error?.message ?: "Unknown error"}") } } }
问题原因分析
MapView未添加到布局层级
setupMap()中仅初始化了MapView,但没有将其添加到Fragment的布局容器中。Mapbox的MapView必须作为View树的一部分存在,否则无法渲染交互内容,只会显示静态占位。MapView生命周期未同步
未实现onStart()、onStop()等Fragment生命周期方法来调用MapView对应的生命周期方法,导致地图无法正常初始化、更新和释放资源,进而出现静态无交互的问题。LocationComponent初始化时机错误
initLocationComponent()在loadStyle调用后立即执行,但loadStyle是异步操作,此时Mapbox地图实例可能还未初始化完成,导致LocationComponent无法正确绑定到地图,无法显示位置标记。冗余GoogleMap变量干扰
代码中声明了private lateinit var map: GoogleMap(Google Maps对象),但未使用,属于冗余代码,可能造成逻辑混淆。权限检查时机滞后
权限检查放在onResume()中,但地图初始化在onViewCreated(),如果权限未授予,地图已开始初始化,后续位置功能无法正常工作。
修复建议
1. 将MapView添加到布局
修改setupMap(),将MapView添加到布局容器(需在xml中添加如FrameLayout的容器,id设为mapContainer):
private fun setupMap() { mapView = MapView(requireContext()) binding.mapContainer.addView(mapView) mapView.mapboxMap.loadStyle(Style.STANDARD) { initLocationComponent() } }
2. 同步MapView生命周期
在Fragment中添加以下方法:
override fun onStart() { super.onStart() mapView.onStart() } override fun onStop() { super.onStop() mapView.onStop() } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) mapView.onSaveInstanceState(outState) } override fun onLowMemory() { super.onLowMemory() mapView.onLowMemory() }
3. 延迟初始化LocationComponent
将initLocationComponent()移到loadStyle的回调中,确保地图样式加载完成后再初始化位置组件。
4. 移除冗余变量
删除private lateinit var map: GoogleMap代码。
5. 调整权限检查顺序
在onViewCreated()中先检查权限,通过后再初始化地图:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val missingPermissions = PermissionUtils.checkPermissions(requireContext(), neededPermissions) if (missingPermissions.isNotEmpty()) { findNavController().navigate(R.id.action_homeFragment_to_missingPermissionsFragment) return } setupMap() setupView() setupOnBackPress() setupViewModelSubscriptions() }
内容的提问来源于stack exchange,提问作者gawron103

