androidkotlinmapboxmapbox-android

Cannot properly initialize Mapbox in android application


I am trying to use Mapbox in my application but I have some problems. My code looks like this:

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)
    }
}

And 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"}")
        }
    }

}

Because of some unknown reason I am unable to see user on map, or even change the camera when location update comes. Current behavior is that app shows map on start and nothing changes. I can even change map styles but that also does nothing. Does anyone know what can be the issue?


Solution

  •     private fun setupMap() {
            mapView = MapView(requireContext())
            mapView.mapboxMap.loadStyle(Style.STANDARD)
            initLocationComponent()
        }
    

    Here, you are creating a MapView widget, are configuring it with a style, and then are throwing it away. In particular, it is not appearing in the UI, because you did not do anything to put it in your UI.

    Current behavior is that app shows map on start

    This implies that you have another MapView somewhere, perhaps in fragment_home.xml, that you should be using via FragmentHomeBinding. So, use that MapView and get rid of the one that you are creating in setupMap().