Kira Resari
Kira Resari

Reputation: 2440

Kotlin Multiplatform ~ Add images on Android and Desktop

I have a problem that I thought would be very simple, but somehow I managed to spend all day searching for a solution and am none the wiser.

Basically, I have a Kotlin Multiplatform app using Compose, which is supposed to work for both Android and Desktop, and so far it does. However, now I want to add a simple title image, and I haven't been able to figure out how to do that.

What I want is to have one image file somewhere - anywhere - in my project structure and have that file be used for both Android and Desktop. Furthermore, I want the image to be loaded in the common UI module, which I think is one of the things that makes this complicated.

As far as I can tell, in Compose, the general way to add an image is like this:

    val image: Painter = painterResource(id = R.drawable.composelogo)
    Image(painter = image,contentDescription = "")

The problem here is that the R seems to be specific to the Android module, while I want to load the image in the common-ui module. For reference, here's my rough project structure:

📁myProject
├📁androidApp
│└📁src
│ └📁main
│  └📁[...]
│   └📄MainActivity.kt <- Uses MainView
├📁desktop
│└📁src
│ └📁jvmMain
│  └📁[...]
│   └📄Main.kt <- Uses MainView
├📁shared
└📁shared-ui
 └📁src
  └📁commonMain
   └📁[...]
    └📄MainView.kt <- Where I want to implement the image display

I've tried out a number of things, including https://github.com/icerockdev/moko-resources , but I couldn't get anything I tried to work. Among the many exciting errors I came across there was one where it suddenly complained that it could not find an AndroidManifest.xml file in the shared-ui folder for whatever reason.

It feels like I am doing something fundamentally wrong here. It shouldn't be that difficult to add a simple image to Android and Desktop at the same time, right?

Can someone please help me out here?

For reference, here's the project where I'm trying to get this working. It also includes a protocol of all the things I already tried: https://github.com/KiraResari/ceal-chronicler

Upvotes: 5

Views: 7212

Answers (3)

akelix
akelix

Reputation: 410

Now we have native solution from jetbrains https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-images-resources.html

Upvotes: 1

Evgeny K
Evgeny K

Reputation: 3167

You can share Image component between Desktop and Android using actual / expect approach

create in your commonMain expect-function that returns Painter

@Composable
internal expect fun getCealChroniclerLogo(): Painter 

and use it later

Image(
    painter = getCealChroniclerLogo(),
    contentDescription = "Ceal Chronicler Logo"
)

Add actual declaration in androidMain:

@Composable
internal actual fun getCealChroniclerLogo() = painterResource(R.drawable.ceal_chronicler_logo)

Don't forget to copy image to drawable Android resource

and desktop:

@Composable
internal actual fun getCealChroniclerLogo() = painterResource("MR/images/CealChroniclerLogo.png")

The only downside that you have to copy image in Android resources. But if you think about it is not an issue, it is great, because probably in android app you will be using different image resolutions and ratio

I created pull-request to your repo with example

Upvotes: 1

Kira Resari
Kira Resari

Reputation: 2440

Someone from the moko-resources team helped me implement this now. In order to help anyone who also has this issue, I then wrote the following short how-to guide about what needs to be done to make this work:

  • You need the dependency moko-resources for that (https://github.com/icerockdev/moko-resources)

    • Note that it might not work with the latest version of the android plugin
      • In my case, I needed to set the version of the android plugin to 7.2.2
  • Apart from the android plugin version, all the changes need to happen in the shared-ui module

  • In the shared-ui/build.gradle.kts add:

    • kotlin {
          [...]
          sourceSets {
              val commonMain by getting {
                  dependencies {
                      [...]
                      api("dev.icerock.moko:resources:0.20.1")
                      api("dev.icerock.moko:resources-compose:0.20.1")
                  }
              }
              [...]
          }
      }
      
  • Add the following files:

    • shared-ui/src/commonMain/kotlin/com/domain/project/ResourcesExt.kt

    • package com.domain.project
      
      import androidx.compose.ui.graphics.painter.Painter
      import dev.icerock.moko.resources.ImageResource
      
      expect fun painterResource(imageResource: ImageResource): Painter
      
    • shared-ui/src/androidMain/kotlin/com/domain/project/ResourcesExt.kt

    • package com.domain.project
      
      import androidx.compose.runtime.Composable
      import androidx.compose.ui.graphics.painter.Painter
      import dev.icerock.moko.resources.ImageResource
      
      @Composable
      actual fun painterResource(imageResource: ImageResource): Painter {
          return androidx.compose.ui.res.painterResource(id = imageResource.drawableResId)
      }
      
    • shared-ui/src/desktopMain/kotlin/com/domain/project/ResourcesExt.kt

    • package com.domain.project
      
      import androidx.compose.runtime.Composable
      import androidx.compose.ui.graphics.painter.Painter
      import androidx.compose.ui.graphics.toPainter
      import dev.icerock.moko.resources.ImageResource
      
      @Composable
      actual fun painterResource(imageResource: ImageResource): Painter {
          return imageResource.image.toPainter()
      }
      
  • The image needs to go under shared-ui/src/commonMain/resources/MR/images/[email protected]

    • I'm not sure what the @1x is there for, but it is definitely required, because otherwise there'll be an error when starting the app
  • In the composable method where you want to add the image, add:

    • ...these dependencies:

      • import androidx.compose.foundation.Image
        import com.domain.project.MR
        import com.domain.project.painterResource
        
    • ...this where you want to display the image:

      •         Image(
                    painter = painterResource(MR.images.MyImage),
                    contentDescription = "Some Description"
                )
        

Upvotes: 3

Related Questions