20210912のAndroidに関する記事は5件です。

Activity からの戻り値を取得する方法

環境 Android Studio で、Empty Compose Activity を使用して空のプロジェクトを作成しています。Empty Compose Activity は、Android Studio のバージョンである Arctic Fox をインストールすることで表れます。 実装 Activity の状態が STARTED になる前に、registerForActivityResult を使って ActivityResultLauncher を作成します。今回は、Activity からの文字列をただ単に標準出力に出力することにします。 class MainActivity : ComponentActivity() { private val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == Activity.RESULT_OK) { println(it.data?.getIntExtra("PENGUIN", 0)) } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Button(onClick = ::move) { Text("click me!") } } } private fun move() { val intent = Intent(this, SubActivity::class.java) startForResult.launch(intent) } } SubActivity は次のように実装します。 class SubActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Button(onClick = ::move) { Text("click me!") } } } private fun move() { val intent = Intent().apply { putExtra("PENGUIN", 100) } setResult(Activity.RESULT_OK, intent) finish() } } まとめ registerForActivityResult を呼ぶタイミングに気をつけるだけで良さそうです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android 12では「おおよそ」の位置情報が選択できるようになるが、LocationManagerでは位置情報の更新を受け取れないらしい

Android 12からは以下のようにランタイムパーミッションのダイアログや、設定画面で「正確」な位置情報を許可せず、「おおよそ」の位置情報だけを許可するということができるようになるようです。 パーミッションダイアログ アプリ設定 「おおよそ」の位置情報が許可された状態、正確な位置情報がオフの状態というのは、ACCESS_FINE_LOCATIONは許可されず、ACCESS_COARSE_LOCATIONだけが許可された状態になっています。 Android 12まではACCESS_FINE_LOCATIONをリクエストしていた場合は、ACCESS_FINE_LOCATIONを許可するかどうかしかユーザーは選択できず、ACCESS_FINE_LOCATIONを許可されればACCESS_COARSE_LOCATIONは許可されるので、その動作を前提に割り切った実装をしていた場合は、ACCESS_COARSE_LOCATIONだけが許可された状態を考慮するように変更が必要ですね。 ACCESS_COARSE_LOCATIONではLocationManagerで位置情報の更新が受け取れない? さて、これだけなら「ユーザーの選択肢が増えた」で終わりなのですが、Android 4.2のころから、ACCESS_COARSE_LOCATIONだけが許可された状態ではLocationManager.requestLocationUpdatesのonLocationChangedがコールされないという問題があるらしいという噂を聞きました。 ということで検証してみます。 Log.e("XXXX", "ACCESS_COARSE_LOCATION: ${checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION)}") Log.e("XXXX", "ACCESS_FINE_LOCATION: ${checkPermission(Manifest.permission.ACCESS_FINE_LOCATION)}") val manager: LocationManager = getSystemService()!! manager.allProviders.forEach { provider -> Log.e("XXXX", "getLastKnownLocation: $provider ${manager.getLastKnownLocation(provider)}") manager.requestLocationUpdates( provider, 1, 1f, LocationListenerAdapter { Log.e("XXXX", "onLocationChanged: $provider $it") }) LocationManagerCompat.getCurrentLocation(manager, provider, null, ContextCompat.getMainExecutor(this), { Log.e("XXXX", "getCurrentLocation: $provider $it") }) } 各Providerに対して、getLastKnownLocation/requestLocationUpdates/getCurrentLocationをコールして結果を表示させています。 最初にパーミッションの状態をチェックしています。実装は以下です。 private fun checkPermission(permission: String): String = if (PermissionChecker.checkSelfPermission(this, permission) == PermissionChecker.PERMISSION_GRANTED ) "GRANTED" else "DENIED" 検証環境は Android 12 Beta 5 の Pixel5 です。 正確な位置情報が許可された状態 まずは、FINE_LOCATIONが許可された状態で見てみます。 ACCESS_COARSE_LOCATION: GRANTED ACCESS_FINE_LOCATION: GRANTED getLastKnownLocation: passive Location[fused 34.xxxxxx,135.xxxxxx hAcc=20.0 et=+1d22h2m57s39ms alt=37.860162605487275 vAcc=3.0] getLastKnownLocation: network Location[network 34.xxxxxx,135.xxxxxx hAcc=20.0 et=+1d22h2m41s72ms alt=38.30000305175781 vAcc=1.3333334 {Bundle[mParcelledData.dataSize=68]}] getLastKnownLocation: fused Location[fused 34.xxxxxx,135.xxxxxx hAcc=20.0 et=+1d22h2m57s39ms alt=37.860162605487275 vAcc=3.0] getLastKnownLocation: gps null onLocationChanged: passive Location[fused 34.xxxxxx,135.xxxxxx hAcc=20.0 et=+1d22h2m57s39ms alt=37.860162605487275 vAcc=3.0] getCurrentLocation: passive Location[fused 34xxxxxx,135.xxxxxx hAcc=20.0 et=+1d22h2m57s39ms alt=37.860162605487275 vAcc=3.0] getCurrentLocation: network Location[network 34.xxxxxx,135.xxxxxx hAcc=20.0 et=+1d22h2m41s72ms alt=38.30000305175781 vAcc=1.3333334 {Bundle[mParcelledData.dataSize=68]}] getCurrentLocation: fused Location[fused 34.xxxxxx,135.xxxxxx hAcc=20.0 et=+1d22h2m57s39ms alt=37.860162605487275 vAcc=3.0] onLocationChanged: fused Location[fused 34.xxxxxx,135.xxxxxx hAcc=20.0 et=+1d22h3m2s50ms alt=37.860162605487275 vAcc=3.0] onLocationChanged: network Location[network 34.xxxxxx,135.xxxxxx hAcc=12.191 et=+1d22h3m3s604ms alt=38.30000305175781 vAcc=1.3333334 {Bundle[mParcelledData.dataSize=68]}] getCurrentLocation: gps null GPS_PROVIDERがnullを返していますが、それぞれ何らかのPROVIDERが位置情報を返しています。 NETWORK_PROVIDERで位置情報がとれているのでACCESS_COARSE_LOCATIONの状態でも位置情報がとれそうです。 おおよその位置情報だけが許可された状態 続いて、ACCESS_COARSE_LOCATIONだけが許可された状態で実行してみます。 ACCESS_COARSE_LOCATION: GRANTED ACCESS_FINE_LOCATION: DENIED getLastKnownLocation: passive Location[fused 34.xxxxxx,135.xxxxxx hAcc=2000.0 et=+1d22h8m57s325ms] getLastKnownLocation: network Location[network 34.xxxxxx,135.xxxxxx hAcc=2000.0 et=+1d22h5m18s717ms] getLastKnownLocation: fused Location[fused 34.xxxxxx,135.xxxxxx hAcc=2000.0 et=+1d22h8m57s325ms] getLastKnownLocation: gps null getCurrentLocation: passive null getCurrentLocation: network null getCurrentLocation: fused null getCurrentLocation: gps null getLastKnownLocationでは位置情報がとれていますが、getCurrentLocationではすべてnull、requestLocationUpdatesではonLocationChangedがコールされません。 うーん、どういうことでしょう? FusedLocationProviderClientの場合 FusedLocationProviderClientが登場してからは、こちらが推奨されていることもあり、位置情報は使うが、LocationManagerは使っていないというアプリも多いかと思います。(GoogleAPIが使えない環境では使えませんが) FusedLocationProviderClientの場合、動作はどうなっているのでしょうか?確認してみます。 Log.e("XXXX", "ACCESS_COARSE_LOCATION: ${checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION)}") Log.e("XXXX", "ACCESS_FINE_LOCATION: ${checkPermission(Manifest.permission.ACCESS_FINE_LOCATION)}") val client = LocationServices.getFusedLocationProviderClient(this) client.requestLocationUpdates(LocationRequest.create(), object : LocationCallback() { override fun onLocationResult(location: LocationResult) { Log.e("XXXX", "onLocationResult: $location") } }, Looper.getMainLooper()) val task = client.getCurrentLocation(LocationRequest.PRIORITY_LOW_POWER, null) task.addOnSuccessListener { Log.e("XXXX", "onSuccess: $it") } 正確な位置情報が許可された状態 当然、FINE_LOCATIONが許可されていれば問題ありません。 ACCESS_COARSE_LOCATION: GRANTED ACCESS_FINE_LOCATION: GRANTED onLocationResult: LocationResult[locations: [Location[fused 34.xxxxxx,135.xxxxxx hAcc=12.552 et=+1d22h20m3s986ms alt=37.799487734693756 vAcc=3.0 {Bundle[mParcelledData.dataSize=52]}]]] onSuccess: Location[fused 34.xxxxxx,135.xxxxxx hAcc=12.552 et=+1d22h22m40s542ms alt=37.799487734693756 vAcc=3.0 {Bundle[mParcelledData.dataSize=52]}] おおよその位置情報だけが許可された状態 FusedLocationProviderClientであれば、COARSE_LOCATIONのみの場合も問題無く取得できます。 ACCESS_COARSE_LOCATION: GRANTED ACCESS_FINE_LOCATION: DENIED onLocationResult: LocationResult[locations: [Location[fused 34.738739,135.394036 hAcc=2000.0 et=+1d22h19m14s26ms]]] onSuccess: Location[fused 34.xxxxxx,135.xxxxxx hAcc=2000.0 et=+1d22h20m0s359ms] まとめ ACCESS_COARSE_LOCATIONだけが許可された状態ではLocationManagerでは位置情報の更新を受け取ることができず、getLastKnownLocationでしか位置情報をとることができません。 一方、FusedLocationProviderClientであれば問題無く位置情報の更新を受け取ることができるようです。 FusedLocationProviderClientを導入すれば問題はありません。GoogleAPIが使えない環境であれば、getLastKnownLocationをうまく利用する必要がありそうです。 ただ、onLocationChangedがコールされないのが仕様通りという訳ではないと思いますが、Android 4.2から動いていないという話が本当なら、なぜ未だに修正されないのか分かりません。謎です。 以上です
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jetpack ComposeのdoCompose()開始からアプリ側のComposable関数の呼び出しに至るまでのコードリーディング

https://qiita.com/takahirom/items/d2a89560f8ff2065a7c0 の続きのstateが書き換わってからの動きを追っています。 @Composable fun Content() { var state by remember { mutableStateOf(true) } LaunchedEffect(Unit) { delay(12000) state = false } if (state) { Node1() } Node2() } 自分用のコードリーディイングメモ記事で、もっとわかりやすいのを後で出すと思います。 以下runRecomposeAndApplyChanges()内処理のまとめ。 以下の中で3:の部分を読んでいきます。3の途中までになります。 1: recordComposerModificationsLocked() を呼ぶ。Recomposer.snapshotInvalidationsを見て、IdentityScopeMapを使って変更点に対しての影響を受けるスコープを取得し、Recomposer.compositionInvalidationsやComposition.invalidationsというフィールドに変更を入れる。 2: compositionInvalidationsをtoRecomposeに入れる。 3: toRecomposeに対して、performRecompose(composition, modifiedValues)、(内部でdoCompose())することでtoApplyを作成。SlotTableを見て、ここで木を見ていき、木への変更をrecordしていく。 4: recordされたtoApplyに対してapplyChanges()をすることで、SlotTableの書き換えが走る。 (予想。コード未読) ComposerImpl.doCompose() 始めからstartRoot()まで まずdoComposeを見ていく。 ComposerImpl.doCompose() // invalidationsRequestedとcontentの引数に取る // invalidationsRequestedはステップ1によって作られたCompositeImpl.invalidationsを使って作られている。 // contentはnullになっている。今回は差分を見ていっているので、これは最初のときにしか使わないと思われる。 private fun doCompose( invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>, content: (@Composable () -> Unit)? ) { runtimeCheck(!isComposing) { "Reentrant composition is not supported" } trace("Compose:recompose") { // 現在のsnapshotを取得してcomposerに保存する snapshot = currentSnapshot() // invalidationsRequestedをComposerImpl.invalidationsに入れていく。 invalidationsRequested.forEach { scope, set -> val location = scope.anchor?.location ?: return invalidations.add(Invalidation(scope, location, set)) } // locationでソートする // locationのコメント: The index of the group in the slot table being invalidated. invalidations.sortBy { it.location } nodeIndex = 0 var complete = false isComposing = true try { // **一旦ここまで読む** startRoot() // Ignore reads of derivedStatOf recalculations observeDerivedStateRecalculations( start = { childrenComposing++ }, done = { childrenComposing-- }, ) { if (content != null) { startGroup(invocationKey, invocation) invokeComposable(this, content) endGroup() } else { skipCurrentGroup() } } endRoot() complete = true } finally { isComposing = false invalidations.clear() providerUpdates.clear() if (!complete) abortRoot() } } } invalidationsRequestedをComposerImpl.invalidationsに入れていくが実際どのような情報がはいるか? Scopeには実際に呼び出すためのブロックが入っている。 locationは4 setにはArraySetでMutableStateが入っている。 startRoot() 始めからstartGroup()まで @OptIn(InternalComposeApi::class) private fun startRoot() { // slotTableのReaderを開く。つまり、SlotTableを読み込みを開始 // これをフィールドに入れる。 reader = slotTable.openReader() // startGroup startGroup(rootKey) // parent reference management parentContext.startComposing() parentProvider = parentContext.getCompositionLocalScope() providersInvalidStack.push(providersInvalid.asInt()) providersInvalid = changed(parentProvider) if (!collectParameterInformation) { collectParameterInformation = parentContext.collectingParameterInformation } resolveCompositionLocal(LocalInspectionTables, parentProvider)?.let { it.add(slotTable) parentContext.recordInspectionTable(it) } startGroup(parentContext.compoundHashKey) } startGroup() 始めからstartReaderGroup()まで ComposerImpl.startGroup() private fun startGroup(key: Int) = start(key, null, false, null) 長い関数来ました。。 ここではついにSlotTableとメモリ内の状況の比較が動き始めます inserting = falseなので、 if文の中は読まなくて良いです。 ComposerImpl.start() private fun start(key: Int, objectKey: Any?, isNode: Boolean, data: Any?) { ... // Check for the insert fast path. If we are already inserting (creating nodes) then // there is no need to track insert, deletes and moves with a pending changes object. if (inserting) { // ここはfalseなので。このif文の中は今回は読まない。 ... return } // **ここはtrueになる** if (pending == null) { val slotKey = reader.groupKey // ここもtrueになる // **ここで今のSlotTableの状況とstartRoot()で渡されてきたkeyが同じかを比較する!** if (slotKey == key && objectKey == reader.groupObjectKey) { // The group is the same as what was generated last time. // startReaderGroupが呼び出される。 startReaderGroup(isNode, data) } else { ... } } val pending = pending var newPending: Pending? = null if (pending != null) { // if文に入らないので、今回は省略 ... } enterGroup(isNode, newPending) } 今のSlotTableの状況はこちら。 reader.groupKeyは今100になっており、currentGroupは0になっている。 デバッグで情報を確かめると以下の通り。 reader.run { "SlotReader(current = $currentGroup end=$currentEnd size = $size)" } SlotReader(current = 0 end=16 size = 16) Group(0) key=100, nodes=2, size=16, slots=[0: {}] **← このグループ、つまりルートのグループを見るようになっている** Group(1) key=1000, nodes=2, size=15 Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider) Group(3) key=-985533309, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@1502095, androidx.compose.runtime.internal.ComposableLambdaImpl@14975aa] Group(4) key=-337788314, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@99d159b] Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=false)@89759032] Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=false)@89759032, Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>] Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.LaunchedEffectImpl@6eff07f] Group(9) key=-337788167, nodes=1, size=4 Group(10) key=1815931657, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@ba14276] Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1] Group(13) key=1815931930, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@32d7077] Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp Group(15) key=125, nodes=0, size=1 node=Node2(name=node2), slots=[22: node2] startReaderGroup() reader.startGroup()を呼ぶと、readerのcurrentGroupなどの位置が変更になる。 ComposerImpl private fun startReaderGroup(isNode: Boolean, data: Any?) { if (isNode) { // falseになる reader.startNode() } else { if (data != null && reader.groupAux !== data) { // falseになる recordSlotTableOperation { _, slots, _ -> slots.updateAux(data) } } reader.startGroup() // ここは呼ばれる。 } } fun startGroup() { if (emptyCount <= 0) { require(groups.parentAnchor(currentGroup) == parent) { "Invalid slot table detected" } parent = currentGroup currentEnd = currentGroup + groups.groupSize(currentGroup) val current = currentGroup++ currentSlot = groups.slotAnchor(current) currentSlotEnd = if (current >= groupsSize - 1) slotsSize else groups.dataAnchor(current + 1) } } reader.startGroup()が呼ばれたあとのreaderの状況は以下のように変化する SlotReader(current = 1 end=16 size = 16) Group(0) key=100, nodes=2, size=16, slots=[0: {}] Group(1) key=1000, nodes=2, size=15 **← このグループを見るようになっている** Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider) Group(3) key=-985533309, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@1502095, androidx.compose.runtime.internal.ComposableLambdaImpl@14975aa] Group(4) key=-337788314, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@99d159b] Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=false)@89759032] Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=false)@89759032, Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>] Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.LaunchedEffectImpl@6eff07f] Group(9) key=-337788167, nodes=1, size=4 Group(10) key=1815931657, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@ba14276] Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1] Group(13) key=1815931930, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@32d7077] Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp Group(15) key=125, nodes=0, size=1 node=Node2(name=node2), slots=[22: node2] ここで呼び出し元に戻る startGroup() startReaderGroup()の終わりからenterGroup()まで ```kotlin:ComposerImpl.start() private fun start(key: Int, objectKey: Any?, isNode: Boolean, data: Any?) { ... // Check for the insert fast path. If we are already inserting (creating nodes) then // there is no need to track insert, deletes and moves with a pending changes object. if (inserting) { // ここはfalseなので。このif文の中は今回は読まない。 ... return } // **ここはtrueになる** if (pending == null) { val slotKey = reader.groupKey // ここもtrueになる // **ここで今のSlotTableの状況とstartRoot()で渡されてきたkeyが同じかを比較する!** if (slotKey == key && objectKey == reader.groupObjectKey) { // The group is the same as what was generated last time. // startReaderGroupが呼び出される。 startReaderGroup(isNode, data) } else { ... } } val pending = pending var newPending: Pending? = null if (pending != null) { // if文に入らないので、今回は省略 ... } enterGroup(isNode, newPending) } enterGroup SlotReader側のrootが終わったという情報更新は終わっているのでそれをComposer側にも書き込む。 ComposerImpl private fun enterGroup(isNode: Boolean, newPending: Pending?) { // When entering a group all the information about the parent should be saved, to be // restored when end() is called, and all the tracking counters set to initial state for the // group. pendingStack.push(pending) this.pending = newPending this.nodeIndexStack.push(nodeIndex) if (isNode) nodeIndex = 0 this.groupNodeCountStack.push(groupNodeCount) groupNodeCount = 0 } ここで、startRoot()まで処理が戻る。 startRoot()、startGroup(rootKey)の終わりから最後まで。 @OptIn(InternalComposeApi::class) private fun startRoot() { // slotTableのReaderを開く。つまり、SlotTableを読み込みを開始 // これをフィールドに入れる。 reader = slotTable.openReader() // startGroup startGroup(rootKey) // parent reference management // あんまり今回はparent reference managementについては考えなくて良さそう。 // parentContext.startComposing()の中身は今回は空になっている。 parentContext.startComposing() // EmptyCompositionLocalMapを返してくるのみ parentProvider = parentContext.getCompositionLocalScope() providersInvalidStack.push(providersInvalid.asInt()) // 以下providersInvalidはfalseになる providersInvalid = changed(parentProvider) if (!collectParameterInformation) { collectParameterInformation = parentContext.collectingParameterInformation } // 以下も今回はあんまり考えなくて良さそう resolveCompositionLocal(LocalInspectionTables, parentProvider)?.let { it.add(slotTable) parentContext.recordInspectionTable(it) } startGroup(parentContext.compoundHashKey) } startGroupを呼ぶ。 このときの引数となるcompoundHashKeyはRecomposerCompoundHashKeyになっており、1000という固定値。 internal override val compoundHashKey: Int get() = RecomposerCompoundHashKey startGroupを呼び出すと先程の繰り返しを行う。 この結果、SlotReaderは以下の状態になる。currentが2になり、少し進む。 SlotReader(current = 2 end=16 size = 16) Group(0) key=100, nodes=2, size=16, slots=[0: {}] Group(1) key=1000, nodes=2, size=15 Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider) **← このグループを見るようになっている** Group(3) key=-985533309, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@1502095, androidx.compose.runtime.internal.ComposableLambdaImpl@14975aa] Group(4) key=-337788314, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@99d159b] Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=false)@89759032] Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=false)@89759032, Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>] Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.LaunchedEffectImpl@6eff07f] Group(9) key=-337788167, nodes=1, size=4 Group(10) key=1815931657, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@ba14276] Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1] Group(13) key=1815931930, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@32d7077] Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp Group(15) key=125, nodes=0, size=1 node=Node2(name=node2), slots=[22: node2] ComposerImpl.doCompose() startRoot()の終わりからobserveDerivedStateRecalculations()まで ComposerImpl.doCompose() // invalidationsRequestedとcontentの引数に取る // invalidationsRequestedはステップ1によって作られたCompositeImpl.invalidationsを使って作られている。 // contentはnullになっている。今回は差分を見ていっているので、これは最初のときにしか使わないと思われる。 private fun doCompose( invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>, content: (@Composable () -> Unit)? ) { runtimeCheck(!isComposing) { "Reentrant composition is not supported" } trace("Compose:recompose") { // 現在のsnapshotを取得してcomposerに保存する snapshot = currentSnapshot() // invalidationsRequestedをComposerImpl.invalidationsに入れていく。 invalidationsRequested.forEach { scope, set -> val location = scope.anchor?.location ?: return invalidations.add(Invalidation(scope, location, set)) } // locationでソートする // locationのコメント: The index of the group in the slot table being invalidated. invalidations.sortBy { it.location } nodeIndex = 0 var complete = false isComposing = true try { startRoot() // Ignore reads of derivedStatOf recalculations observeDerivedStateRecalculations( start = { childrenComposing++ }, done = { childrenComposing-- }, ) { if (content != null) { startGroup(invocationKey, invocation) invokeComposable(this, content) endGroup() } else { skipCurrentGroup() } } endRoot() complete = true } finally { isComposing = false invalidations.clear() providerUpdates.clear() if (!complete) abortRoot() } } } SnapshotState.kt observeDerivedStateRecalculations() internal fun <R> observeDerivedStateRecalculations( start: (derivedState: State<*>) -> Unit, done: (derivedState: State<*>) -> Unit, block: () -> R ) { val previous = derivedStateObservers.get() try { derivedStateObservers.set( (derivedStateObservers.get() ?: persistentListOf()).add( start to done ) ) block() } finally { derivedStateObservers.set(previous) } } 今回は特にderivedStateObserversにはまともなデータは入っていなそう。 ただ、このderivedStateObservers.set()を行ったあとにはstartとdoneが保存される。 そして終わったらpreviousとしてまた戻される。 ComposerImpl.doCompose() observeDerivedStateRecalculations()のブロック ComposerImpl.doCompose() // invalidationsRequestedとcontentの引数に取る // invalidationsRequestedはステップ1によって作られたCompositeImpl.invalidationsを使って作られている。 // contentはnullになっている。今回は差分を見ていっているので、これは最初のときにしか使わないと思われる。 private fun doCompose( invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>, content: (@Composable () -> Unit)? ) { runtimeCheck(!isComposing) { "Reentrant composition is not supported" } trace("Compose:recompose") { // 現在のsnapshotを取得してcomposerに保存する snapshot = currentSnapshot() // invalidationsRequestedをComposerImpl.invalidationsに入れていく。 invalidationsRequested.forEach { scope, set -> val location = scope.anchor?.location ?: return invalidations.add(Invalidation(scope, location, set)) } // locationでソートする // locationのコメント: The index of the group in the slot table being invalidated. invalidations.sortBy { it.location } nodeIndex = 0 var complete = false isComposing = true try { startRoot() // Ignore reads of derivedStatOf recalculations observeDerivedStateRecalculations( start = { childrenComposing++ }, done = { childrenComposing-- }, ) { if (content != null) { // ここでcontentがnullなので入らない!! startGroup(invocationKey, invocation) invokeComposable(this, content) endGroup() } else { // skipCurrentGroup()が呼ばれる skipCurrentGroup() } } endRoot() complete = true } finally { isComposing = false invalidations.clear() providerUpdates.clear() if (!complete) abortRoot() } } } skipCurrentGroup()からrecomposeToGroupEnd()まで ComposerImpl override fun skipCurrentGroup() { // ここはfalseになる // invalidationsには先程入れた変更場所情報などが入っている。 if (invalidations.isEmpty()) { skipGroup() } else { val reader = reader val key = reader.groupKey val dataKey = reader.groupObjectKey val aux = reader.groupAux // compoundKeyHashはsaved stateのために使うみたいなので、ここでは読まない。 updateCompoundKeyWhenWeEnterGroup(key, dataKey, aux) // 以下を呼ぶとreaderの現在の位置が変わる。 startReaderGroup(reader.isNode, null) recomposeToGroupEnd() reader.endGroup() updateCompoundKeyWhenWeExitGroup(key, dataKey, aux) } } SlotReader(current = 3 end=16 size = 16) Group(0) key=100, nodes=2, size=16, slots=[0: {}] Group(1) key=1000, nodes=2, size=15 Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider) **↓ このグループを見るようになっている** Group(3) key=-985533309, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@1502095, androidx.compose.runtime.internal.ComposableLambdaImpl@14975aa] Group(4) key=-337788314, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@99d159b] Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=false)@89759032] Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=false)@89759032, Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>] Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.LaunchedEffectImpl@6eff07f] Group(9) key=-337788167, nodes=1, size=4 Group(10) key=1815931657, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@ba14276] Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1] Group(13) key=1815931930, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@32d7077] Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp Group(15) key=125, nodes=0, size=1 node=Node2(name=node2), slots=[22: node2] recomposeToGroupEnd()、 始めからrecordUpsAndDowns()まで /** * Recompose any invalidate child groups of the current parent group. This should be called * after the group is started but on or before the first child group. It is intended to be * called instead of [skipReaderToGroupEnd] if any child groups are invalid. If no children * are invalid it will call [skipReaderToGroupEnd]. * 今のparent groupの無効になったchild groupをすべてrecomposeする。 * これはグループ開始後に呼ばれるべき。 * 無効になったchild groupがある場合にskipReaderToGroupEnd()の代わりに呼ばれることを意図している。 * もし無効になったchildがいなければ、skipReaderToGroupEnd()を呼び出す。 */ private fun recomposeToGroupEnd() { val wasComposing = isComposing isComposing = true var recomposed = false val parent = reader.parent val end = parent + reader.groupSize(parent) val recomposeIndex = nodeIndex val recomposeCompoundKey = compoundKeyHash val oldGroupNodeCount = groupNodeCount var oldGroup = parent // invalidationsは1つしかないので、自動的にその1つが選ばれる。 (これまで何度か出てきたinvalidations) var firstInRange = invalidations.firstInRange(reader.currentGroup, end) while (firstInRange != null) { val location = firstInRange.location // ここでinvalidationsは消される! invalidations.removeLocation(location) // 以下はtrueになる if (firstInRange.isInvalid()) { recomposed = true // reader.reposition(location)によってreaderの見ている位置が移動!! reader.reposition(location) // 移動した今の位置を取得 val newGroup = reader.currentGroup // Record the changes to the applier location recordUpsAndDowns(oldGroup, newGroup, parent) oldGroup = newGroup // Calculate the node index (the distance index in the node this groups nodes are // located in the parent node). nodeIndex = nodeIndexOf( location, newGroup, parent, recomposeIndex ) // Calculate the compound hash code (a semi-unique code for every group in the // composition used to restore saved state). compoundKeyHash = compoundKeyOf( reader.parent(newGroup), parent, recomposeCompoundKey ) firstInRange.scope.compose(this) // Restore the parent of the reader to the previous parent reader.restoreParent(parent) } else { // If the invalidation is not used restore the reads that were removed when the // the invalidation was recorded. This happens, for example, when on of a derived // state's dependencies changed but the derived state itself was not changed. invalidateStack.push(firstInRange.scope) firstInRange.scope.rereadTrackedInstances() invalidateStack.pop() } // Using slots.current here ensures composition always walks forward even if a component // before the current composition is invalidated when performing this composition. Any // such components will be considered invalid for the next composition. Skipping them // prevents potential infinite recomposes at the cost of potentially missing a compose // as well as simplifies the apply as it always modifies the slot table in a forward // direction. firstInRange = invalidations.firstInRange(reader.currentGroup, end) } if (recomposed) { recordUpsAndDowns(oldGroup, parent, parent) reader.skipToGroupEnd() val parentGroupNodes = updatedNodeCount(parent) nodeIndex = recomposeIndex + parentGroupNodes groupNodeCount = oldGroupNodeCount + parentGroupNodes } else { // No recompositions were requested in the range, skip it. skipReaderToGroupEnd() } compoundKeyHash = recomposeCompoundKey isComposing = wasComposing } reader.reposition(location)後 SlotReader(current = 4 end=16 size = 16) Group(0) key=100, nodes=2, size=16, slots=[0: {}] Group(1) key=1000, nodes=2, size=15 Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider) Group(3) key=-985533309, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@1502095, androidx.compose.runtime.internal.ComposableLambdaImpl@14975aa] **↓ このグループを見るようになっている** Group(4) key=-337788314, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@99d159b] Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=false)@89759032] Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=false)@89759032, Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>] Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.LaunchedEffectImpl@6eff07f] Group(9) key=-337788167, nodes=1, size=4 Group(10) key=1815931657, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@ba14276] Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1] Group(13) key=1815931930, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@32d7077] Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp Group(15) key=125, nodes=0, size=1 node=Node2(name=node2), slots=[22: node2] recordUpsAndDowns()とdoRecordDownsFor() (今回は何もしないので飛ばしてOK) ComposerImpl /** * Records the operations necessary to move the applier the node affected by the previous * group to the new group. */ private fun recordUpsAndDowns(oldGroup: Int, newGroup: Int, commonRoot: Int) { val reader = reader val nearestCommonRoot = reader.nearestCommonRootOf( oldGroup, newGroup, commonRoot ) // Record ups for the nodes between oldGroup and nearestCommonRoot var current = oldGroup while (current > 0 && current != nearestCommonRoot) { // このif文の中には入らない。 if (reader.isNode(current)) recordUp() current = reader.parent(current) } // Record downs from nearestCommonRoot to newGroup doRecordDownsFor(newGroup, nearestCommonRoot) } 以下で再帰処理をしていて、nearestCommonRoot = 2でgroup = 4なので、もう一度doRecordDownsFor()が呼ばれなおされ、次はnearestCommonRoot = 2でgroup = 3で呼ばれ、最後はnearestCommonRoot = 2でgroup = 2で呼ばれて、returnするが、全てisNode()がfalseになるのでなにもしない。 private fun doRecordDownsFor(group: Int, nearestCommonRoot: Int) { if (group > 0 && group != nearestCommonRoot) { doRecordDownsFor(reader.parent(group), nearestCommonRoot) if (reader.isNode(group)) recordDown(reader.nodeAt(group)) } recomposeToGroupEnd()、 recordUpsAndDowns()の終わりからfirstInRange.scope.compose(this)まで /** * Recompose any invalidate child groups of the current parent group. This should be called * after the group is started but on or before the first child group. It is intended to be * called instead of [skipReaderToGroupEnd] if any child groups are invalid. If no children * are invalid it will call [skipReaderToGroupEnd]. * 今のparent groupの無効になったchild groupをすべてrecomposeする。 * これはグループ開始後に呼ばれるべき。 * 無効になったchild groupがある場合にskipReaderToGroupEnd()の代わりに呼ばれることを意図している。 * もし無効になったchildがいなければ、skipReaderToGroupEnd()を呼び出す。 */ private fun recomposeToGroupEnd() { val wasComposing = isComposing isComposing = true var recomposed = false val parent = reader.parent val end = parent + reader.groupSize(parent) val recomposeIndex = nodeIndex val recomposeCompoundKey = compoundKeyHash val oldGroupNodeCount = groupNodeCount var oldGroup = parent // invalidationsは1つしかないので、自動的にその1つが選ばれる。 (これまで何度か出てきたinvalidations) var firstInRange = invalidations.firstInRange(reader.currentGroup, end) while (firstInRange != null) { val location = firstInRange.location // ここでinvalidationsは消される! invalidations.removeLocation(location) // 以下はtrueになる if (firstInRange.isInvalid()) { recomposed = true // reader.reposition(location)によってreaderの見ている位置が移動!! reader.reposition(location) // 移動した今の位置を取得 val newGroup = reader.currentGroup // Record the changes to the applier location recordUpsAndDowns(oldGroup, newGroup, parent) oldGroup = newGroup // Calculate the node index (the distance index in the node this groups nodes are // located in the parent node). // 親のノードの中に位置しているこのグループのノードたちの中での距離のindexを求める // これによってComposer側のnodeIndexが更新される。 nodeIndex = nodeIndexOf( location, newGroup, parent, recomposeIndex ) // Calculate the compound hash code (a semi-unique code for every group in the // composition used to restore saved state). // savedStateで使う値。 compoundKeyHash = compoundKeyOf( reader.parent(newGroup), parent, recomposeCompoundKey ) // ↓!!! firstInRange.scope.compose(this) // Restore the parent of the reader to the previous parent reader.restoreParent(parent) } else { // If the invalidation is not used restore the reads that were removed when the // the invalidation was recorded. This happens, for example, when on of a derived // state's dependencies changed but the derived state itself was not changed. invalidateStack.push(firstInRange.scope) firstInRange.scope.rereadTrackedInstances() invalidateStack.pop() } // Using slots.current here ensures composition always walks forward even if a component // before the current composition is invalidated when performing this composition. Any // such components will be considered invalid for the next composition. Skipping them // prevents potential infinite recomposes at the cost of potentially missing a compose // as well as simplifies the apply as it always modifies the slot table in a forward // direction. firstInRange = invalidations.firstInRange(reader.currentGroup, end) } if (recomposed) { recordUpsAndDowns(oldGroup, parent, parent) reader.skipToGroupEnd() val parentGroupNodes = updatedNodeCount(parent) nodeIndex = recomposeIndex + parentGroupNodes groupNodeCount = oldGroupNodeCount + parentGroupNodes } else { // No recompositions were requested in the range, skip it. skipReaderToGroupEnd() } compoundKeyHash = recomposeCompoundKey isComposing = wasComposing } nodeIndexOfの計算 そしてやっと、アプリ側のComposable関数までたどり着きました。 /** * Restart the scope's composition. It is an error if [block] was not updated. * scopeのcompositionをrestartする。もしblockが更新(代入)されていないとエラーになる。以下はそのエラーになるパターンの説明。 * The code * generated by the compiler ensures that when the recompose scope is used then [block] will * be set but it might occur if the compiler is out-of-date (or ahead of the runtime) or * incorrect direct calls to [Composer.startRestartGroup] and [Composer.endRestartGroup]. */ fun compose(composer: Composer) { block?.invoke(composer, 1) ?: error("Invalid restart scope") } ここからお楽しみのアプリ側のRecomposeです。 まとめ 今回は基本的にSlotTableのReaderがrootからcurrentGroupを変えて、アプリ側が強調しながら動いていることがなんとなくわかりました。また変更のところまでジャンプをしていき、最終的にアプリ側のblockを呼び出すところまで生きました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

var count by remember { mutableStateOf(0) } の意味

この記法の意味と、以下のような import 文を記述しなければならない理由がわからなかったので、これらの意味を調べていく。 import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue var ... by ... とは 委譲プロパティと呼ばれるもの。この構文を使うためには、Foo クラスに getValue(Nothing?, KProperty<*>): T と setValue(Nothing?, KProperty<*>, value: T) を実装する必要がある。 import kotlin.reflect.KProperty class Foo { operator fun getValue(a: Nothing?, b: KProperty<*>): Int { return 1 } operator fun setValue(a: Nothing?, b: KProperty<*>, value: Int) { } } fun main() { var foo by Foo() println(foo) //=> 1 foo = 2 println(foo) //=> 1 } foo は Int 型にも関わらず、式評価時と代入時には getValue() と setValue() が実行される。プロパティに似ている。 まとめると、次のような構造をしていることがわかる。 var foo/* 変数名 */ by Foo()/* getValueとsetValueを持つオブジェクト */ var count by remember { mutableStateOf(0) } とは これを var count by remember { mutableStateOf(0) } に当てはめる。 count : 変数名 remember { mutableStateOf(0) } : getValueとsetValueを持つオブジェクト remember { mutableStateOf(0) } は MutableState<Int> 型のインスタンスを返す関数の呼び出しである。MutableState<Int> とそのインタフェースである State<T> はそれぞれ次のように定義されている。 package androidx.compose.runtime @Stable interface State<out T> { val value: T } @Stable interface MutableState<T> : State<T> { override var value: T operator fun component1(): T operator fun component2(): (T) -> Unit } var count by remember { ... } といった記述を行うために必要な getValue と setValue の定義が見当たらない。これらのメソッドは拡張関数として定義されている。 package androidx.compose.runtime @Suppress("NOTHING_TO_INLINE") inline operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value @Suppress("NOTHING_TO_INLINE") inline operator fun <T> MutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) { this.value = value } よって、var count by remember { ... } と書くためには次の import 文を追加する必要がある。 import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue まとめ var A by B という記法は委譲プロパティと呼ばれるもので、B は getValue と setValue を持つオブジェクトでなければならない remember { mutableStateOf(0) } は MutableState<T> 型のインスタンスを返すが、この型とそのインタフェースである State<T> 自体には getValue と setValue の定義が含まれていない。これらは拡張関数として定義されており、androidx.compose.runtime.getValue と androidx.compose.runtime.setValue をインポートすることで使えるようになる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NDK at /Users/user.name/Library/Android/sdk/ndk/XX.X.XXXXX did not have a source.properties file エラーが発生する

やりたいこと Androidアプリをビルドしたい。 ./gradlew build エラー文 以下のエラーが発生。 ❯ ./gradlew build FAILURE: Build failed with an exception. * What went wrong: A problem occurred configuring project ':renderscript-toolkit'. > com.android.builder.errors.EvalIssueException: NDK at /Users/user.name/Library/Android/sdk/ndk/21.3.6528147 did not have a source.properties file * Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights. * Get more help at https://help.gradle.org BUILD FAILED in 1s 原因 対応するバージョンのNDKがインストールされていない。 解決策 NDKのインストール 以下から、エラーログに記載されていたバージョンのNDKをインストールする。 https://developer.android.com/studio/projects/install-ndk#specific-version gradle build の実行 ❯ ./gradlew build
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む