TLDR
どういう現象が発生し得るか?
- Composable関数の入力値にJavaで書かれたクラスがある場合、スマートコンポジションが効かなくなってしまう場合があるようです。
つまり、__入力値が前回と今回で全く同じであったとしても、毎回再コンポジションが発生してしまうということ__です。
回避するには
クラスに@Stable or @Immutableアノテーションを付与しましょう。
- 公開プロパティが全て不変である場合は、@Immutalbeを付与しましょう。
- 公開プロパティの変更をComposable関数に通知したい場合(再コンポジションを発生させたい場合)は、@Stableを付与しましょう。
ただし、闇雲に上記アノテーションを付与すると、再コンポジションの仕組みがうまく行かない場合があるため、アノテーションを付与できる細かい条件は下記公式ドキュメントを参照して下さい!!
https://developer.android.com/jetpack/compose/lifecycle?hl=ja#skipping
詳細
前提
-
Jetpack Composeでは、Composable関数の入力値(関数の引数やインスタンス変数)が前回実行時と同じであれば、再コンポジションがスキップされるとても便利な仕組みが提供されています。__これをスマートコンポジション__という。
( 前回値との比較には、クラスのequalsメソッドが使われる。 ) -
ただし、スマートコンポジションには__「全ての入力が安定した型である場合にのみ、スマートコンポジションが有効になる」__という制約があります。
-
「安定した型」とみなすことができる条件は、公式ドキュメントでは下記となっています。
・ 2つのインスタンスの equals の結果が、同じ 2 つのインスタンスについて常に同じになる。
・ 型の公開プロパティが変化すると、Composition に通知される。
・ すべての公開プロパティの型も安定している。
-
公式ドキュメントによると、__「上記制約に合致する場合のみ、Composable関数に入る「前回入力値」と「今回入力値」をequalsメソッド比較する。」__と説明しているように解釈できます。
-
逆に言うと、上記制約に合致していると言い切れない場合は、equalsメソッド使った比較が行われず、たとえ、「前回入力値」と「今回入力値」が同じであったとしても再コンポジションが発生してしまうということです。
-
これを回避するためにComposeでは@Stableと@Immutableというアノテーションを提供しています。
このアノテーションをクラスに付与することで、Composable関数に「安定した型である」と強制することができるようになります。
本題
Kotlinで書かれたクラスの場合は、わざわざ@Stable or @Immutableアノテーションを付与しなくても、そのクラスが「安定した型」である場合は、スマートコンポジションを実行してくれるようです。(インターフェースの場合は別)
ですが、
Javaで書かれたクラスの場合は、明示的に@Stable or @Immutableを付与しないと、スマートコンポジションが有効化されないようです。
以下例
MainActivity
const val TAG = "sample"
class MainActivity : ComponentActivity() {
private val sampleJava = SampleJava()
private val sampleKotlin = SampleKotlin()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var mutableValue by remember { mutableStateOf(0) }
TextButton(onClick = { mutableValue++ }) { Text(mutableValue.toString()) }
SampleComposable(mutableValue, sampleKotlin, sampleJava)
SideEffect {
Log.d(TAG, "root composable called.")
}
}
}
}
@Composable
fun SampleComposable(
mutableValue: Int,
sampleKotlin: SampleKotlin,
sampleJava: SampleJava
) {
Text(mutableValue.toString())
SampleK(sampleKotlin = sampleKotlin)
SampleJ(sampleJava = sampleJava)
SideEffect {
Log.d(TAG, "SampleComposable called.")
}
}
@Composable
fun SampleK(sampleKotlin: SampleKotlin) {
Text(sampleKotlin.sampleInt.toString())
SideEffect {
Log.d(TAG, "Sample3K Composable called.")
}
}
@Composable
fun SampleJ(sampleJava: SampleJava) {
Text(sampleJava.sampleInt.toString())
SideEffect {
Log.d(TAG, "Sample3J Composable called.")
}
}
Samplekotlin
class SampleKotlin {
val sampleInt: Long = 0
override fun equals(other: Any?): Boolean {
Log.d(TAG,"SampleKotlin equals() called.")
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val that = other as SampleJava
return sampleInt == that.sampleInt
}
override fun hashCode(): Int {
Log.d(TAG,"SampleKotlin hashCode() called.")
return Objects.hash(sampleInt)
}
}
SampleJava
public class SampleJava {
private long sampleInt = 0;
public long getSampleInt() {
return sampleInt;
}
@Override
public boolean equals(Object o) {
Log.d("sample", "SampleJava equals() called.");
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SampleJava that = (SampleJava) o;
return sampleInt == that.sampleInt;
}
@Override
public int hashCode() {
Log.d("sample", "SampleJava hashCode() called.");
return Objects.hash(sampleInt);
}
}
このコードを実行して、mutableValueの値を変更すること以下のようなログが吐き出されます。
D/sample: SampleKotlin equals() called.
D/sample: SampleJ Composable called.
D/sample: SampleComposable called.
D/sample: root composable called.
ご覧の通り、SampleKotlinクラスのequalsメソッドは実行されていますが、SampleJavaクラスのequalsは実行されていません。
そのため、SampleJavaクラスのインスタンスが仮に全く同じであったとしても、問答無用で再コンポジションが実行されることになります。
これを回避するため、Javaクラスに@Immutableアノテーションを付与してあげると・・・・
@Immutable //<-これを追加!
public class SampleJava {
private long sampleInt = 0;
public long getSampleInt() {
return sampleInt;
}
@Override
public boolean equals(Object o) {
Log.d("sample", "SampleJava equals() called.");
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SampleJava that = (SampleJava) o;
return sampleInt == that.sampleInt;
}
@Override
public int hashCode() {
Log.d("sample", "SampleJava hashCode() called.");
return Objects.hash(sampleInt);
}
}
以下のようなログになります。
D/sample: SampleKotlin equals() called.
D/sample: SampleJava equals() called.
D/sample: SampleComposable called.
D/sample: root composable called.
ちゃんとSampleJavaクラスのequalsも実行されて、SampleJコンポーザブル関数の再実行も抑制されてます!