How to pass Mapbox Access Token in Jetpack Compose - kotlin

I'm trying to add Mapbox to a Jetpack Compose project. The Documentation has no information about Jetpack Compose so I'm struggling with adding my Access Token to the Map instance.
I have added my Access Token as a string resource named mapbox_access_token as described in the Docs. But the Token doesn't get send to the request to fetch the styles I just get a 401 Error returned:
OnMapLoadError: STYLE, message: Failed to load style: HTTP status code 401, sourceID: null, tileID: null
This is my Composable to display a Map:
#Composable
fun MapboxMap() {
val mapView = rememberMap(0.0, 0.0)
val accessToken = stringResource(R.string.mapbox_access_token)
AndroidView(
factory = { mapView },
modifier = Modifier.fillMaxSize(),
update = {}
)
}
#Composable
private fun rememberMap(latitude: Double, longitude: Double): MapView {
val accessToken = stringResource(R.string.mapbox_access_token)
val context = LocalContext.current
val isDarkTheme = LocalDarkMode.current
val mapView = remember {
MapView(context).apply {
val mapBoxMap = this.getMapboxMap()
mapBoxMap.loadStyleUri(if (isDarkTheme) Style.DARK else Style.LIGHT)
mapBoxMap.setCamera(
cameraOptions = cameraOptions {
center(Point.fromLngLat(longitude, latitude))
zoom(6.0)
}
)
}
}
return mapView
}
I store the mapbox_access_token in a file at this location: res/values/mapbox_access_token.xml. The Docs say to store it inside a files called R.strings.xml might this cause MapBox to not find the my access token?
This is how my screen looks when I add my MapboxMap Composable. The map is not getting loaded because my access token is not sent with the request to get the style.

I think that you did not initialize the map properly.
Here is almost minimal example that is at least working for me.
#Composable
fun MapExample(latitude: Double = 0.0, longitude: Double = 0.0) {
val mapView = rememberMap(latitude, longitude)
ConstraintLayout(
modifier = Modifier.fillMaxSize()
) {
val mapRef = createRef()
AndroidView(
factory = { mapView },
modifier = Modifier
.constrainAs(mapRef) {
height = Dimension.fillToConstraints
width = Dimension.fillToConstraints
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
update = { }
)
}
}
#Composable
fun rememberMap(latitude: Double, longitude: Double): MapView {
val context = LocalContext.current
val mapView = remember {
MapView(context).apply {
val mapboxMap = this.getMapboxMap()
mapboxMap.loadStyleUri(Style.MAPBOX_STREETS)
mapboxMap.setCamera(
cameraOptions = cameraOptions {
center(Point.fromLngLat(longitude, latitude))
zoom(6.0)
}
)
}
}
return mapView
}
The second thing which might be the case is that you did not create and import your secret token, as can be seen here. Hope this helps. But main reason is that you probably just did not call
MapView(context).apply{
val mapboxMap = this.getMapboxMap()
mapboxMap.loadStyleUri(Style.MAPBOX_STREETS)}

I think you forgot to add this code in your rememberMap function
Mapbox.getInstance(context, stringResource(R.string.mapbox_access_token))
this is your full code look like
#Composable
private fun rememberMap(latitude: Double, longitude: Double): MapView {
// val accessToken = stringResource(R.string.mapbox_access_token)
val context = LocalContext.current
val isDarkTheme = LocalDarkMode.current
val mapView = remember {
Mapbox.getInstance(context, stringResource(R.string.mapbox_access_token))
MapView(context).apply {
val mapBoxMap = this.getMapboxMap()
mapBoxMap.loadStyleUri(if (isDarkTheme) Style.DARK else Style.LIGHT)
mapBoxMap.setCamera(
cameraOptions = cameraOptions {
center(Point.fromLngLat(longitude, latitude))
zoom(6.0)
}
)
}
}
return mapView
}

Related

Coroutines not working in jetpack Compose

I use the following way to get network data.
I start a network request in a coroutine but it doesn't work, the pagination load is not called.
But if I call the network request through the init method in the ViewModel I can get the data successfully.
#Composable
fun HomeView() {
val viewModel = hiltViewModel<CountryViewModel>()
LaunchedEffect(true) {
viewModel.getCountryList() // Not working
}
val pagingItems = viewModel.countryGroupList.collectAsLazyPagingItems()
Scaffold {
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 96.dp),
verticalArrangement = Arrangement.spacedBy(32.dp),
modifier = Modifier.fillMaxSize()) {
items(pagingItems) { countryGroup ->
if (countryGroup == null) return#items
Text(text = "Hello", modifier = Modifier.height(100.dp))
}
}
}
}
#HiltViewModel
class CountryViewModel #Inject constructor() : ViewModel() {
var countryGroupList = flowOf<PagingData<CountryGroup>>()
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
init {
getCountryList() // can work
}
fun getCountryList() {
countryGroupList = Pager(config) {
CountrySource()
}.flow.cachedIn(viewModelScope)
}
}
I don't understand what's the difference between the two calls, why doesn't it work?
Any helpful comments and answers are greatly appreciated.
I solved the problem, the coroutine was used twice in the code above, which caused network data to not be fetched.
A coroutine is used here:
fun getCountryList() {
countryGroupList = Pager(config) {
CountrySource()
}.flow.cachedIn(viewModelScope)
}
Here is another coroutine:
LaunchedEffect(true) {
viewModel.getCountryList() // Not working
}
current usage:
val execute = rememberSaveable { mutableStateOf(true) }
if (execute.value) {
viewModel.getCountryList()
execute.value = false
}

how to add a points system to an app preferences DataStore Jetpack Compose

I'm working on a Quiz app and I'm trying to add a points system to the app so that everytime the user gets a question right he gets a +1pts.
and for storing the points I use jetpack compose preferences Datastore.
the problem is whenever I want to add a point to the already saved points it doesn't work.
this is my PointsData
class PointsData(private val context: Context) {
//create the preference datastore
companion object{
private val Context.datastore : DataStore<Preferences> by preferencesDataStore("points")
val CURRENT_POINTS_KEY = intPreferencesKey("points")
}
//get the current points
val getpoints: Flow<Int> =context.datastore.data.map { preferences->
preferences[CURRENT_POINTS_KEY] ?: 0
}
// to save current points
suspend fun SaveCurrentPoints(numPoints : Int){
context.datastore.edit {preferences ->
preferences[PointsData.CURRENT_POINTS_KEY] = numPoints
}
}
}
save points methode
class SavePoints {
companion object {
#Composable
fun savepoints(numPointsToSave : Int) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val datastore = PointsData(context)
LaunchedEffect(1) {
scope.launch {
datastore.SaveCurrentPoints(numPointsToSave)
}
}
}
}
}
and whenever i want to get the number of points from my DataStore i use
val pointsdatastore = PointsData(context)
val currentpoints = pointsdatastore.getpoints.collectAsState(initial = 0)
//display it as text for example
Text(text = currentpoints.value.toString(), fontSize = 30.sp, fontWeight = FontWeight.Bold,
color = Color.White)
and to do the operation i want (add +1 o the already existing points i do this
val pointsdatastore = PointsData(context)
val currentpoints = pointsdatastore.getpoints.collectAsState(initial = 0)
SavePoints.savepoints(numPointsToSave = currentpoints.value + 1)
but it doesn't seem to work because the number of points always stays 1.
if you know whats the problem please help.
I found the answer my self but for anyone who is stuck in same situation the solution is to another method in PointsData(look at the question provided code)
the method is:
suspend fun incrementpoints(){
context.datastore.edit { preferences->
val currentpoints = preferences[CURRENT_POINTS_KEY] ?: 0
preferences[CURRENT_POINTS_KEY] = currentpoints + 1
}
}
(if you want decrement not increment you can just change + to - )
and now in the PointsMethod(look at the question provided code) you should add
#Composable
fun incrementpoints() {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val datastore = PointsData(context)
LaunchedEffect(key1 = 1) {
scope.launch {
datastore.incrementpoints()
}
}
}

Jetpack compose custom snackbar material 3

How to achieve a custom snackbar in compose using material 3? I want to change the alignment of the snackbar. Also I want dynamic icon on the snackbar either on left or right side.
You can customize your snackbar using SnackBar composable and can change alignment using SnackbarHost alignment inside a Box if that's what you mean.
val snackState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
Box(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
) {
Column(modifier = Modifier.fillMaxSize()) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
coroutineScope.launch {
snackState.showSnackbar("Custom Snackbar")
}
}) {
Text("Show Snackbar")
}
}
SnackbarHost(
modifier=Modifier.align(Alignment.BottomStart),
hostState = snackState
) { snackbarData: SnackbarData ->
CustomSnackBar(
R.drawable.baseline_swap_horiz_24,
snackbarData.visuals.message,
isRtl = true,
containerColor = Color.Gray
)
}
}
#Composable
fun CustomSnackBar(
#DrawableRes drawableRes: Int,
message: String,
isRtl: Boolean = true,
containerColor: Color = Color.Black
) {
Snackbar(containerColor = containerColor) {
CompositionLocalProvider(
LocalLayoutDirection provides
if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
) {
Row {
Icon(
painterResource(id = drawableRes),
contentDescription = null
)
Text(message)
}
}
}
}
Validated answer does not answer the question properly because the icon is provided in a static way, not dynamic. The icon is not passed to showSnackbar.
You can do it by having your own SnackbarVisuals :
// Your custom visuals
// Default values are the same than SnackbarHostState.showSnackbar
data class SnackbarVisualsCustom(
override val message: String,
override val actionLabel: String? = null,
override val withDismissAction: Boolean = false,
override val duration: SnackbarDuration = if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite
// You can add custom things here (for you it's an icon)
#DrawableRes val drawableRes: Int
) : SnackbarVisuals
// The way you decide how to display your custom visuals
SnackbarHost(hostState = snackbarHostState) {
val customVisuals = it.visuals as SnackbarVisualsCustom
Snackbar {
// Here is your custom snackbar where you use your icon
}
}
// To display the custom snackbar
snackbarHostStateScope.launch {
snackbarHostState.showSnackbar(
SnackbarVisualsCustom(
message = "The message",
drawableRes = R.drawable.your_icon_id
)
)
}

Expose value from SharedPreferences as Flow

I'm trying to get a display scaling feature to work with JetPack Compose. I have a ViewModel that exposes a shared preferences value as a flow, but it's definitely incorrect, as you can see below:
#HiltViewModel
class MyViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
private val _densityFactor: MutableStateFlow<Float> = MutableStateFlow(1.0f)
val densityFactor: StateFlow<Float>
get() = _densityFactor.asStateFlow()
private fun getDensityFactorFromSharedPrefs(): Float {
val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
return sharedPreference.getFloat("density", 1.0f)
}
// This is what I look at and go, "this is really bad."
private fun densityFactorFlow(): Flow<Float> = flow {
while (true) {
emit(getDensityFactorFromSharedPrefs())
}
}
init {
viewModelScope.launch(Dispatchers.IO) {
densityFactorFlow().collectLatest {
_densityFactor.emit(it)
}
}
}
}
Here's my Composable:
#Composable
fun MyPageRoot(
modifier: Modifier = Modifier,
viewModel: MyViewModel = hiltViewModel()
) {
val densityFactor by viewModel.densityFactor.collectAsState(initial = 1.0f)
CompositionLocalProvider(
LocalDensity provides Density(
density = LocalDensity.current.density * densityFactor
)
) {
// Content
}
}
And here's a slider that I want to slide with my finger to set the display scaling (the slider is outside the content from the MyPageRoot and will not change size on screen while the user is using the slider).
#Composable
fun ScreenDensitySetting(
modifier: Modifier = Modifier,
viewModel: SliderViewModel = hiltViewModel()
) {
var sliderValue by remember { mutableStateOf(viewModel.getDensityFactorFromSharedPrefs()) }
Text(
text = "Zoom"
)
Slider(
value = sliderValue,
onValueChange = { sliderValue = it },
onValueChangeFinished = { viewModel.setDisplayDensity(sliderValue) },
enabled = true,
valueRange = 0.5f..2.0f,
steps = 5,
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colors.secondary,
activeTrackColor = MaterialTheme.colors.secondary
)
)
}
The slider composable has its own viewmodel
#HiltViewModel
class PersonalizationMenuViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
fun getDensityFactorFromSharedPrefs(): Float {
val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
return sharedPreference.getFloat("density", 1.0f)
}
fun setDisplayDensity(density: Float) {
viewModelScope.launch {
val sharedPreference = context.getSharedPreferences(
"MEAL_ASSEMBLY_PREFS",
Context.MODE_PRIVATE
)
val editor = sharedPreference.edit()
editor.putFloat("density", density)
editor.apply()
}
}
}
I know that I need to move all the shared prefs code into a single class. But how would I write the flow such that it pulled from shared prefs when the value changed? I feel like I need a listener of some sort, but very new to Android development.
Your comment is right, that's really bad. :) You should create a OnSharedPreferenceChangeListener so it reacts to changes instead of locking up the CPU to constantly check it preemptively.
There's callbackFlow for converting listeners into Flows. You can use it like this:
fun SharedPreferences.getFloatFlowForKey(keyForFloat: String) = callbackFlow<Float> {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (keyForFloat == key) {
trySend(getFloat(key, 0f))
}
}
registerOnSharedPreferenceChangeListener(listener)
if (contains(key)) {
send(getFloat(key, 0f)) // if you want to emit an initial pre-existing value
}
awaitClose { unregisterOnSharedPreferenceChangeListener(listener) }
}.buffer(Channel.UNLIMITED) // so trySend never fails
Then your ViewModel becomes:
#HiltViewModel
class MyViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
private val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
val densityFactor: StateFlow<Float> = sharedPreferences
.getFloatFlowForKey("density")
.stateIn(viewModelScope, SharingStarted.Eagerly, 1.0f)
}

How to eliminate passing View Model to this Jetpack Compose Kotlin Home Screen?

I'm trying to clean-up my code and eliminate View Models where not necessary.
I'd like to be able to access itemList, filtering, and itemListFiltered in my HomeScreen function without explicitly passing the MainViewModel as a parameter, but I can't figure out how to do it.
I tried using itemList = model::itemList but Android Studio gives error: Type mismatch. Required: List<Int> Found: KProperty0<SnapshotStateList<Int>>
View Model
class MainViewModel : ViewModel() {
val itemList = mutableStateListOf<Int>()
val filtering = mutableStateOf<Boolean>(false)
val itemListFiltered = mutableStateListOf<Int>()
fun addItem(item: Int) {
itemList.add(item)
}
fun clearItemList() {
itemList.clear()
}
fun filterListGreaterThan(greaterThan: Int) {
itemListFiltered.clear()
itemList.forEach { item ->
if (item > greaterThan) itemListFiltered.add(item)
}
}
init {
clearItemList()
} // End Initializer
}
Home Screen Scaffolding
#Composable
fun HomeScreen(
model: MainViewModel // <-- How do I eliminate this
) {
val scope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
Scaffold(
scaffoldState = scaffoldState,
topBar = { TopBar(scope, scaffoldState) },
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
FloatingActionButton(
onAddItem = model::addItem
) },
drawerContent = {
DrawerContent(
onAddItem = model::addItem,
onResetList = model::clearItemList
) },
drawerGesturesEnabled = true,
content = {
Content(
itemList = model.itemList, // <-- How do I pass this itemList without model?
onAddItem = model::addItem
) },
bottomBar = { BottomBar() }
)
}
Can you help me figure out how to do that?
Thanks for your help!
Pass it just like that...
model.itemsList
The :: symbol doesn't work how you seem to think it does. Consider it to be used for static non-changing variables/methods. Like you could use model::addItem or model::clearItemList since they are just public methods and do not hold a value.