Skip to content

Setup compose multiplatform and migrate first screen to it#2829

Open
Luna712 wants to merge 21 commits into
recloudstream:masterfrom
Luna712:compose
Open

Setup compose multiplatform and migrate first screen to it#2829
Luna712 wants to merge 21 commits into
recloudstream:masterfrom
Luna712:compose

Conversation

@Luna712

@Luna712 Luna712 commented May 23, 2026

Copy link
Copy Markdown
Contributor

I explained most in code comments the best I could as well. Icons are automatically generated from https://fonts.google.com/icons which provides automatically generated code for Compose as well. In the future most icons will use this if a relevant one is found, otherwise we can use SVG icons. Compose multiplatform also supports android XML versions in a KMP context, but I actually found this to be very inconsistent and we shouldn't use that directly if we can avoid it.

I am also going to go through a lot of other changes here and some reasoning as well:

This adds reusable components that will be useful elsewhere as well:

  • .cloudStreamRipple: replacement for selectableItemBackground
  • .tvFocusable: handle manual focus for TV to ensure it is correctly following it and handles the border outline for it also, will probably need improved/changed more later though as we need different versions for it in some places
  • ProfilePicture: to reuse in all the different profile picture containers, accepts parameters where things will need to be different later
  • SettingsItem: Used for all settings category headers for now, will later also be used in all settings subpages for consistency and avoid code duplication. This may need some changes later though also

This handles Monet/Material You differently. This is a dynamic color scheme only available on Android, therefore to define it it uses expect/actual declarations. Other platforms do have their own concept of dynamic themes though, so in the commonMain API it just uses the DYNAMIC terminology for better support for these later on and a more consistent cross platform API. This applies to both the theme and primary colors for it.

Since things are not XML based (and when we do use XML based stuff like strings in composeResources, there is still no @string/ usage), this also adds new PreferenceKeys and PreferenceDefaults objects with constants for preferences. PreferenceKeys will replace all the string keys values in the current donottranslate-strings.xml and PreferenceDefaults will just include the default values for the preferences. Both are there to ensure consistency (not different defaults in different places) and to avoid accidental typos or something. For now it keeps what is added there only to what we actually use in composeApp as more are needed there these will be populated more as well.

For now, this also duplicates the default profile background images to composeResources so we can use them in :composeApp directly as well.

This also avoids refactoring to much of the app intentionally like removing/moving the companion object from the old SettingsFragment to avoid this PR growing massively. This adds a KMP version for device layout detection though as I needed to get if its a TV in :composeApp.

Eventually we will have to split :app into :androidApp which just includes the signing stuff and entry points (AndroidManifest.xml, MainActivity (but a much slimmer version) and CloudStreamApp and other entry points like services (things defined in AndroidManifest).

AGP 9 prevents the ability to have both com.android.application and com.android.kotlin.multiplatform.library at the same time, which means android entry points/APK generation can't be in a KMP module. :app will slowly be drained into fully KMP APIs into :composeApp. Stuff in there should not be stuff extensions will typically need though, otherwise it should go in :library instead.

Also, it is not immediately necessarily, but eventually we should also add the composeResources directory to weblate. This also sets up the locales python script to support that directory if and/or when we add it.

Happy to make any changes necessary though.

I explained most in code comments the best I could as well. Icons are automatically generated from https://fonts.google.com/icons which provides automatically generated code for Compose as well. In the future most icons will use this if a relevant one is found, otherwise we can use SVG icons. Compose multiplatform also supports android XML versions in a KMP context, but I actually found this to be very inconsistent and we shouldn't use that directly if we can avoid it.

I am also going to go through a lot of other changes here and some reasoning as well:

This adds reusable components that will be useful elsewhere as well:
* `.cloudStreamRipple`: replacement for `selectableItemBackground`
* `.tvFocusable`: handle manual focus for TV to ensure it is correctly following it and handles the border outline for it also, will probably need improved/changed more later though as we need different versions for it in some places
* ProfilePicture: to reuse in all the different profile picture containers, accepts parameters where things will need to be different later
* SettingsItem: Used for all settings category headers for now, will later also be used in all settings subpages for consistency and avoid code duplication. This may need some changes later though also

This handles Monet/Material You differently. This is a dynamic color scheme only available on Android, therefore to define it it uses expect/actual declarations. Other platforms do have their own concept of dynamic themes though, so in the commonMain API it  just uses the `DYNAMIC` terminology for better support for these later on and a more consistent cross platform API. This applies to both the theme and primary colors for it.

Since things are not XML based (and when we do use XML based stuff like strings in composeResources, there is still no `@string/` usage), this also adds new `PreferenceKeys` and `PreferenceDefaults` objects with constants for preferences. `PreferenceKeys` will replace all the string keys values in the current `donottranslate-strings.xml` and `PreferenceDefaults` will just include the default values for the preferences. Both are there to ensure consistency (not different defaults in different places) and to avoid accidental typos or something. For now it keeps what is added there only to what we actually use in `composeApp` as more are needed there these will be populated more as well.

For now, this also duplicates the default profile background images to `composeResources` so we can use them in `:composeApp` directly as well.

This also avoids refactoring to much of the app intentionally like removing/moving the companion object from the old SettingsFragment to avoid this PR growing massively. This adds a KMP version for device layout detection though as I needed to get if its a TV in `:composeApp`. 

Eventually we will have to split `:app` into `:androidApp` which just includes the signing stuff and entry points (AndroidManifest.xml, MainActivity (but a much slimmer version) and CloudStreamApp and other entry points like services (things defined in AndroidManifest).

AGP 9 prevents the ability to have both `com.android.application`  and `com.android.kotlin.multiplatform.library` at the same time, which means android entry points/APK generation can't be in a KMP module. `:app` will slowly be drained into fully KMP APIs into `:composeApp`. Stuff in there should not be stuff extensions will typically need though, otherwise it should go in `:library` instead.

Also, it is not immediately necessarily, but eventually we should also add the `composeResources` directory to weblate. This also sets up the locales python script to support that directory if and/or when we add it.


Happy to make any changes necessary though.

@fire-light42 fire-light42 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please try to add @Preview annotations to components and screens. To make compose accessible people need to see the layouts.

@Luna712

Luna712 commented May 25, 2026

Copy link
Copy Markdown
Contributor Author

Please try to add @Preview annotations to components and screens. To make compose accessible people need to see the layouts.

That was a good idea and is now done. Thank you!

Comment thread composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/utils/DeviceInfo.kt Outdated
@kazemcodes

Copy link
Copy Markdown

Thanks for your efforts, just a question Google material contains the vector images, is it not possible to use them instead of using pure vector designin in code?

@Luna712

Luna712 commented May 31, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for your efforts, just a question Google material contains the vector images, is it not possible to use them instead of using pure vector designin in code?

Google material extended that includes the actual icons is deprecated and no longer maintained, and increases the app size by about 20MB as it loads everything. We don't need a library with thousands of icons we will never use.

@kazemcodes

Copy link
Copy Markdown

​Based on my experience migrating several apps to Compose multiplatform, here is how to save yourself time and headaches:
​1. Wrap Your Core Components
​Always create wrapper functions for common Compose code.
The framework evolves quickly, and APIs can change. If you wrap your components in shared code, a future update means you only have to fix the code in one place, instead of rewriting it across the entire app.
​2. Use SQLDelight for Databases
​I highly recommend SQLDelight. It is efficient, reliable, and built perfectly for multiplatform development. (However, feel free to use whatever database fits your project best!)
​3. Choose Koin for Dependency Injection
​For DI, use Koin. It is lightweight, easy to set up, and works seamlessly with Kotlin Multiplatform to keep your architecture clean

@Luna712

Luna712 commented May 31, 2026

Copy link
Copy Markdown
Contributor Author

2 and 3 are planned. Koin is still under consideration. If it will end up helpful it will be used. SQLDelight I have already started writing a lib and migration code for SharedPreferences for it.

@kazemcodes

Copy link
Copy Markdown

Thanks for your efforts, just a question Google material contains the vector images, is it not possible to use them instead of using pure vector designin in code?

Google material extended that includes the actual icons is deprecated and no longer maintained, and increases the app size by about 20MB as it loads everything. We don't need a library with thousands of icons we will never use.

Thanks I didn't know that it increases the size that much but R8 will remove unnecessary items from code and resources

@fire-light42 fire-light42 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can make the layout system a bit more compose friendly.

By having a mutable state, we do not need to restart the entire activity.
Instead we simply have to call the update function and every "by isLayoutState" will cause a recomposition.

private val layoutState = mutableStateOf(layoutId)
fun isLayoutState(layoutFlags : Layout) = derivedStateOf { layoutState.value.and(layoutFlags) }
fun update() {
	layoutId = resolveLayout()
	layoutState.value = layoutId
}
val isTV by isLayoutState(TV)

This is easier to read, and reason about.

I also have some nitpicks that I want to clear up early.

  1. More things can be marked as @Immutable, this includes "Layout", "SettingsProfileState" and "SettingsVersionState"

  2. No need to specify the structuralEqualityPolicy, it is the default

  3. I would rather you use LocalContext.current and pass it as a variable than use globals

val version = buildVersionState(LocalContext.current)
val profile = buildProfileState(LocalContext.current)

Where the functions use context.currentCommitHash() and DataStoreHelper.getDefaultAccount(context)

This is because we want to avoid all requireContext/requireActivity, use inflater.context for ComposeView and LocalContext.current for @Composable.

@fire-light42

Copy link
Copy Markdown
Collaborator

Now after we have released a new stable, I think this should be merged as soon as possible!

@Luna712

Luna712 commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

I will work on what you mentioned later today and push in the next few days. I am a little confused about some of what you said though I will work on it and might ask here for some clarity in a bit if I can't figure out what you mean.

@Luna712

Luna712 commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

I pushed some of the fixes now. Will do the rest later.

Also just to note, I have tests written for compose (both UI and logic tests) but I will push that in a seperate PR. I also have detekt setup to ensure code standards while things are migrated that I will push in a seperate PR. It would ensure things like that bracket newline if-block thing you wanted in my other PRs and other standards. I am not sure what all you will want but I can push it later, and it can be reviewed. I mention that now because some of what I also just pushed was found from that.

@Luna712

Luna712 commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

@fire-light42 I believe I did everything you were wanting. Hopefully correctly. If not I apologize and can adjust.

@Luna712 Luna712 requested a review from fire-light42 July 12, 2026 21:14

@LagradOst LagradOst left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using this PR I started porting QN to compose. And I found out that

  1. Missing AmoledLight, Amoled is not the same as AmoledLight
    <style name="AmoledModeLight" parent="AmoledMode">
  2. MaterialTheme typography? I think we use google_sans.xml for all fonts

@Luna712

Luna712 commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Using this PR I started porting QN to compose. And I found out that

  1. Missing AmoledLight, Amoled is not the same as AmoledLight
    <style name="AmoledModeLight" parent="AmoledMode">
  2. MaterialTheme typography? I think we use google_sans.xml for all fonts

@LagradOst

  1. Whoops nice catch. That was originally intentional while I first set it up before splitting that but I forgot to finish it.
  2. I will take a look again. I was aiming to use material stuff mostly to ensure more consistent cross platform but I will look into that.

@LagradOst

Copy link
Copy Markdown
Contributor

Using this PR I started porting QN to compose. And I found out that

  1. Missing AmoledLight, Amoled is not the same as AmoledLight
    <style name="AmoledModeLight" parent="AmoledMode">
  2. MaterialTheme typography? I think we use google_sans.xml for all fonts

@LagradOst

1. Whoops nice catch. That was originally intentional while I first set it up before splitting that but I forgot to finish it.

2. I will take a look again. I was aiming to use material stuff mostly to ensure more consistent cross platform but I will look into that.

Please also include comments directly on the colors, as it makes it a lot easier to convert.

    /** primaryBlackBackground */
    var background by mutableStateOf(background)
    /** primaryGrayBackground */
    var surfaceVariant by mutableStateOf(surfaceVariant)
    /** iconGrayBackground */
    var surface by mutableStateOf(surface)
    /** boxItemBackground */
    var surfaceContainer by mutableStateOf(surfaceContainer)
    /** textColor */
    var onBackground by mutableStateOf(onBackground)
    /** grayTextColor */
    var onSurfaceVariant by mutableStateOf(onSurfaceVariant)
    /** iconColor */
    var icon by mutableStateOf(icon)
    /** colorPrimary */
    var primary by mutableStateOf(primary)
    /** colorOngoing */
    var ongoing by mutableStateOf(ongoing)

@LagradOst

Copy link
Copy Markdown
Contributor

Also, is there a good reason to use https://fonts.google.com/icons imagevectors directly? I found painterResource(R.drawable....) to be very easy to use without bloating the code directory. It should be the same as painterResource(Res.drawable....) on KMP.

@Luna712

Luna712 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Also, is there a good reason to use https://fonts.google.com/icons imagevectors directly? I found painterResource(R.drawable....) to be very easy to use without bloating the code directory. It should be the same as painterResource(Res.drawable....) on KMP.

This was a personal preference, because it was easier for me to build and maintain it in pure Kotlin where possible, faster build by reducing the stuff in composeResources a bit, and while mostly negligible reduced the app size just a tiny bit. I did first use drawables before I realized fonts.google.com gave the Kotlin version also. Not 100% against changing if you really want though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants