Currently migrating my Navigation drawer to compos...
# compose-android
m
Currently migrating my Navigation drawer to compose, and it seems we need to design the drawerContent ourselves. Is there no out-of-the-box composable for this, or at least some sample code?
j
Not sure if fits your needs or not, but checkout: https://m3.material.io/components/navigation-drawer/overview
m
Thanks, but I’m surprised there is no implementation of this. Also I’m currently using M2 and trying to figure out which order to do things next: Migrate to M3, migrate main activity layout to compose (note: all fragments’ content already migrated), migrate from androidx XML navigation to e.g. voyager
j
What you mean, there is compose implementation of this linked on that page?
Just do like:
Copy code
ModalNavigationDrawer(
    drawerContent = {
        ModalDrawerSheet {
            Text("Drawer title", modifier = Modifier.padding(16.dp))
            Divider()
            NavigationDrawerItem(
                label = { Text(text = "Drawer Item") },
                selected = false,
                onClick = { /*TODO*/ }
            )
            // ...other drawer items
        }
    }
) {
    // Screen content
}
More samples here https://developer.android.com/jetpack/compose/components/drawer
Here is btw recommmend approach how to mgirate: https://developer.android.com/jetpack/compose/migrate/strategy Go from bottom up is my suggestion, that in your case seems like first fragment, then Activity. For navigation depending on what arch you have right now I recommend migrate all screens to composables, and then using androidx navigation, which is compliant with XML navigation I think programmatic generate.
m
ModalDrawerSheet
is M3 so I can’t use that now. Migration is rather tricky. Like I mentioned, I’ve migrated all Fragments to essentially be shell fragments calling screen composables. So it’s all about the main activity and migrating that now. Current strategy is to migrate the navigation drawer to Compose and programmatically navigate from there, which I guess is what you are recommending too.
j
Sorry forgot it was M2 and not M3. the M2 can be found here: https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#ModalDrawer(kotlin.Function1[…]hics.Color,kotlin.Function0) You can also using drawerContent in Scaffold component 🙂
Its hard to tell whats best strategy in those details without knowing more about your actual code and commited strategy. If you can I recommend go aggressive migration, meaning do as much as can and not doing to small steps in the last steps. Having a middle ground of mixing to much XML, Fragment and COMpose just ends up in a mess imo. Remove Fragments as fast as possible can is my recommendation, and have everything as composable blocks. I imagine you already having root Fragment view content as Composables only. If having that, would be possible share those composables in a navigation library or such with a Navigation host container, per each Activity or such. Depending how many fragments having ofc. For DIalogs like BottomsheetDialog a little bit trickier 😛
m
The M2 ModalDrawer (and Scaffold) only provides a placeholder for the drawerContent, not the drawerContent itself, unfortunately.
j
Its quite easy to build it yourself though, copy paste the ModalDrawer code and modify it to your needs 🙂 But the step from M2 to M3 is in many cases easy, if you dont have a lot of customization in your theme or such, like custom design system I mean. I recommend M3 in general, as having more components and better granular control. For those cases cannot, can copy paste code or using compose foundation covering most things.
👍 1
Also you can mix M2 and M3 in phased migration, even though I recommend avoid it, because of things like LocalContentColor being mixed up in M2 and M3. Would need to lend over things between MaterialTheme variants of M2 vs M3. But its possible ofc. If not have time to do full migration to M3 I mean 🙂
👍 1
s
Yeah we had a period in time where we had something like what this table https://github.com/HedvigInsurance/android/blob/develop/app%2Fcore%2Fcore-design-system%2FREADME.md shows to use m2 and m3 at the same time. It was not perfect with things like some style composition locals being material specific etc, but the colors worked fine after some initial setup where we made sure they were fine.
👍 2
m
My theme is pretty basic. I have a primary color and then use different shades of that for primaryVariant, secondary and secondaryVariant. I tried using the theme builder but then my app bar (which is still using legacy View-based implementation) was showing some strange background color in dark theme. I couldn’t figure out where that color was coming from so just shelved the migration until after migrating more of the activity-level View-based UI to Compose. The hope being that a Compose-based app bar would play better with the M3 theme.
Maybe a good first step would be to migrate from using
MdcTheme
to Material 2 Theme?
s
Oh yeah that we did first before anything. It helped reason about a bunch of things too, as opposed to scouring through xml code to make sense of how everything works
m
Oh joy, I have the added complexity of the app being multi-module (30 modules). Furthermore, multiple product flavors. https://stackoverflow.com/questions/70679085/android-compose-modular-app-design-how-to-manage-theming
The product variant part is actually quite tricky. With Android resource resolution, the base common module could define placeholders like:
Copy code
<resources>
    <item type="color" name="colorPrimary" />
    ...
</resources>
and each variant (perhaps in the top level app module) would provide it’s own values and those could be picked up in any module. But how to do the same thing with composables?
CompositionLocalProvider
? EDIT:
CompositionLocal
doesn’t seem to solve the problem when, for example, we have a top-level composable (so needs to set a theme) in a base module. So it looks like the colors need to be provided using DI instead.
s
Why not have this be part of MaterialTheme? The UI composable just uses MaterialTheme.colorScheme.primary, and you make sure that the theme that is provided at the beginning of your app just sends in the right color for primary inside the ColorScheme
m
Isn’t the way to initialise
MaterialTheme
to do something like:
Copy code
@Composable
fun JetnewsTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    MaterialTheme(
        colors = if (darkTheme) DarkColors else LightColors,
        typography = JetnewsTypography,
        shapes = JetnewsShapes,
        content = content
    )
}
I suppose this could be declared in variant-specific source folders of the app module and then would be used at each Activity entry point. And then for other modules, just use
MaterialTheme
? I suppose this would work so long as there are no entry points outside of the app module.
Even though my app is mostly single-Activity, there are some exceptions. For example, there is a homescreen widget module which has an Activity for the widget preferences. So in order to theme this correctly, it needs some way to get the app theme with flavor-specific colors. These won’t somehow appear in
MaterialTheme
. One option might be to have a “flavor” module that declares an
AppTheme
in each flavor-specific source. This module will be a dependency for the app module as well as any other module (e.g. homescreen widget module) that needs the proper theme at an app entry point (i.e. where
MaterialTheme
would not already be initiated properly)
In the above code, I notice that
LocalContentColor.current
and
LocalTextStyle.current.color
are not set, so I need to do:
Copy code
@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    MaterialTheme(
        colors = if (darkTheme) DarkColors else LightColors,
    ) {
        CompositionLocalProvider(
            LocalContentColor provides MaterialTheme.colors.onSurface,
            LocalTextStyle provides LocalTextStyle.current.merge(color = MaterialTheme.colors.onSurface),
            content = content,
        )
    }
}
Is that expected?
s
Yeah the local content color is setup by Surface typically, which is kinda expected to be used once top-level. If you look inside MaterialTheme I believe it should setup text style however, can you confirm that it's not the case if you look at the source code? For multiple entry points, yeah all you need is one place which properly determines the dark/light color schemes and you just call that. We have a [AppName]Theme function that does that correctly for example. That function for you can take as input the flavor in order to determine the color properly
m
Ah yes, good point regarding the
LocalContentColor
. I still think (for my use case at least), it’s better to provide the colors using DI. One benefit is that they are accessible outside of the
Composable
context. One thing I haven’t seen mentioned is how to migrate legacy
dimens.xml
resources. Presumably, just add a
Dimension.kt
file and add a bunch of top-level properties there.
j
dimens.xml works in compose already, just use dimensionResource composable 🙂 See https://developer.android.com/jetpack/compose/resources#dimensions You can also refer to existing colors.xml references in compose if prefer that. Like reference them inside your own theme and send to MaterialTheme, I often do that when migrating from XML to Compose to share same references in old and new system.
I do not recommend injecting the colors with DI, as blocking your ui from loading resources fast in app launch. Because most DI is blocking disk operations and such. I mean its possible, but decreasing performance of ui responsiveness in app start. Also takes up additional memory. Its much more efficient to rely on cached disk files, like dimens.xml or such, or having the color references in the theme level and avoid copy to much when not need to.
m
Yes, I already use
dimensionResource
(It’s pretty much the only sensible way to access
dimens.xml
from Compose, so not sure how I could access those values otherwise). There are a couple of motivations for migrating
dimens.xml
to kotlin: 1. No longer need a
Context
(nor
@Composable
) to access the values 2. No longer have theming values spread across different locations (i.e.
res/values/
and
src
The strongest reason for keep using
dimens.xml
is to take advantage of different configurations (e.g.
sw600
,
land
)
Regarding DI, I use Koin and the singleton injected is instantiated lazily. I really don’t think there is any speed or memory issue here. I already use Koin extensively, so it’s not like I’m using it just for this.
The real benefit is point (1) above, because consider what I had before here (this is for when joining cards together vertically):
Copy code
@Composable
fun Modifier.topCardPadding(): Modifier {
    val horizontalPadding = dimensionResource(id = R.dimen.card_view_horizontal_margin)
    val verticalPadding = dimensionResource(id = R.dimen.card_view_vertical_margin)
    return this.padding(start = horizontalPadding, end = horizontalPadding, top = verticalPadding, bottom = 0.dp)
}
becomes:
Copy code
fun Modifier.topCardPadding(): Modifier {
    return this.padding(
        start = CardDimensions.horizontalMargin,
        end = CardDimensions.horizontalMargin,
        top = CardDimensions.verticalMargin,
        bottom = 0.dp,
    )
}
So no longer
Composable
j
• Resources, yes its benefitial not using Context, but mosty using R.dimen.a and get Context in compose whenever needed, as having cached for file references. For colors and dimensions agree its better have in Kotlin, much easier to maintain. You could start off having references to R.dimen.A in Kotlin as well. Start with a ResourceWrapper class with @ColorInt and @DimensionRes annoations for all integer variants. Then you can do phased migrations resolve the Dp in Kotlin directly instead. For resolve qualifiers I think almost zero benefit of layout queries, easier do that programmaticly instead imo, as you can couple together more things can just one R.resourceType at the time, like combine dimensions and window size classes, and columns/grids etc. Otherwise need to assign each subset per folder qualifier, which is very annoying imo. In most cases there is generic formular can apply, to calculate all things dynamicly instead of static references. But yeah a topic in itself 😄 • For Koin still having init of the framework itself and all graph declarations regardless if singletons created lazy or not. But yes 🙂 • For the topCardPadding sure, but would apply that in MyCard composable instead and not as an modifier. Easy get fragmented, in similar way as with Kotlin extensions. Dont see benefit of re-usable modifier for that case. I would think maybe having DImensions.card.padding and using
PaddingValues
perhaps, if that even make sense. In many cases I think need apply "negative" paddings in layers to compensate for Material hard coded 48x48dp touch surface, not always desirable in compact ui designs. Like text or chip buttons.
m
Thanks Joel. Regarding using
PaddingValues
instead of
Modifier
extension functions, I agree. Not sure why I wasn’t doing that earlier.
Copy code
fun cardInListPaddingValues(index: Int, itemCount: Int): PaddingValues = when {
    itemCount == 1 -> soloCardPaddingValues()
    index == 0 -> topCardPaddingValues()
    index == itemCount - 1 -> bottomCardPaddingValues()
    else -> middleCardPaddingValues()
}

fun soloCardPaddingValues() = PaddingValues(
    horizontal = Dimensions.Card.marginHorizontal,
    vertical = Dimensions.Card.marginVertical,
)

fun topCardPaddingValues() = PaddingValues(
    start = Dimensions.Card.marginHorizontal,
    end = Dimensions.Card.marginHorizontal,
    top = Dimensions.Card.marginVertical,
    bottom = 0.dp,
)

fun bottomCardPaddingValues() = PaddingValues(
    start = Dimensions.Card.marginHorizontal,
    end = Dimensions.Card.marginHorizontal,
    top = 0.dp,
    bottom = Dimensions.Card.marginVertical,
)

fun middleCardPaddingValues() = PaddingValues(
    start = Dimensions.Card.marginHorizontal,
    end = Dimensions.Card.marginHorizontal,
    top = 0.dp,
    bottom = 0.dp,
)
I’ll have a think about the other things you mentioned.
j
Instead of having them as functions, can optimize by use them as values instead btw. To avoid re-calling method each time 🙂 Like val bottomCardPaddingValues = PaddingValues( start = CardDimensions.marginHorizontal, end = CardDimensions.marginHorizontal, top = 0.dp, bottom = CardDimensions.marginVertical, )
m
Ah yes of course!
j
I often do it like this:
Copy code
internal object MyDimensionsImpl: MyDimensions {
    override val itemDelimiterSpace: Dp = 8.dp
    override val componentSpace: Dp = 16.dp
    override val windowInsets: Dp = 20.dp
    override val icon: Dp = 24.dp
    override val illustration: Dp = 56.dp
}

interface MyDimensions {
    val itemDelimiterSpace: Dp
    val componentSpace: Dp
    val windowInsets: Dp
    val illustration: Dp
    val icon: Dp
}
If I for some reason want to replace the values in a Local composition, like previews or anything else 🙂
And then using a static object thing:
Copy code
object MyTheme {
    val typography: MyTypography
        @Composable
        @ReadOnlyComposable
        get() = LocalMyTypography.current

    val colorTheme: MyColorTheme
        @Composable
        @ReadOnlyComposable
        get() = LocalMyColorTheme.current

    val shapes: MyShapes
        @Composable
        @ReadOnlyComposable
        get() = LocalMyShapes.current

    val dimensions: MyDimensions
        @Composable
        @ReadOnlyComposable
        get() = LocalMyDimensions.current
  
}
For easy calling them from any place I want in my compose theme scope 🙂
There is ofc nothing right or wrong here, just multiple ways of doing it right. Where right is whats right for your case 🙂 But maybe give some ideas how can try proceed 🙂
m
Interesting. Some stuff to think about. I noticed the docs had two suggestions for custom theme properties like this. One was what you suggest, but then for colors, there is also
Colors
extensions which can be accessed through
MaterialTheme.colors.X
https://developer.android.com/jetpack/compose/designsystems/custom#extending-material
j
Yeah a lot of these ideas is from how MaterialTheme doing under the hood 🙂 If youre using Material 3 as it is however and only need overload their system colors, fonts etc, I recommend use what they have. In my case I almost always having 100% custom or mixing M3 and custom components, where my setup works in any case, where I can decide if I want to combine MaterialTheme or not from case to case 🙂
One huge benefit of having everything in Kotlin, is that R8 and stuff can minify fingerprint. Like Material icons is using vector paths programmatic, can they sharing the icon shapes across filled vs unfilled variants as of example. Then in end can reduce to ONLY embed the code actual used. But with files like XML need to include everything often, even if not used.
m
Even with shrinkResources enabled?
j
In many cases shrinkResources do not work for everything, just subsets of resources. It does the job in most cases, but not all. But as I mostly used 100% Kotlin/Compose never had to deal with that so much 😛
m
Instead of going the
interface
Impl
route, couldn’t you just use a data class instead like this: https://stackoverflow.com/a/73722401/444761 ? Although I suppose then you need something else (e.g. Companion object property) for access outside of
Composable
j
Yes you can, problem with data classes is you can mutate/copy them 😛
Which often do not want in design systems, dont want the consumer to modify the system, it should be used in a certain way 🙂
m
How to migrate attrs like
?android:attr/listPreferredItemHeight
? Just hard code them?
j
Depends, it would be possible resolve I think but imo easier just lookup value and set same in your own dimension subset :) I guess no right or wrong here, just different approaches.
m
One thing I don’t understand: Suppose I apply
AppTheme
(which provides a
LocalDimensions
) at the activity screen level. What happens with things like
ComposeView
and dialogs? Do I need to apply a theme there? What if I only apply
MaterialTheme
for those, will the
LocalDimensions
be somehow carried over, or is that only for
MaterialTheme.colors
?
j
If you using XML things like Android Views, you will need to provide additional bridge theme, where Compose has their theme and XML its own kind a. Hence nice to share same values while migrating. For Dialogs you can use Compose dialogs btw, which is kind a overlay thing. If using those should be able using same AppTheme for those if I remember correct. Not same issue with Activity vs Dialog window in the old world if I may call it that. But some of these things is ground I never went into, I always did chunks of migration take one complete screen at the time. Like convert entire Fragment to Compose, or entire Activity to compose (Multi Activity projects that is). Having to mix a lot like in this case can be tricky if doing that for a very long time, easy get fragmented code and design.
m
Hmm, that’s exactly what I’m migrating away from.
j
Ah right sorry 😄
m
I have very little legacy View code left. Maybe forget about the
ComposeView
thing. That was only out of interest. My general strategy has been to apply
MaterialTheme
everywhere except entry level Activity, in which case
AppTheme
j
One way of dealing with dialogs could be create a overlay thing at top level in compose. But using Dialog or AlertDialog in compose is kind of that. See https://developer.android.com/jetpack/compose/components/dialog
If you can avoid mixing to much AndroidView in COmpose vs ComposeView in AndroidView I recommend doing that, as often comes with a cost in code or tradeoffs in messy things like themes. Everything ofc possible, but mostly I think not worth it.
In some cases its just faster bite the apple and do the heavy lifting right away and trust Compose doing it good kind a 🙂 A leap of faith kind a. There is so much successful stories about Compose, I would in most cases almost promise its worth it. But ofc my opinion 🙂
m
I have almost no
AndroidView
or
ComposeView
left. I’m using androidx navigation (XML) and there is a (secondary) screen where I use
MaterialTheme
. I notice that
LocalDimensions
has been applied even though I only set it in
AppTheme
. How is that working?
j
• When I migrated in the past I often first analyze in the DI graph, what deps and libs do I have. Is all compliant with COmpose or not. Like if using Glide maybe want or need to migrate to Coil. In most cases can stick with what having, but some cases need to switch I think. • Then in phased migration start with AppTheme bridge MaterialTheme. Start with bare minimum like colors + typography if can. • Then creating if have time components for all Material components, like AppButton wrap Button, just copy paste function. That helps to mix different libs and mix custom views vs compose components or hybride solutions. • Then migrate from bottom -> up, like custom views -> composables components, fragments, activity. Try doing it in layers if possible. Ideally take entire screen at the time. • End with dialogs and dialog Fragments,as they can be opened from Compose standalone kind a.
Copy code
notice that LocalDimensions has been applied even though I only set it in AppTheme . How is that working?
If you nesting Local compositions in compose, they inherit each other, but also means you can override the child local compositions if that is accessible 🙂 Like AppTheme { MaterialTheme {} } youre AppTheme delegating LocalDimensions so also accssible in MaterialTheme in that case.
That meaning if having AppTheme at top level, no need to provide themes anywhere else. Like if using single Activity app.
m
so is it androidx navigation that is somehow carrying over
LocalDimensions
from one fragment to the next? or is it just using the same Composable context (or whatever you call it)?
j
Dont know how you using androidx navigation, depends how setup each Fragment content view. But maybe easier take this in DM? If can share parts of the code, I can try help give feedback if you want.
423 Views