♦️

Jetpack Composeで、Flash Cardを作ってみた

2024/08/17に公開

横スワイプスするとスライドできる

Flutterで、フラッシュカードのUIを作ってみたが、動き「ぬるー」として、好きになれなかった....

同じAndroidとはいえ、オリジナルのKotlinで作るとどうなるのか試してみたら、動きがすごく綺麗だった...
やはり、ネイティブは違うな😅

ネイティブなアプリとは何ですか?
ネイティブアプリという用語は、デバイスにダウンロードしてインストールできるアプリを意味します。 ネイティブモバイルアプリは、モバイルデバイス専用に開発されています。 ネイティブアプリ、ネイティブモバイルアプリ、およびモバイルアプリという用語は、多くの場合、同じ種類のソフトウェアを指すために相互互換的に使用されます。

私の知り合いたちは、ネイティブと聞くと、上の表現の人もいれば、Swift, Kotlinで作ったアプリのことを言ってたりしますね。

思考覚悟して作ったサンプル。SwiftUIより難しいようだ...

package com.junichi.flashcardcompose

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import com.junichi.flashcardcompose.ui.theme.FlashCardComposeTheme
import kotlin.math.abs
import kotlin.random.Random

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            FlashCardComposeTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    FlashcardApp()
                }
            }
        }
    }
}

data class Flashcard(val title: String, val content: String)

@Composable
fun FlashcardApp() {
    val flashcards = listOf(
        Flashcard("コーヒーショップの場所", "Where is the coffee shop after exiting the station?"),
        Flashcard("レストランの予約", "I'd like to make a reservation for dinner."),
        Flashcard("電車の乗り方", "How do I take the train to the city center?"),
        Flashcard("観光スポットの推薦", "Can you recommend some popular tourist attractions?"),
        Flashcard("ホテルのチェックイン時間", "What time is check-in at the hotel?")
    )

    var currentIndex by remember { mutableStateOf(0) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    var backgroundColor by remember { mutableStateOf(randomColor()) }

    val rotation by animateFloatAsState(
        targetValue = offset.x / 50,
        animationSpec = spring(stiffness = Spring.StiffnessLow),
        label = "card rotation"
    )

    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Card(
            modifier = Modifier
                .size(300.dp, 200.dp)
                .offset(offset.x.dp, offset.y.dp)
                .rotate(rotation)
                .pointerInput(Unit) {
                    detectDragGestures(
                        onDrag = { _, dragAmount ->
                            offset += dragAmount
                        },
                        onDragEnd = {
                            if (abs(offset.x) > 100) {
                                if (offset.x > 0) {
                                    currentIndex = (currentIndex - 1 + flashcards.size) % flashcards.size
                                } else {
                                    currentIndex = (currentIndex + 1) % flashcards.size
                                }
                                backgroundColor = randomColor()
                            }
                            offset = Offset.Zero
                        }
                    )
                },
            colors = CardDefaults.cardColors(containerColor = backgroundColor),
            elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
        ) {
            Column(
                modifier = Modifier
                    .padding(16.dp)
                    .fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(
                    text = flashcards[currentIndex].title,
                    style = MaterialTheme.typography.titleLarge,
                    color = Color.White

                )
                Spacer(modifier = Modifier.height(16.dp))
                Text(
                    text = flashcards[currentIndex].content,
                    style = MaterialTheme.typography.bodyLarge,
                    color = Color.White
                )
            }
        }
    }
}

fun randomColor(): Color {
    return Color(
        red = Random.nextFloat(),
        green = Random.nextFloat(),
        blue = Random.nextFloat(),
        alpha = 1f
    )
}

私の感想ですが、動きが綺麗なアニメーションがあるフラッシュカードのUIが作れました。

https://youtube.com/shorts/fv3F7y9V9eU

最後に

Flutterが本業ですが、最近は仕事で、Reactをやったりもしてます。副業や個人開発では、SwiftUI, Jetpack Composeを使っております。
綺麗なUIの動きを見ると、ネイティブの魅力に最近取り憑かれてしまいました。Flutterには、できないアプリを作ってみたいと思いました。

Discussion