Use Android Studio composable preview with multiple flavored projects - android

I have a project with several flavors. Each of these flavors has its own configuration which is available as a json file in the assets folder in the respective project structure.
In the theme definition I read the JSON using Gson and cast it into a corresponding model.
My problem is now the following:
At runtime of the app this all works wonderfully but in the composable preview in Android Studio it unfortunately only works for a single flavor. As soon as I switch to another flavor in the build variant, the json-asset of the old variant continues to load. Since the configuration also contains assets that are only available in the respective flavors, this leads to a crash of the preview.
I debugged the preview handling by throwing some exceptions during the casting and it seems, like if there'S something cached and not reset after build-variant change. A restart of Android Studio didn't also help so I don't quite know what to do about it.
Has anyone noticed a similar behavior and/or found a solution for it?
Here is some code to explain::
My theme definition:
object AppTheme {
val colors: AppColors
#Composable
#ReadOnlyComposable
get() = LocalAppColors.current
val typography: AppTypography
#Composable
#ReadOnlyComposable
get() = LocalAppTypography.current
val configuration: ConfigurationC
#Composable
#ReadOnlyComposable
get() = LocalAppConfiguration.current
}
private val LocalAppColors = staticCompositionLocalOf {
lightAppColors
}
private val LocalAppTypography = staticCompositionLocalOf {
appTypography
}
private val LocalAppConfiguration = staticCompositionLocalOf {
ConfigurationC()
}
#Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: #Composable () -> Unit,
) {
val colors = if (darkTheme) darkAppColors else lightAppColors
CompositionLocalProvider(
LocalAppConfiguration provides ConfigurationC.init(LocalContext.current),
LocalAppColors provides colors,
LocalAppTypography provides typography,
) {
MaterialTheme(
colors = colors.materialColors,
typography = typography.materialTypography,
content = content,
)
}
}
A simple Preview:
#Composable
#Preview(name = "light", showBackground = true)
#Preview(name = "dark", showBackground = true, uiMode = UI_MODE_NIGHT_YES)
fun EnabledPreview() {
AppTheme {
Button.MyCustomButton(
modifier = Modifier,
title = "Custom Button",
font = AppTheme.configuration.font.h1
color = AppTheme.configuration.colors.text1
enabled = enabled,
onClick = {}
)
}
}

Related

Can we or should use Preview compose function for main widget as well?

Like below are two functions
#Composable
private fun WaterCounter(modifier: Modifier = Modifier) {
val count = 0
Text(
text = "You've had $count glasses of water",
modifier = modifier.padding(all = 16.dp)
)
}
#Preview(showBackground = true)
#Composable
private fun PreviewWaterCounter() {
WaterCounter()
}
So, wouldn't it be better if we add #Preview annotation to the WaterCounter, which will save some lines of code and will work both as a preview and a widget?
For simple situations like your posted code, having a separate composable preview seems a bit too much, but consider this scenario with 2 composables with non-default parameters,
#Composable
fun PersonBiography(
details: Data,
otherParameters : Any?
) {
Box(
modifier = Modifier.background(Color.Red)
) {
Text(details.dataValue)
}
}
#Composable
fun AccountDetails(
details: Data
) {
Box(
modifier = Modifier.background(Color.Green)
) {
Text(details.dataValue)
}
}
both of them requires same data class , the first one has an additional parameter. If I have to preview them I have to break their signature, assigning default values to them just for the sake of the preview.
#Preview
#Composable
fun PersonBiography(
details: Data = Data(dataValue = ""),
otherParameters : Any? = null
) { … }
#Preview
#Composable
fun AccountDetails(
details: Data = Data(dataValue = "")
) { … }
A good workaround on this is having 2 separate preview composables and taking advantage of PreviewParameterProvider to have a re-usable utility that can provide instances of the parameters I needed.
class DetailsPreviewProvider : PreviewParameterProvider<Data> {
override val values = listOf(Data(dataValue = "Some Data")).asSequence()
}
#Preview
#Composable
fun PersonBiographyPreview(#PreviewParameter(DetailsPreviewProvider::class) details: Data) {
PersonBiography(
details = details,
// you may also consider creating a separate provider for this one if needed
null
)
}
#Preview
#Composable
fun AccountDetailsPreview(#PreviewParameter(DetailsPreviewProvider::class) details: Data) {
AccountDetails(details)
}
Or if PreviewParameterProvider is a bit too much, you can simply create a preview composable where you can create and supply the mock data.
#Preview
#Composable
fun AccountDetailsPreview() {
val data = Data("Some Account Information")
AccountDetails(data)
}
With any of these approaches, you don't need to break your actual composable's structure just to have a glimpse of what it would look like.

How to use sealed class for placeholder values in string resource

Is it possible within Jetpack Compose to use a sealed class to display strings with different values in their placeholders? I got confused when trying to figure out what to use for the Text objects. i.e. text = stringResource(id = it.?)
strings.xml
<string name="size_placeholder">Size %1$d</string>
<string name="sizes_placeholder_and_placeholder">Sizes %1$d and %2$d</string>
MainActivity.kt
sealed class Clothes {
data class FixedSizeClothing(val size: String, val placeholder: String): Clothes()
data class MultiSizeClothing(val sizes: String, val placeholders: List<String>): Clothes()
}
#Composable
fun ClothesScreen() {
val clothingItems = remember { listOf(
Clothes.FixedSizeClothing(itemSize = stringResource(id = R.string.size), itemPlaceholder = "8"),
Clothes.MultiSizeClothing(itemSizes = stringResource(id = R.string.sizes), itemPlaceholders = listOf("0", "2"))
)
}
Scaffold(
topBar = { ... },
content = { it ->
Row {
LazyColumn(
modifier = Modifier.padding(it)
) {
items(items) {
Column() {
Text(
text = stringResource(id = it.?)
)
Text(
text = stringResource(id = it.?)
)
}
}
}
}
},
containerColor = MaterialTheme.colorScheme.background
)
expected result
This is more a question about how to check for the type of a subclass of a sealed class (or sealed interface). Just to avoid any confusion, it should be made clear that these are Kotlin features and are not related to Jetpack Compose.
But yes, they can be used inside Composables as well or anywhere you want, really.
You would use a when (...) expression on the value of your sealed class to determine what to do based on the (sub)type of your sealed class (it works the same for sealed interfaces). Inside the when expression you then use the is operator to check for different subtypes.
val result = when (it) {
is Clothes.FixedSizeClothing -> {
// it.size and it.placeholder are accessible here due to Kotlin smart cast
// do something with them...
// last line will be returned as the result
}
is Clothes.MultiSizeClothing -> {
// it.sizes and it.placeholders are accessible here due to Kotlin smart cast
// do something with them...
// last line will be returned as the result
}
In situations when you don't need the result you just omit the val result = part. Note that the name result is arbitrary, pick whatever best describes the value you are creating.
The advantage of this type of the when expression is that it will give you a warning (and in future versions an error) if you forget one of the subtypes inside the when expression. This means the when expression is always exhaustive when there is no warning present, i.e. the Kotlin compiler checks at compile-time for all subtypes of the specific sealed class that are defined inside your whole codebase and makes sure that you are accounting for all of them inside the when expression.
For more on sealed classes inside a when expression see https://kotlinlang.org/docs/sealed-classes.html#sealed-classes-and-when-expression
In your case, you would do the same to generate the text value that you would then pass into the Text(text = ...) composable.
Scaffold(
topBar = { ... },
content = { it ->
Row {
LazyColumn(
modifier = Modifier.padding(it)
) {
items(clothingItems) {
val text = when (it) {
is Clothes.FixedSizeClothing ->
stringResource(id = R.string.size, it.placeholder)
is Clothes.MultiSizeClothing ->
stringResource(id = R.string.sizes, it.placeholders[0], it.placeholders[1])
}
Text(text = text)
}
}
}
},
containerColor = MaterialTheme.colorScheme.background
)
I used the stringResource(#StringRes id: Int, vararg formatArgs: Any): String version of the call above to construct the final text. See here for other options available in Compose https://developer.android.com/jetpack/compose/resources
If you do want to store the presentation String inside your data classes as you are trying to do in your example you could store the resource id instead of the resolved String.
Also since you are using integer placeholders (%1$d) in your resource strings, the type of your size and sizes values can be Int. So all together that would be something like this
import androidx.annotation.StringRes
sealed class Clothes {
data class FixedSizeClothing(#StringRes val size: Int, val placeholder: Int): Clothes()
data class MultiSizeClothing(#StringRes val sizes: Int, val placeholders: List<Int>): Clothes()
}
And then when you define the items you would not call stringResource(id = ...) anymore and your size and sizes values are just integers.
val clothingItems = remember {
listOf(
Clothes.FixedSizeClothing(size = R.string.size, placeholder = 8),
Clothes.MultiSizeClothing(sizes = R.string.sizes, placeholders = listOf(0, 2))
)
}
The added benefit of this is that now you do not need a Composable context (or a Context or a Resources reference) to create instances of your Clothes sealed class.
Then when declaring the UI, you would do something like this
LazyColumn(
modifier = Modifier.padding(it)
) {
items(clothingItems) {
val text = when (it) {
is Clothes.FixedSizeClothing ->
stringResource(id = it.size, it.placeholder)
is Clothes.MultiSizeClothing ->
stringResource(id = it.sizes, it.placeholders[0], it.placeholders[1])
}
Text(text = text)
}
}

Prevent system font scaling - Jetpack Compose

I am trying to restric the app from affected fro system font scaling. I had gone through many solutions but none helped. Most of them tell use dp instead of sp for text sizes but in compose we can use only sp if i am right as it expects a Text Unit.
Is there any right way to restrict font scaling in our app done with jetpack compose ? Please help .
(Solutions refered) : https://l.workplace.com/l.php?u=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F21546805%2Fhow-to-prevent-system-font-size-changing-effects-to-android-application&h=AT0zIuBPbUONm0T6q8PtqbxCdX6P_ywlp-yFGrqPMqZt7H3wsWYltKO5XwbW3i0lenrxxLi3nn_kMO4aPtFUfig2iG0BcRZpd0wTuZ1_XFpdsjDM6E7RPyZ-G_c2dlmuzGqsSEHYbqBJun0hLLZgOpRUszKbe9-1xQ
You can have an extension for Int or Float like this
#Composable
fun Int.scaledSp(): TextUnit {
val value: Int = this
return with(LocalDensity.current) {
val fontScale = this.fontScale
val textSize = value / fontScale
textSize.sp
}
You can add an extension parameter of Int
val Int.scaledSp:TextUnit
#Composable get() = scaledSp()
Text(text = "Hello World", fontSize = 20.scaledSp)
override fun attachBaseContext(newBase: Context?) {
val newOverride = Configuration(newBase?.resources?.configuration)
if (newOverride.fontScale >= 1.1f)
newOverride.fontScale = 1.1f
applyOverrideConfiguration(newOverride)
super.attachBaseContext(newBase)
}
You can use something like this in your main activity.
Till there is no solution on jetpack compose for Text(), you can use AndroidView:
#Composable
fun CustomText(
// attributes you need to set
){
AndroidView(factory = { context ->
AppCompatTextView(context).apply {
setTextSize(TypedValue.COMPLEX_UNIT_DIP, 25)
setText("")
// other attributes you want to set or other features which is not available in jetpack compose now.
}
},)
}

Jetpack Compose application-wide conditional TopAppBar best practice

I have an Android Jetpack Compose application that uses BottomNavigation and TopAppBar composables. From the tab opened via BottomNavigation users can navigate deeper into the navigation graph.
The problem
The TopAppBar composable must represent the current screen, e.g. display its name, implement some options that are specific to the screen opened, the back button if the screen is high-level. However, Jetpack Compose seems to have no out-of-the-box solution to that, and developers must implement it by themselves.
So, obvious ideas come with obvious drawbacks, some ideas are better than others.
The baseline for tracking navigation, as suggested by Google (at least for BottomNavigation), is a sealed class containing objects that represent the current active screen. Specifically for my project, it's like this:
sealed class AppTab(val route: String, #StringRes val resourceId: Int, val icon: ImageVector) {
object Events: AppTab("events_tab", R.string.events, Icons.Default.EventNote)
object Projects: AppTab("projects_tab", R.string.projects, Icons.Default.Widgets)
object Devices: AppTab("devices_tab", R.string.devices, Icons.Default.DevicesOther)
object Employees: AppTab("employees_tab", R.string.employees, Icons.Default.People)
object Profile: AppTab("profile_tab", R.string.profile, Icons.Default.AccountCircle)
}
Now the TopAppBar can know what tab is opened, provided we remember the AppTab object, but how does it know if a screen is opened from within a given tab?
Solution 1 - obvious and obviously wrong
We provide each screen its own TopAppBar and let it handle all the necessary logic. Aside from a lot of code duplication, each screen's TopAppBar will be recomposed on opening the screen, and, as described in this post, will flicker.
Solution 2 - not quite elegant
From now on I decided to have a single TopAppBar in my project's top level composable, that will depend on a state with current screen saved. Now we can easily implement logic for Tabs.
To solve the problem of screens opened from within a Tab, I extended Google's idea and implemented a general AppScreen class that represents every screen that can be opened:
// This class represents any screen - tabs and their subscreens.
// It is needed to appropriately change top app bar behavior
sealed class AppScreen(#StringRes val screenNameResource: Int) {
// Employee-related
object Employees: AppScreen(R.string.employees)
object EmployeeDetails: AppScreen(R.string.profile)
// Events-related
object Events: AppScreen(R.string.events)
object EventDetails: AppScreen(R.string.event)
object EventNew: AppScreen(R.string.event_new)
// Projects-related
object Projects: AppScreen(R.string.projects)
// Devices-related
object Devices: AppScreen(R.string.devices)
// Profile-related
object Profile: AppScreen(R.string.profile)
}
I then save it to a state in the top-level composable in the scope of TopAppBar and pass currentScreenHandler as an onNavigate argument to my Tab composables:
var currentScreen by remember { mutableStateOf(defaultTab.asScreen()) }
val currentScreenHandler: (AppScreen) -> Unit = {navigatedScreen -> currentScreen = navigatedScreen}
// Somewhere in the bodyContent of a Scaffold
when (currentTab) {
AppTab.Employees -> EmployeesTab(currentScreenHandler)
// And other tabs
// ...
}
And from inside the Tab composable:
val navController = rememberNavController()
NavHost(navController, startDestination = "employees") {
composable("employees") {
onNavigate(AppScreen.Employees)
Employees(it.hiltViewModel(), navController)
}
composable("employee/{userId}") {
onNavigate(AppScreen.EmployeeDetails)
Employee(it.hiltViewModel())
}
}
Now the TopAppBar in the root composable knows about higher-level screens and can implement necessary logic. But doing this for every subscreen of an app? A considerable amount of code duplication, and architecture of communication between this app bar and a composable it represents (how the composable reacts to actions performed on the app bar) is yet to be composed (pun intended).
Solution 3 - the best?
I implemented a viewModel for handling the needed logic, as it seemed like the most elegant solution:
#HiltViewModel
class AppBarViewModel #Inject constructor() : ViewModel() {
private val defaultTab = AppTab.Events
private val _currentScreen = MutableStateFlow(defaultTab.asScreen())
val currentScreen: StateFlow<AppScreen> = _currentScreen
fun onNavigate(screen: AppScreen) {
_currentScreen.value = screen
}
}
Root composable:
val currentScreen by appBarViewModel.currentScreen.collectAsState()
But it didn't solve the code duplication problem of the second solution. First of all, I had to pass this viewModel to the root composable from MainActivity, as there appears to be no other way of accessing it from inside a composable. So now, instead of passing a currentScreenHandler to Tab composables, I pass a viewModel to them, and instead of calling the handler on navigate event, I call viewModel.onNavigate(AppScreen), so there's even more code! At least, I maybe can implement a communication mechanism mentioned in the previous solution.
The question
For now the second solution seems to be the best in terms of code amount, but the third one allows for communication and more flexibility down the line for some yet to be requested features. I may be missing something obvious and elegant. Which of my implementations you consider the best, and if none, what would you do to solve this problem?
Thank you.
I use a single TopAppBar in the Scaffold and use a different title, drop-down menu, icons, etc by raising events from the Composables. That way, I can use just a single TopAppBar with different values. Here is an example:
val navController = rememberNavController()
var canPop by remember { mutableStateOf(false) }
var appTitle by remember { mutableStateOf("") }
var showFab by remember { mutableStateOf(false) }
var showDropdownMenu by remember { mutableStateOf(false) }
var dropdownMenuExpanded by remember { mutableStateOf(false) }
var dropdownMenuName by remember { mutableStateOf("") }
var topAppBarIconsName by remember { mutableStateOf("") }
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
val tourViewModel: TourViewModel = viewModel()
val clientViewModel: ClientViewModel = viewModel()
navController.addOnDestinationChangedListener { controller, _, _ ->
canPop = controller.previousBackStackEntry != null
}
val navigationIcon: (#Composable () -> Unit)? =
if (canPop) {
{
IconButton(onClick = { navController.popBackStack() }) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back Arrow"
)
}
}
} else {
{
IconButton(onClick = {
scope.launch {
scaffoldState.drawerState.apply {
if (isClosed) open() else close()
}
}
}) {
Icon(Icons.Filled.Menu, contentDescription = null)
}
}
}
Scaffold(
scaffoldState = scaffoldState,
drawerContent = {
DrawerContents(
navController,
onMenuItemClick = { scope.launch { scaffoldState.drawerState.close() } })
},
topBar = {
TopAppBar(
title = { Text(appTitle) },
navigationIcon = navigationIcon,
elevation = 8.dp,
actions = {
when (topAppBarIconsName) {
"ClientDirectoryScreenIcons" -> {
// search icon on client directory screen
IconButton(onClick = {
clientViewModel.toggleSearchBar()
}) {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = "Search Contacts"
)
}
}
}
if (showDropdownMenu) {
IconButton(onClick = { dropdownMenuExpanded = true }) {
Icon(imageVector = Icons.Filled.MoreVert, contentDescription = null)
DropdownMenu(
expanded = dropdownMenuExpanded,
onDismissRequest = { dropdownMenuExpanded = false }
) {
// show different dropdowns based on different screens
when (dropdownMenuName) {
"ClientDirectoryScreenDropdown" -> ClientDirectoryScreenDropdown(
onDropdownMenuExpanded = { dropdownMenuExpanded = it })
}
}
}
}
}
)
},
...
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
NavHost(
navController = navController,
startDestination = Screen.Tours.route
) {
composable(Screen.Tours.route) {
TourScreen(
tourViewModel = tourViewModel,
onSetAppTitle = { appTitle = it },
onShowDropdownMenu = { showDropdownMenu = it },
onTopAppBarIconsName = { topAppBarIconsName = it }
)
}
Then set the TopAppBar values from different screens like this:
#Composable
fun TourScreen(
tourViewModel: TourViewModel,
onSetAppTitle: (String) -> Unit,
onShowDropdownMenu: (Boolean) -> Unit,
onTopAppBarIconsName: (String) -> Unit
) {
LaunchedEffect(Unit) {
onSetAppTitle("Tours")
onShowDropdownMenu(false)
onTopAppBarIconsName("")
}
...
Not probably the perfect way of doing it, but no duplicate code.

Change the size of ModalDrawer when expanded, Jetpack Compose

I am trying to implement a drawer in my application but it's currently way to large. I'll clarify what I mean and I am aware of other questions but they don't seem to have gotten a proper answer and my usecase seems to differ a bit.
This is the code that has helped me achieve this
#Composable
fun MainContent(component: Main) {
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
val openDrawer = {
scope.launch {
drawerState.open()
}
}
Children(routerState = component.routerState, animation = crossfadeScale()) {
when (val child = it.instance) {
is Main.Child.Dashboard -> {
HomeView() { ModalDrawer(drawerState) { DashboardContent(child.component) { openDrawer() } } }
}
}
}
}
#Composable // This is a wrapper around current screen
fun HomeView( content: #Composable () -> Unit) {
Scaffold(modifier = Modifier.fillMaxSize()) {
content()
}
}
#Composable // Here is the actual drawer
fun ModalDrawer(drawerState: DrawerState, content: #Composable () -> Unit) {
ModalDrawer(
drawerState = drawerState,
drawerContent = {
Text("1234")
Text("12345")
},
content = {
content()
},
gesturesEnabled = drawerState.isOpen
)
}
the children part you can ignore, it just basically checks what screen is currently active (right now its only dashboard, but as I add screens to the mixture it will also feature other ones, and to work properly with the Drawer I had to go about this way.)
This is a multiplatform app, all of the compose code is in the CommonMain Module.
No, this was build using material guidelines and this size cannot be changed
But you can take source code of this composable and create your own modal drawer.
It'll take your some time, try copying all code from container file and you will be left to deal with a small number of internals

Categories

Resources