Composing palettes
Tags: android, jetpack, jetpackcompose, kotlin
Published: 2021-09-16 10:12:32

In this article I will show you a barely known Jetpack library, *Palette* It can extract significant colors from an `android.graphics.Bitmap`. We will use the data created by *Palette* in a Jetpack Compose app. Here's how the app looks like right after the start:

![The app PaletteDemo right after the start](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7usbhv30dtmgziy99lkn.png)

After clicking the FAB the user can select an image. Then, the app looks like this:

![PaletteDemo showing the selected image and significant colors](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/333g6kslkwxw0vxnd6fc.png)

Not too bad, right? Let's dive into the source code. You can find it on [GitHub](https://github.com/tkuenneth/android-demos/tree/master/misc/PaletteDemo).

### Loading a bitmap and getting a palette

To use Jetpack Palette, we need to add it to the implementation dependencies.

```groovy
implementation 'androidx.palette:palette-ktx:1.0.0'
```

Next, let's look at the activity.

```kotlin
class PaletteDemoActivity : ComponentActivity() {

  private lateinit var viewModel: PaletteDemoViewModel

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    viewModel = ViewModelProvider(this).get(PaletteDemoViewModel::class.java)
    setContent {
      PaletteDemoTheme {
        Surface(color = MaterialTheme.colors.background) {
          PaletteDemo(
            onClick = { showGallery() }
          )
        }
      }
    }
  }
  …
```

To get a nice architecture, we use `ViewModel`. You will see shortly where `viewModel` is used in the activity. But let's look at `PaletteViewModel` first.

```kotlin
class PaletteDemoViewModel : ViewModel() {

  private val _bitmap: MutableLiveData<Bitmap> =
    MutableLiveData<Bitmap>()

  val bitmap: LiveData<Bitmap>
    get() = _bitmap

  fun setBitmap(bitmap: Bitmap) {
    _bitmap.value = bitmap
  }

  private val _palette: MutableLiveData<Palette> =
    MutableLiveData<Palette>()

  val palette: LiveData<Palette>
    get() = _palette

  fun setPalette(palette: Palette) {
    _palette.value = palette
  }
}
```

So, we have two properties, a bitmap and a palette. Both will be set from inside the activity and consumed inside composable functions. `PaletteDemo()` is the root of my composable hierarchy. It receives a lambda expression that invokes `showGallery()`. Here's what this function does:

```kotlin
private fun showGallery() {
  val intent = Intent(Intent.ACTION_PICK)
  intent.type = "image/*"
  val mimeTypes =
    arrayOf("image/jpeg", "image/png")
  intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
  startActivityForResult(intent, REQUEST_GALLERY)
}
```

Please note that we should replace `startActivityForResult()` because it is deprecated in `ComponentActivity`, but let's save this for a future article. 😎 Next comes the interesting part. What happens when the user has picked an image?

```kotlin
override fun onActivityResult(requestCode: Int, 
                              resultCode: Int, 
                              intent: Intent?) {
  super.onActivityResult(requestCode, resultCode, intent)
  when (requestCode) {
    REQUEST_GALLERY -> {
      if (resultCode == RESULT_OK) {
        intent?.let {
          it.data?.let { uri ->
            val source = ImageDecoder.createSource(
              contentResolver,
              uri
            )
            val bitmap = ImageDecoder.decodeBitmap(source).asShared()
            viewModel.setBitmap(bitmap)
            lifecycleScope.launch {
              viewModel.setPalette(
                Palette.Builder(bitmap).generate()
              )
            }
          }
        }
      }
    }
  }
}
```

To get a bitmap, we first create a source using `ImageDecoder.createSource()`. The source is then passed to `ImageDecoder.decodeBitmap()`. Have you spotted `asShared()`? Inside Jetpack Palette, `getPixels()` is invoked. This method may fail with `IllegalStateException: unable to getPixels(), pixel access is not supported on Config#HARDWARE bitmaps`. `asShared()` prevents this. The docs [say](https://developer.android.com/reference/android/graphics/Bitmap#asShared()):

> Return an immutable bitmap backed by shared memory which
> can be efficiently passed between processes via Parcelable.
>
> If this bitmap already meets these criteria it will return
> itself.

The method was introduced with API level 31, so to support older platforms you should replace it by something similar.

Back to the code. How do we obtain significant colors from the bitmap? First we create a `Palette.Builder` instance, passing the bitmap. Then we invoke `generate()` on this object. Besides this synchronous version, there is also a variant based on `AsyncTask`. As you can see, I chose to use a coroutine instead.

Now, let's turn to the composable function `PaletteDemo()`.

### Composing the palette

Most Compose apps will have a `Scaffold()` as its root, which may include a `TopAppBar()` and, like my example, a `FloatingActionButton()`. My content area consists of a vertically scrollable `Column()`, which remains empty until an image has been selected. Then it contains an `Image()` and several `Box()` elements which represent the significant colors.

```kotlin
@Composable
fun PaletteDemo(
  viewModel: PaletteDemoViewModel = viewModel(),
  onClick: () -> Unit
) {
  val bitmap = viewModel.bitmap.observeAsState()
  val palette = viewModel.palette.observeAsState()
  Scaffold(topBar = {
    TopAppBar(title = { Text(stringResource(id =
                                 R.string.app_name)) })
  },
    floatingActionButton = {
      FloatingActionButton(onClick = onClick) {
        Icon(
          Icons.Default.Search,
          contentDescription =
              stringResource(id = R.string.select)
        )
      }
    }
  ) {
    Column(
      modifier = Modifier
        .verticalScroll(rememberScrollState())
        .padding(16.dp),
      horizontalAlignment = Alignment.CenterHorizontally
    ) {
      bitmap.value?.run {
        Image(
          bitmap = asImageBitmap(),
          contentDescription = null,
          alignment = Alignment.Center
        )
      }
      palette.value?.run {
        swatches.forEach {
          Box(
            modifier = Modifier
              .padding(top = 8.dp)
              .fillMaxWidth()
              .height(32.dp)
              .clip(RectangleShape)
              .background(Color(it.rgb))
          )
        }
      }
    }
  }
}
```

To get informed about changes, we need to invoke `observeAsState()` on both `bitmap` and `palette`. Have you noticed `asImageBitmap()`? It converts the bitmap to `ImageBitmap`.

There a quite a few methods you can invoke on `Palette` instances, for example `getVibrantColor()` or `getDarkVibrantColor()`. My code just iterates over a list of [swatches](https://developer.android.com/reference/androidx/palette/graphics/Palette.Swatch). Please refer to the documentation for details.

### Conclusion

Using Jetpack Palette inside Composable apps is easy and fun. It will be interesting to see if the library receives updates in the wake of Material You. I hope you liked this post. Please share your thoughts in the comments.