たくさんの自由帳

Jetpack Composeを既存アプリへ導入

投稿日 : | 0 日前

文字数(だいたい) : 18020

どうもこんばんわ。ニコ生でCLANNAD一挙見てます。
本編で流れてるBGMすきすき

前提

  • Kotlin Android Extensionsは使えない模様。ViewBindingへ乗り換えて。

環境

なまえあたい
Android StudioAndroid Studio Arctic Fox \ 2020.3.1 Canary 3
minSdkVersion21

遭遇した問題 2021/01/03 現在

  • Android 5でリソースが見つからないエラーでクラッシュ 今までの方法でDrawableを取得してBitmapへ変換してComposeで扱えるBitmapへ変換すれば取れる。
val icon = AmbientContext.current.getDrawable(R.drawable.android)?.toBitmap()?.asImageBitmap()
if (icon != null) {
    Icon(
        modifier = Modifier.padding(5.dp),
        bitmap = icon
    )
}
  • ScrollableRowAndroidViewがずれる Beta03で修正されました。

    • Android 7以前で観測
    • 直し方はわからんBeta03に上げればおk(Kotlinは1.4.31)
  • そもそもAndroid StudioでJDKのパスが間違ってるとか言われてビルドまで進めない

    • .ideaを消してみる(か適当に名前を変える)と直りました。

既存アプリへ導入

割と頻繁にアップデートが入るので、こっちも見て

AGPのアップデート

必要かどうかはわかりませんが、たしかサンプルコードいじってるときもAGPのバージョンがなんとかみたいな感じでアップデートした記憶があるので更新しておきましょう。
家のWi-Fi遅くてつらい

多分右下にアップデートする?みたいな通知が出てると思うのでそこから

Imgur

Kotlinのアップデート

appフォルダじゃない方のbuild.gradleを開いて、書き換えていきます。

buildscript {
    ext.kotlin_version = '1.4.21'
    repositories {
        google()
        jcenter()
        
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:7.0.0-alpha03'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

ext.kotlin_version1.4.21へ変えます。
classpath 'com.android.tools.build:gradle:7.0.0-alpha03'はバージョンを上げるとそうなると思う。

app/build.gradle

appフォルダの方のbuild.gradleを開いて、必要なライブラリを書いていきます。
以下は一例です。

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

// apply plugin: 'kotlin-android-extensions' さよなら

apply plugin: 'kotlin-kapt' // Room使うときのなにか。一番上に

android {

    compileSdkVersion 30
    defaultConfig {
        applicationId "io.github.takusan23.tatimidroid"
        minSdkVersion 22
        targetSdkVersion 30
        versionCode 78
        versionName "12.2.1"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    // Kotlinを書くきっかけになった Kotlin Android Extensions がいよいよ非推奨になってしまった
    // のでViewBindingに乗り換える
    buildFeatures {
        // ViewBinding有効
        viewBinding true
        // Jetpack Compose有効
        compose true
    }

    compileOptions {
        targetCompatibility 1.8
        sourceCompatibility 1.8
    }

    kotlinOptions {
        jvmTarget = '1.8'
        useIR = true
    }

    composeOptions {
        kotlinCompilerVersion kotlin_version // 1.4.21が入ると思う
        kotlinCompilerExtensionVersion '1.0.0-alpha09'
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    // Jetpack Compose --- ここから

    implementation 'androidx.compose.ui:ui:1.0.0-alpha09'
    // Tooling support (Previews, etc.)
    implementation 'androidx.compose.ui:ui-tooling:1.0.0-alpha09'
    // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)
    implementation 'androidx.compose.foundation:foundation:1.0.0-alpha09'
    // Material Design
    implementation 'androidx.compose.material:material:1.0.0-alpha09'
    // Material design icons
    implementation 'androidx.compose.material:material-icons-core:1.0.0-alpha09'
    implementation 'androidx.compose.material:material-icons-extended:1.0.0-alpha09'
    // Integration with observables
    implementation 'androidx.compose.runtime:runtime-livedata:1.0.0-alpha09'

    // Jetpack Compose --- ここまで

}

buildFeaturesとかkotlinOptionscomposeOptions
dependenciesに書き足す感じですかね

できたらSyncしてBuildします。

Jetpack Compose を試す

適当にクラスを作って書いていきます。

/**
 * Jetpack Compose 略してJC
 * */

@Composable
fun VideoInfoCard() {
    Card(modifier = Modifier.padding(10.dp)) {
        Text(text = "Hello World")
    }
}

@Preview
@Composable
fun PreviewVideoInfoCard() {
    VideoInfoCard()
}

@Previewをつけることで隣のプレビューに表示されるようになります。@Previewを付けた関数には引数を設定してはいけません。

Activityに置く

setContentView()の代わりにsetContent { }を置いてその中にさっき書いたVideoInfoCard()を書きましょう

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            VideoInfoCard()
        }
    }
}

他にも@Previewのついた関数のところで実機プレビューが使えますが、
私の環境ではうまく動かなかったのでActivityに置いたほうがいいと思います。

Modifierについて

よく登場するModifier、多分すべてのComposeで共通するheightとかwidthとかpaddingとかを設定するときに使う。
xmlでレイアウト組んでたときも共通で使えた属性あったから多分そんなやつ

連結して設定していきます

Image(
    imageVector = Icons.Outlined.Book,
    modifier = Modifier.height(50.dp).width(50.dp),
)

レシピ集

Jetpack Composeが流行ると願って
手を動かそう

カウンターアプリを作る

var count by remember { mutableStateOf(0) }を使うことでカウント値を保持できます。
var count = 0は動きませんでした。


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TestCompose()
        }
    }
}

@Composable
fun TestCompose() {
    // 押した回数を保持する
    var count by remember { mutableStateOf(0) }

    Column(
        modifier = Modifier.padding(10.dp), // スペース確保
        horizontalAlignment = Alignment.CenterHorizontally, // 真ん中にする
    ) {
        Text(
            text = "押した回数 $count",
        )
        Button(
            onClick = {
                // おしたとき
                count += 1
            },
            colors = ButtonDefaults.textButtonColors(backgroundColor = Color.White)
        ) {
            Text(text = "ここをおせ!")
        }
    }
}

@Preview
@Composable
fun TestComposePreview() {
    TestCompose()
}

実行結果

Imgur

好きなUIにクリックイベントを置きたい(Button { }以外で押せるようにしたい)

Modifierにクリックするやつがあります。Ripple(波みたいなやつ)もできます。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ClickableCompose()
        }
    }
}

@Composable
fun ClickableCompose() {
    // 押した回数を保持する
    var count by remember { mutableStateOf(0) }
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .width(150.dp)
            .height(100.dp)
            .clickable(
                onClick = { count++ },
                indication = rememberRipple(color = Color.Blue),
                interactionSource = remember { MutableInteractionSource() },
            )
    ) {
        Text(text = "おせますよ~ $count")
        Image(imageVector = Icons.Outlined.Add)
    }
}

実行結果

Imgur

アイコンを表示 + 押せるようにする

今まで、マテリアルアイコンを使う際はAsset Studioからアイコンを持ってくると思うんですが、

Imgur

なんと!Jetpack Composeを使うことでコード一行でアイコンを用意できます。Outlined以外も用意できます。

Icon(imageVector = Icons.Outlined.Android)

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TestCompose()
        }
    }
}

@Composable
fun TestCompose() {
    // 押した回数を保持する
    var count by remember { mutableStateOf(0) }

    Row(
        modifier = Modifier.padding(10.dp), // スペース確保
        verticalAlignment = Alignment.CenterVertically, // まんなかに
    ) {
        Text(
            text = "押した回数 $count",
        )
        IconButton(onClick = {
            // カウントアップ
            count++
        }) {
            Icon(imageVector = Icons.Outlined.Home)
        }
    }
}

@Preview
@Composable
fun TestComposePreview() {
    TestCompose()
}

実行結果

Imgur

動画説明文見え隠れするやつ


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            VideoInfo()
        }
    }
}

@Composable
fun VideoInfo() {
    // 表示するか
    var isShow by remember { mutableStateOf(false) }
    Card(
        modifier = Modifier.padding(10.dp)
    ) {
        Column(modifier = Modifier.padding(10.dp)) {
            Row {
                Text(text = "動画タイトル", modifier = Modifier.weight(1f)) // アイコンまでずっと伸ばす
                IconButton(onClick = {
                    isShow = !isShow
                }) {
                    // アイコンも変更
                    Icon(imageVector = if (isShow) Icons.Outlined.ExpandLess else Icons.Outlined.ExpandMore)
                }
            }

            // 表示するか
            if (isShow) {
                Text(
                    text = """
                    ニコ動にもサムネ登録機能ついてから最後一瞬だけサムネ用の画像を表示させるやつも見なくなりましたね。
                    """.trimIndent()
                )
            }

        }
    }
}

実行結果

Imgur

リスト表示

RecyclerViewみたいに画面外は表示しないLazyColumnってのがありますのでこれを使っていきます

まずRecyclerViewのAdapterみたいに一つだけのやつを用意します。

@Composable
fun BlogCard(blogTitle: String) {
    // 押した回数を保持する
    var count by remember { mutableStateOf(0) }

    Card(
        modifier = Modifier.padding(10.dp),
        elevation = 10.dp
    ) {
        Column(
            modifier = Modifier.padding(10.dp), // スペース確保
            verticalArrangement = Arrangement.Center, // まんなかに
        ) {
            Row(
                verticalAlignment = Alignment.CenterVertically
            ) {
                Image(
                    imageVector = Icons.Outlined.Book,
                    modifier = Modifier.height(50.dp).width(50.dp),
                )
                Text(
                    text = blogTitle,
                    modifier = Modifier.weight(1f)
                )
            }
            Divider(modifier = Modifier.padding(0.dp, 10.dp, 0.dp, 0.dp)) // 区切り線
            IconButton(onClick = {
                // カウントアップ
                count++
            }) {
                Row(
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Icon(imageVector = Icons.Outlined.FavoriteBorder)
                    Text(text = "$count")
                }
            }
        }
    }
}

そしたら一覧表示を作って完成


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BlogCardList()
        }
    }
}

@Composable
fun BlogCardList() {
    val blogList = listOf(
        "Jetpack Compose 略して?",
        "MotionLayoutでミニプレイヤー",
        "2020年おすすめボカロ",
        "ボカロバラードいいよね",
    )
    Surface(Modifier.fillMaxSize()) {
        LazyColumn(content = {
            items(blogList) {
                BlogCard(it)
            }
        })
    }
}

@Preview
@Composable
fun TestComposePreview() {
    BlogCardList()
}

こんな感じになると思う。RecyclerViewより一覧表示がかんたんで嬉しい。
(Vue.jsのv-forみたいで使いやすい)

Imgur

Fragment に設置する

とりあえず一つにFragmentからJetpack Compose使ってみようかなって。そんな方に

今(2020/12/30現在)だけ?

Fragmentを作るにはFragmentのバージョンを上げる必要があります。あげないと、prepareCallとか言う謎のメソッドを書かないといけなくなります(正体不明)
appフォルダにあるbuild.gradleを開いてdependenciesに書き足します

dependencies {
    // Fragmentを作ろうとすると謎のメソッドをオーバーライドさせようとするので
    implementation 'androidx.fragment:fragment-ktx:1.3.0-rc01'
}

そしたらBlogListFragment.ktを作成してコピペ

class BlogListFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return ComposeView(requireContext()).apply {
            setContent {
                MaterialTheme {
                    Scaffold(
                        topBar = {
                            TopAppBar() {
                                Column(
                                    horizontalAlignment = Alignment.CenterHorizontally,
                                    verticalArrangement = Arrangement.Center,
                                    modifier = Modifier.fillMaxHeight().padding(10.dp),
                                ) {
                                    Text(text = "ブログ一覧")
                                }
                            }
                        }
                    ) {
                        BlogCardList()
                    }
                }
            }
        }
    }
}

BlogCardList()どこから出てきたんだって話ですが、MainActivity.ktに書いてあるのをそのまま使いました。
本来はComposeのUI用クラスを用意すべきです。

あとBlogCardList()関数?メソッド?はどこのクラスにも所属していない(class { } の中に書いてない)
トップレベル関数ってやつなので、どこでもかけます?。注意点ですが名前がかぶるとエラーになります。

後はActivityに置きましょう。今までのXMLな感じで
サンプルコードではxml使わず(R.layout.activity_main)、動的にFrameLayoutを置いてますが別にXMLでFrameLayout置いてid設定してもいいです。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // レイアウト作るのめんどいので動的に作る。別にXMLでレイアウト置いてもいい
        val fragmentHostLayout = FrameLayout(this)
        fragmentHostLayout.id = View.generateViewId()
        setContentView(fragmentHostLayout)

        supportFragmentManager.beginTransaction().replace(fragmentHostLayout.id, BlogListFragment()).commit()

    }
}

ちなみにNavigationは使ったことがないです。argument渡すのが楽になるとかなんとか

真ん中に表示させたい

一番実用性がありそう

Row(横並び編)

注意点としてはverticalAlignmenthorizontalArrangementで指定している値のスペルが若干似てるってことですかね。

なまえあたい
verticalAlignmentAlignment.CenterVertically
horizontalArrangementArrangement.Center

Column(縦並び編)

Rowとは逆になります。

なまえあたい
verticalArrangementArrangement.Center
horizontalAlignmentAlignment.CenterHorizontally

以下サンプルコード


override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        CardCenterText()
    }
}

@Composable
fun CardCenterText() {
    Column {
        Card(
            modifier = Modifier.padding(10.dp).fillMaxWidth().height(100.dp),
            backgroundColor = Color.Cyan,
        ) {
            Row(
                horizontalArrangement = Arrangement.Center,
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Text(text = "まんなかに居座る Row")
                Icon(imageVector = Icons.Outlined.Android)
            }
        }
        Card(
            modifier = Modifier.padding(10.dp).fillMaxWidth().height(100.dp),
            backgroundColor = Color.Cyan,
        ) {
            Column(
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                Text(text = "まんなかに居座る Column")
                Icon(imageVector = Icons.Outlined.Android)
            }
        }
    }
}

こんな感じ

Imgur

遭遇したエラー

R.jar: プロセスはファイルにアクセスできません。別のプロセスが使用中です。

@Previewから実行しようとすると失敗する。ので、ActivityとかFragmentに置いていつもどおり実行したほうがいい。てかプレビューより早くない?

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            VideoInfo()
        }
    }
}

どうしても@Previewから実行したければ

Make Projectした後に実行させるとうまくいく?

Imgur

Type 'TypeVariable(T)' has no method 'getValue(Nothing?, KProperty<*>)' and thus it cannot serve as a delegate

by remember { } じゃないもう一つの方法

// プロパティデリゲート とか言う書き方
var selectTabIndex_ by remember { mutableStateOf(0) }
// アクセス
selectTabIndex_ = 0

// 割り当て演算子
var selectTabIndex = remember { mutableStateOf(0) }
selectTabIndex.value = 0

上も下も同じことができるので、だめな場合は下の方を試してみてください。アクセスの際にvalueを付ける必要がありますが

else

なんかコピペし直したら治った。一応使ってるimport書いておきますね

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Book
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

Caused by: java.lang.ClassNotFoundException: Composable Method ~

@Previewのついた関数を消してそのまま実行すると出る。切り替えてあげよう

Imgur

java.lang.NoSuchMethodError: No static method ~

関数有るのに無いとか言ってくるやつ。

ツールバーのBuildからClean Projectを実行した後に再度実行すると直るんじゃないかな

Imgur

なんか真っ赤になった。Importしてもなんか別なのがImportされるんだけど?

Sync Projectしたらなんかアップデートしませんか?(Android Gradle Pluginだと思われ)って聞かれたのでアプデしたらなんか治った。難しいね

Imgur

ソースコード

Android Studio Arctic Fox | 2020.3.1 Canary 3で動作確認済です。
とりあえずブログ一覧を表示させた状態の画面が開くようになってるはずです。

https://github.com/takusan23/BlogListCompose

終わりに

  • CPUとかメモリとかのリソースくっっっそ持っていくなこいつ。メモリも16GBあるのに無くなりそう()
  • あとプレビューまだ不十分感。なんかずれたりするし。
    • Activityに置いて普通に実行したほうが(私は)はやかった
  • アイコンがすでに用意されているのは嬉しい。取り込む作業がなくなった
    • ただAndroid StudioのDrawable表示機能が便利だったのでちょっとつらい

あとmarginってないのかな。見つけられなかったんだけど