JetpackCompose チュートリアル6 Jetpack Compose の状態

1つのComposableで2つの状態を表現できる。
UI上でそれぞれ別の部品なので更新のあった値(waterCount, juiceCount)に関わるComposableだけが再Composeされる。

@Composable
fun StatefulCounter() {
    var waterCount by remember { mutableStateOf(0) }

    var juiceCount by remember { mutableStateOf(0) }

    StatelessCounter(waterCount, { waterCount++ })
    StatelessCounter(juiceCount, { juiceCount++ })
}

また、

ステートフルでコンポーズ可能な関数は、複数のコンポーズ可能な関数に同じ状態を提供できます。

とあるように、1つの状態を複数のComposableに反映することももちろんできる。

@Composable
fun StatefulCounter() {
   var count by remember { mutableStateOf(0) }

   StatelessCounter(count, { count++ })
   AnotherStatelessMethod(count, { count *= 2 })
}

設計パターンとしてステートレスなComposableとステートフルなComposable(状態だけ持つ)を組み合わせることは多々ある

@Composable
fun WellnessTaskItem(
    taskName: String,
    checked: Boolean,
    onCheckedChange: (Boolean) -> Unit,
    onClose: () -> Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 16.dp),
            text = taskName
        )
        Checkbox(
            checked = checked,
            onCheckedChange = onCheckedChange
        )
        IconButton(onClick = onClose) {
            Icon(Icons.Filled.Close, contentDescription = "Close")
        }
    }
}

@Composable
fun WellnessTaskItem(taskName: String, modifier: Modifier = Modifier) {
    var checkedState by remember { mutableStateOf(false) }

    WellnessTaskItem(
        taskName = taskName,
        checked = checkedState,
        onCheckedChange = { newValue -> checkedState = newValue },
        onClose = {},
        modifier = modifier
    )
}

JetpackCompose チュートリアル5

更新滞りがちですがちまちま公式のCodeLab進めてます。

背景色の指定

Jetpack Compose のテーマ設定より

要素の色を設定する際は、適切なコンテンツ色である CompositionLocal 値が設定されるため、Surface を使用することをおすすめします。Modifier.background を直接呼び出すと適切なコンテンツ色が設定されないため、注意が必要です。

ColumnやRowなどのレイアウト用のコンポーネントのmodifierにModifier.background()でMaterialTheme.colors.backgroundなどを設定しても、その内部のコンテンツはonBackgroundなどにはなりません。その場合はSurface()で色を指定し、その内部にColumnやRowなどを配置することで、ColumnやRowの内部コンテンツにもテーマカラーを適用できます。

これ思いっきりModifier側でやってたので意識して修正して行かねば。

-Row(Modifier.background(MaterialTheme.colors.primary)) {
+Surface(color = MaterialTheme.colors.primary) {
単純な値変化のアニメーション
val backgroundColor = if (tabPage == TabPage.Home) Purple100 else Green300

で変化させていたのをanimateColorAsStateを使って指定するだけ
実際の適用は各要素で。

val backgroundColor by animateColorAsState(if (tabPage == TabPage.Home) Purple100 else Green300)
表示(ボタンの拡大・縮小)のアニメーション

単純に表示を切り替える場合はifの中がextendedによって表示・非表示

if (extended) {
    Text(
        text = stringResource(R.string.edit),
        modifier = Modifier
            .padding(start = 8.dp, top = 3.dp)
    )
}

アニメーションさせる場合

AnimatedVisibility(extended) {
    Text(
        text = stringResource(R.string.edit),
        modifier = Modifier
            .padding(start = 8.dp, top = 3.dp)
    )
}
アイテムの幅に合わせてアニメーション(表示・非表示)

enterに表示契機、exitに非表示契機を指定する。
各OffsetでfullHeight(縦の表示非表示なので)を指定する。
animationSpecを指定することで様々なアニメーションの指定が可能になるが後日見てみる。

AnimatedVisibility(
    visible = shown,
    enter = slideInVertically(
        // Enters by sliding down from offset -fullHeight to 0.
        initialOffsetY = { fullHeight -> -fullHeight }
    ),
    exit = slideOutVertically(
        // Exits by sliding up from offset 0 to -fullHeight.
        targetOffsetY = { fullHeight -> -fullHeight }
    )
) {
    Surface(
        modifier = Modifier.fillMaxWidth(),
        color = MaterialTheme.colors.secondary,
        elevation = 4.dp
    ) {
        Text(
            text = stringResource(R.string.edit_message),
            modifier = Modifier.padding(16.dp)
        )
    }
}


参考:Jetpack Compose で要素をアニメーション化するのCodeLab

JetpackCompose チュートリアル4

リストをアニメーションする

状態変数を定義(expanded)
状態による変化幅を定義(extraPadding)。ここでは高さを変える
アニメーションさせたい部分で上記で定義した変化幅を指定する

    var expanded by remember { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp
    )
…省略
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
既存styleから少しだけ変更したものを使いたい場合

copyを使って変更点だけ書き換える

Text(
    text = name,
    style = MaterialTheme.typography.headlineMedium.copy(
        fontWeight = FontWeight.ExtraBold
    )
)
アイコンボタン

開閉ボタンの例。
imageVectorにアイコンを指定する。
Iconsに開閉のアイコンも用意されている。
build.gradle(app)に以下を追加する必要がある。
implementation 'androidx.compose.material:material-icons-extended'

IconButton(
    onClick = { expanded = !expanded }
) {
    Icon(
        imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
            contentDescription = if (expanded) {
                stringResource(id = R.string.show_less)
            } else {
                stringResource(id = R.string.show_more)
            }
    )
}

参考:
Jetpack Compose: IconButtonコンポーザブルを使う | TechBooster

JetpackCompose チュートリアル3

状態の保持

状態はrememberで保持できるが画面回転やプロセス終了で状態破棄されるので保持したい場合はrememberSaveableを使う。
MyApp内での分岐にshouldShowOnboardingを使いたいので定義自体はMyApp内に保持してOnboardingScreenにメソッドを渡す→ボタンのonClickで更新を実装。

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }
}

JetpackCompose チュートリアル2

要素の中央寄せ

中身を中央寄せしたい親要素で以下を指定

verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
Textに背景をつける

Surfaceで囲ってSurfaceにcolorを指定する。
Surafceのようなマテリアルコンポーネントはその上に乗るText等の色を適切なものに変えてくれるらしい。

Surface(color = MaterialTheme.colorScheme.primary) {
    Text (text = "Hello $name!")
}
Composeの状態の保持

JetpackCodeLab参照

State と MutableState は、なんらかの値を保持し、その値が変化するたびに UI の更新(再コンポジション)をトリガーするインターフェースです。
しかし、コンポーザブル内の変数に mutableStateOf を割り当てることはできません。前述のとおり、再コンポジションが随時発生する可能性があります。その結果、コンポーザブルが再度呼び出されて変更可能な状態がリセットされ、値が新たに false になることが考えられます。
コンポジションの前後で状態を保持するには、remember を使用して可変状態を「記憶」します。

@Composable
private fun Greeting(name: String) {
    val expanded = remember { mutableStateOf(false) }

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

次回はJetpackCodeLabの8. 状態ホイスティングから

Jetpack Compose チュートリアル1

理解出来てなかった部分のメモ

materialとmaterial3では色指定方法が若干違う

material→MaterialTheme.colors.secondary
material3→MaterialTheme.colorScheme.secondary

ダークテーマの確認
Composeの前に以下でプレビューにも表示される
@Preview(
    uiMode = Configuration.UI_MODE_NIGHT_YES,
    showBackground = true,
    name = "Dark Mode"
)
リスト

LazyColumnに必要な情報のリストを渡して表示
ここでは表示するMessageCardの中に各要素を表示するTextを置いている
SampleDataはMessageのリスト

data class Message(val author: String, val body: String)

@Composable
fun MessageCard(msg: Message) {
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(id = R.drawable.profile_picture),
            contentDescription = "Contact profile picture",
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
                .border(1.5.dp, MaterialTheme.colorScheme.secondary, CircleShape)
        )

        Spacer(modifier = Modifier.width(8.dp))

        Column {
            Text(
                text = msg.author,
                color = MaterialTheme.colorScheme.secondary,
                style = MaterialTheme.typography.titleSmall
            )
            Spacer(modifier = Modifier.height(4.dp))
            Text(text = msg.body)
        }
    }
}

@Composable
fun Conversation(messages: List<Message>) {
    LazyColumn {
        items(messages) { message ->
            MessageCard(msg = message)
        }
    }
}

@Preview
@Composable
fun PreviewConversation() {
    ComposeTutorialTheme() {
        Conversation(messages = SampleData.conversationSample)
    }
}
Composeの状態

remember, mutableStateOf等を使って状態を監視、変更できる

var isExpanded by remember { mutableStateOf(false) }
Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
            Text(
                text = msg.author,
                color = MaterialTheme.colors.secondaryVariant,
                style = MaterialTheme.typography.subtitle2
            )
...省略
)

Handlerについて

Androidまじめに使えるようにしていこうということでメモがてら記録に残していく。

 

Handlerクラスはスレッド間でメッセージを受け渡すための仕組み

UIを管理するメインスレッドでは、内部的にLooper(メッセージキュー:別スレッドからのメッセージを管理して順に処理する)を持っている。

HandlerはLooperに対してメッセージ(以下の参考でいうRunnableオブジェクト)を送信するためのオブジェクト。

Handlerは別スレッドでnewすると上手く動かない。

public class MainActivity extends Activity {
private TextView txt;
  private Handler handler = new Handler();
  // 省略
  Thread t = new Thread(new Runnable() {
    @Override
    publid void run() {
      try {
        // Http通信処理省略
       final String body = EntitiyUtils.toString(res.getEntity(), "UTF-8");
       handler.post(new Runnable() {
    @Override
      public void run() {
           txt.setText(body)
       })
       } else //以下省略

SendMessage

メッセージの種類に応じて処理を分けたい場合はこっちのが良いかも

Messageのwhat, arg1, arg2, objあたりで処理を分岐する。

参考:Message  |  Android Developers

    handler = object: Handler(Looper.getMainLooper()) {
      override fun handleMessage(msg: Message) {
          if (msg.what == 1) {
              contentBinding.result.text = msg.obj.toString()
          }
      }
  }

  val msg = Message.obtain(handler, 1, "message 1") // 本来は通信結果をここに設定
  handler.sendMessage(msg)