Jetpack Compose 1.7+ でクリップボードコピーをどう書く?

 

🧑🏻‍💻 LocalClipboard と suspend 関数の組み合わせ

Compose 1.7 以降では、従来の ClipboardManager が非推奨になり、代わりに LocalClipboard + 非同期コピー が公式に推奨されています。

以下はシンプルなサンプルです。rememberCoroutineScope を使い、クリックイベントで非同期コピーを行っています。


val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()

Box(modifier = Modifier.clickable {
    scope.launch {
        val clipData = ClipData.newPlainText(uuid, uuid)
        clipboard.setClipEntry(clipData.toClipEntry())
    }
})

👉 Jason Ernst: Android ClipboardManager Deprecated: How to fix

 

🧑🏻‍💻 ViewModel 側でコピー処理をまとめる

世界的に著名な Android 開発者 Chris Banes や Jake Wharton のサンプルコードでは、UI 層から直接 Clipboard を操作せず、ViewModel に処理をまとめる パターンが多く見られます。

このアプローチを取ることで、UI の再コンポーズと Clipboard 操作が分離でき、よりテストしやすい設計になります。


class NoteViewModel : ViewModel() {
  fun copy(block: suspend () -> Unit) {
    viewModelScope.launch {
      block()
    }
  }
}

UI 側では以下のように呼び出せます:


IconButton(
  onClick = { 
    viewModel.copy {
      clipboard.setText(item.text)
    }
  }
) {
  Icon(Icons.Default.ContentCopy, contentDescription = null)
}

Clipboard 拡張関数を定義しておくと便利です。


suspend fun Clipboard.setText(text: String) { 
    val clipData = ClipData.newPlainText(text, text).toClipEntry() 
    setClipEntry(clipData)
}

 

🧑🏻‍💻 まとめ

ClipboardManager は非推奨 → LocalClipboard + suspend が公式推奨。

UI 層はイベントを投げるだけ、コピー処理は ViewModel で完結。

Coroutine scope を ViewModel 内で扱うことで UI の再コンポーズに影響しない。

ViewModel が clipboard を直接握るのは避けたほうがベター。
(非 UI 層に UI 依存を持ち込むことになるため)

拡張関数で共通処理化すれば再利用性が高まる。

つまり、

「UI はシンプルに」「コピー処理は ViewModel に集約」

これが現代的な Compose + Clipboard のベストプラクティスです。


【Jetpack Compose】回転するアニメーションの作り方

これ、おもしろい!!




👉 回転おじさん - Facebook

ちょっと画像をお借りして、

やってみましたが、、、


@Composable
fun Humans() {
  var currentRotation by remember { mutableFloatStateOf(0f) }
  val rotation = remember { Animatable(currentRotation) }

  LaunchedEffect(Unit) {
    rotation.animateTo(
      targetValue = currentRotation + 360f,
      animationSpec = infiniteRepeatable(
        animation = tween(3000, easing = LinearEasing),
        repeatMode = RepeatMode.Restart
      )
    ) {
      currentRotation = rotation.value
    }
  }
  Image(
    modifier = Modifier
      .fillMaxSize()
      .rotate(rotation.value)
      .aspectRatio(1.0f),
    painter = painterResource(R.drawable.humans),
    contentDescription = null
  )
}

ダメでした。。。

なんで、おじさんが走ってくれないのでしょうかー。


Jetpack Compose で Activity を取得する

なんとなくまだ古くさい感じはありますが。


val context = LocalContext.current


fun Context.getActivityOrNull(): Activity? {
    var context = this
    while (context is ContextWrapper) {
        if (context is Activity) return context
        context = context.baseContext
    }    
    return null
}


fun Context.findActivity(): Activity {
    var context = this
    while (context is ContextWrapper) {
        if (context is Activity) return context
        context = context.baseContext
    }
    throw IllegalStateException("Permissions should be called in the context of an Activity")
}

👉 android - How to get activity in compose - Stack Overflow


【Jetpack Compose】メールアプリを最も簡単に開く方法

指定したURLでWEBブラウザを開くならこう書けます。


@Composable
fun OpenLinkButton(
  urlHandler: UriHandler = LocalUriHandler.current
) {
  Button(
    onClick = {
      urlHandler.openUri("https://android.benigumo.com/")
    }
  ) {
    Text("Open Web link")
  }
}

では、

指定した宛先メールアドレスでメールアプリを開く場合。


@Composable
fun OpenMailAppButton(
  urlHandler: UriHandler = LocalUriHandler.current
) {
  Button(
    onClick = {
      urlHandler.openUri("mailto:[email protected]")
    }
  ) {
    Text("Open Mail App")
  }
}

旧来の Intent は Jetpack Compose では使わないことが多くなってきました!

👉 顔文字 (かおもじ) パック - Google Play のアプリ


Jetpack Compose 「Modifierは子1つ目だけに適用する」の考え方

実際のコード例で考えてみる。


Column {
  Text(
    text = "Hello",
    modifier = Modifier.padding(16.dp)
  )
  Text(
    text = "World",
    modifier = Modifier.padding(16.dp)
  )
}


val modifier = Modifier.padding(16.dp)

Column {
  Text(text = "Hello", modifier = modifier)
  Text(text = "World", modifier = modifier)
}


Column(
  modifier = Modifier.padding(16.dp)
) {
  Text(text = "Hello")
  Text(text = "World")
}


@Composable
fun ParentLayout() {
  Column(
    modifier = Modifier.verticalScroll(rememberScrollState())
  ) {
    for (i in 1..10) {
      ListItem(
        text = "Item $i",
        modifier = Modifier.padding(8.dp)
      )
    }
  }
}

@Composable
fun ListItem(modifier: Modifier, text: String) {
  Text(
    text = text,
    modifier = modifier.background(Color.LightGray)
  )
}

共有することの意味は「統一性」だろうけども、

見通しが悪くなるので「1つ目まで」としているのだろう。

「まずは個別につけてから共通部分をホイストしていく。」

「親から渡すときは子まで影響。孫は個別に。」

そんな考え方の順番が簡単で自然だと思います。