<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Jetpack Compose</title>
	<atom:link href="https://bps-e.com/dev/category/android/jetpack-compose/feed/" rel="self" type="application/rss+xml" />
	<link>https://bps-e.com/dev</link>
	<description>android アプリ開発 kotlin + jetpack compose + material 3</description>
	<lastBuildDate>Thu, 02 Oct 2025 22:50:01 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.8.3</generator>

<image>
	<url>https://bps-e.com/dev/wp-content/uploads/2022/10/cropped-logo3-32x32.png</url>
	<title>Jetpack Compose</title>
	<link>https://bps-e.com/dev</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>[Android] 006. TopAppBarのshadow</title>
		<link>https://bps-e.com/dev/android-006-topappbar%e3%81%aeshadow/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Thu, 02 Oct 2025 22:48:54 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Material 3]]></category>
		<category><![CDATA[Narwhal]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=1712</guid>

					<description><![CDATA[Material 3のTopAppBarに境界線の影がなくて、古いアプリのデザインと合わない；；みたいな人少ないかもでググってもサクッと出てこなかったので需要ないっぽいけど、こんなコードを見かけたので&#8230; 昔は [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Material 3のTopAppBarに境界線の影がなくて、古いアプリのデザインと合わない；；みたいな人少ないかもでググってもサクッと出てこなかったので需要ないっぽいけど、こんなコードを見かけたので&#8230;</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>Scaffold(
    topBar = {
        Surface(shadowElevation = 4.dp) {
            CenterAlignedTopAppBar(
                title = { Text(&quot;hoge&quot;) },
            )
        }
    },
)</code></pre></div>



<p>昔はModifierにshadowなかった？<br>単純にSurfaceにshadowElevationがあったから使っただけかもだけど</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>CenterAlignedTopAppBar(
    title = { Text(&quot;hoge&quot;) },
    Modifier.shadow(4.dp)
)</code></pre></div>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="has-text-align-right">Android Studio Narwhal 3 Feature Drop 2025.1.3, built on August 28, 2025</p>



<p></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 005. ネストしてるScaffold</title>
		<link>https://bps-e.com/dev/android-005-%e3%83%8d%e3%82%b9%e3%83%88%e3%81%97%e3%81%a6%e3%82%8bscaffold/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Thu, 02 Oct 2025 22:05:45 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Material 3]]></category>
		<category><![CDATA[Narwhal]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=1702</guid>

					<description><![CDATA[よくありそうなBottomBarにNavigationBarをおいてTopBarは各View側で制御したい場合のView配置で適当にpadding設定すると隙間があいてしまう場合の対応コピペサンプル 正解 ダメな実装 内 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>よくありそうなBottomBarにNavigationBarをおいてTopBarは各View側で制御したい場合のView配置で適当にpadding設定すると隙間があいてしまう場合の対応コピペサンプル</p>



<h2 class="wp-block-heading"><span id="toc1">正解</span></h2>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>Scaffold(
    Modifier.fillMaxSize(),
    bottomBar = {
        Row(Modifier.fillMaxWidth().height(80.dp).background(Color.Cyan)) {}
    },
    containerColor = Color.Red
) { innerPadding -&gt;
    val title = &quot;test&quot;
    val layoutDirection = LocalLayoutDirection.current
    Scaffold(
        Modifier.padding(
            start = innerPadding.calculateLeftPadding(layoutDirection),
            end = innerPadding.calculateRightPadding(layoutDirection),
            bottom = innerPadding.calculateBottomPadding()
        ),
        topBar = {
            CenterAlignedTopAppBar(
                title = { Text(title) },
                colors = TopAppBarDefaults.topAppBarColors().copy(containerColor = Color.Magenta)
            )
        },
        containerColor = Color.Green
    ) { innerPadding -&gt;
        val topPadding = innerPadding.calculateTopPadding()
        Column(
            Modifier.fillMaxSize().padding(top = topPadding),
        ) {
            Text(title)
            Column(
                Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Bottom
            ) {
                Text(title)
            }
        }
    }
}</code></pre></div>



<figure class="wp-block-image size-large is-resized"><img fetchpriority="high" decoding="async" width="576" height="1024" src="https://bps-e.com/dev/wp-content/uploads/2025/10/image-2-576x1024.png" alt="" class="wp-image-1705" style="width:250px" srcset="https://bps-e.com/dev/wp-content/uploads/2025/10/image-2-576x1024.png 576w, https://bps-e.com/dev/wp-content/uploads/2025/10/image-2-169x300.png 169w, https://bps-e.com/dev/wp-content/uploads/2025/10/image-2.png 720w" sizes="(max-width: 576px) 100vw, 576px" /></figure>



<h2 class="wp-block-heading"><span id="toc2">ダメな実装</span></h2>



<p>内部のScaffoldのbottomに余計なpaddingが入ってしまう</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>Scaffold(
    Modifier.fillMaxSize(),
    bottomBar = {
        Row(Modifier.fillMaxWidth().height(80.dp).background(Color.Cyan)) {}
    },
    containerColor = Color.Red
) { innerPadding -&gt;
    val title = &quot;test&quot;
    Scaffold(
        Modifier.padding(innerPadding),
        topBar = {
            CenterAlignedTopAppBar(
                title = { Text(title) },
                colors = TopAppBarDefaults.topAppBarColors().copy(containerColor = Color.Magenta)
            )
        },
        containerColor = Color.Green
    ) { innerPadding -&gt;
        Column(
            Modifier.fillMaxSize().padding(innerPadding),
        ) {
            Text(title)
            Column(
                Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Bottom
            ) {
                Text(title)
            }
        }
    }
}</code></pre></div>



<figure class="wp-block-image size-full is-resized"><img decoding="async" width="720" height="1280" src="https://bps-e.com/dev/wp-content/uploads/2025/10/image.png" alt="" class="wp-image-1703" style="width:250px" srcset="https://bps-e.com/dev/wp-content/uploads/2025/10/image.png 720w, https://bps-e.com/dev/wp-content/uploads/2025/10/image-169x300.png 169w" sizes="(max-width: 720px) 100vw, 720px" /></figure>



<h2 class="wp-block-heading"><span id="toc3">おしい実装</span></h2>



<p>statusbar分がズレている</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>Scaffold(
    Modifier.fillMaxSize(),
    bottomBar = {
        Row(Modifier.fillMaxWidth().height(80.dp).background(Color.Cyan)) {}
    },
    containerColor = Color.Red
) { innerPadding -&gt;
    val title = &quot;test&quot;
    Scaffold(
        Modifier.padding(innerPadding),
        topBar = {
            CenterAlignedTopAppBar(
                title = { Text(title) },
                colors = TopAppBarDefaults.topAppBarColors().copy(containerColor = Color.Magenta)
            )
        },
        containerColor = Color.Green
    ) { innerPadding -&gt;
        val topPadding = innerPadding.calculateTopPadding()
        Column(
            Modifier.fillMaxSize().padding(top = topPadding),
        ) {
            Text(title)
            Column(
                Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Bottom
            ) {
                Text(title)
            }
        }
    }
}</code></pre></div>



<figure class="wp-block-image size-large is-resized"><img decoding="async" width="576" height="1024" src="https://bps-e.com/dev/wp-content/uploads/2025/10/image-1-576x1024.png" alt="" class="wp-image-1704" style="width:250px" srcset="https://bps-e.com/dev/wp-content/uploads/2025/10/image-1-576x1024.png 576w, https://bps-e.com/dev/wp-content/uploads/2025/10/image-1-169x300.png 169w, https://bps-e.com/dev/wp-content/uploads/2025/10/image-1.png 720w" sizes="(max-width: 576px) 100vw, 576px" /></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="has-text-align-right">Android Studio Narwhal 3 Feature Drop 2025.1.3, built on August 28, 2025</p>



<p></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 004. @Preview(デバイスサイズごと)</title>
		<link>https://bps-e.com/dev/android-008-004/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Wed, 25 Jun 2025 19:04:39 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Material 3]]></category>
		<category><![CDATA[Narwhal]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=1691</guid>

					<description><![CDATA[最新のNow in Androidを眺めていたら&#8230; ん？@DevicePreviews？？？ たしかにいちいち個別にデバイスごとにPreview実装する必要なかった&#8230;もっとはやく知りたかったorz]]></description>
										<content:encoded><![CDATA[
<p>最新のNow in Androidを眺めていたら&#8230;</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>@DevicePreviews
@Composable
fun ForYouScreenPopulatedAndLoading(...)</code></pre></div>



<p>ん？@DevicePreviews？？？</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>/**
 * Multipreview annotation that represents various device sizes. Add this annotation to a composable
 * to render various devices.
 */
@Preview(name = &quot;phone&quot;, device = &quot;spec:shape=Normal,width=360,height=640,unit=dp,dpi=480&quot;)
@Preview(name = &quot;landscape&quot;, device = &quot;spec:shape=Normal,width=640,height=360,unit=dp,dpi=480&quot;)
@Preview(name = &quot;foldable&quot;, device = &quot;spec:shape=Normal,width=673,height=841,unit=dp,dpi=480&quot;)
@Preview(name = &quot;tablet&quot;, device = &quot;spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480&quot;)
annotation class DevicePreviews</code></pre></div>



<p>たしかにいちいち個別にデバイスごとにPreview実装する必要なかった&#8230;<br>もっとはやく知りたかったorz</p>



<p></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 003. 状態ホルダーとUI状態(UiState)</title>
		<link>https://bps-e.com/dev/android-008-003/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Wed, 25 Jun 2025 18:51:42 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Narwhal]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=1685</guid>

					<description><![CDATA[雰囲気で実装している感が若干あるUiStateGoogleのサンプルでも時代と中の人のトレンドによって扱いが変わってきているのでちょっと見直し 2022/10/26あたりのGoogleの考え方https://develo [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>雰囲気で実装している感が若干あるUiState<br>Googleのサンプルでも時代と中の人のトレンドによって扱いが変わってきているのでちょっと見直し</p>



<p>2022/10/26あたりのGoogleの考え方<br><a rel="noopener" href="https://developer.android.com/topic/architecture/ui-layer/stateholders" target="_blank">https://developer.android.com/topic/architecture/ui-layer/stateholders</a></p>



<p>状態の保存など<br><a rel="noopener" href="https://developer.android.com/develop/ui/compose/state" target="_blank">https://developer.android.com/develop/ui/compose/state</a></p>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><span id="toc1">よくあるUiStateの実装</span></h3>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>class HogeViewModel {
    private val _uiState = MutableStateFlow(UiState())
    val uiState = _uiState.asStateFlow()
    
    data class UiState(
        val isLoading: Boolean = false,
        val text: String = &quot;huga&quot;
    )

    init {
        _uiState.update(...)
    }
}

fun HogeView(vm: ViewModel) {
    val uiState by viewModel.uiState.collectAsState()
    ...
}</code></pre></div>



<p>昔はcollectAsStateWithLifecycleはなかったのでcollectAsStateで呼び出していたけど、今はKMP向けとかではなければcollectAsStateWithLifecycleで呼び出すのがマスト</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>プラットフォームに依存しないコードの場合は、Android のみの&nbsp;<code>collectAsStateWithLifecycle</code>&nbsp;ではなく、<code>collectAsState</code>&nbsp;を使用します。<br><a href="https://developer.android.com/develop/ui/compose/state#use-other-types-of-state-in-jetpack-compose">https://developer.android.com/develop/ui/compose/state#use-other-types-of-state-in-jetpack-compose</a></p>
</blockquote>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><span id="toc2">最近のNow in Android</span></h3>



<p>ForYouScreenをベースと実装同じメソッド名にしてたり直接UiStateを部品に渡す作りはちょっと好みじゃないというか流派が違うけど、これが最新の中の人のトレンドっぽい<br>Now in Androidのサンプルの画面構成のためそうなってる感もあるけど、UiStateはViewModelとセットではなく、コンテンツごとに定義して状態ベースで表示内容を渡す作り</p>



<p>UiStateの状態取得はStateFlow上で実装</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>sealed interface OnboardingUiState {
    /**
     * The onboarding state is loading.
     */
    data object Loading : OnboardingUiState
    /**
     * The onboarding state was unable to load.
     */
    data object LoadFailed : OnboardingUiState

    /**
     * There is no onboarding state.
     */
    data object NotShown : OnboardingUiState

    /**
     * There is a onboarding state, with the given lists of topics.
     */
    data class Shown(
        val topics: List&lt;FollowableTopic&gt;,
    ) : OnboardingUiState {
        /**
         * True if the onboarding can be dismissed.
         */
        val isDismissable: Boolean get() = topics.any { it.isFollowed }
    }
}

class ForYouViewModel(...) {
    val onboardingUiState: StateFlow&lt;OnboardingUiState&gt; =
        combine(
            shouldShowOnboarding,
            getFollowableTopics(),
        ) { shouldShowOnboarding, topics -&gt;
            if (shouldShowOnboarding) {
                OnboardingUiState.Shown(topics = topics)
            } else {
                OnboardingUiState.NotShown
            }
        }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = OnboardingUiState.Loading,
            )

}

internal fun ForYouScreen(...) {
    val onboardingUiState by viewModel.onboardingUiState.collectAsStateWithLifecycle()
    ForYouScreen(onboardingUiState)
}

internal fun ForYouScreen(
    onboardingUiState: OnboardingUiState
) {
    val isOnboardingLoading = onboardingUiState is OnboardingUiState.Loading

    LazyVerticalStaggeredGrid(...) {
        onboarding(
            onboardingUiState = onboardingUiState,
            ...
    }
}

private fun LazyStaggeredGridScope.onboarding(
    onboardingUiState: OnboardingUiState
) {
    when (onboardingUiState) {
        OnboardingUiState.Loading,
        OnboardingUiState.LoadFailed,
        OnboardingUiState.NotShown,
        -&gt; Unit

        is OnboardingUiState.Shown -&gt; {
        ...
}</code></pre></div>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<p>どっちがベストプラクティスかってこともないと思います<br>Now in Androidのように一つの画面にいろいろなコンテンツ制御があって複雑でないなら、よくあるUiStateで十分で状態に対して表示される内容を分離して扱えるので、むしろ直感的に実装できると思います（たとえばUiStateにErrorの状態を追加した場合、画面に渡すデータはShownの内容と同じものを渡さないといけないのか&#8230;などいちいち考えなくてすむ）</p>



<pre class="wp-block-preformatted">以前のパターンとの違いと使い分け<br>以前の「UiStateは単一のdata classで、その中にローディングやエラーのフラグを持つ」というパターンも引き続き有効であり、多くのシンプルな画面で使われます。<br><br>単一のdata classパターンが向いている場合:<br>状態が比較的シンプルで、ローディング中やエラー時でも、一部のデータは常に表示され続けるような画面。<br>例えば、ユーザープロフィール画面で、名前やプロフィール画像は常に表示しつつ、詳細データ（過去の投稿履歴など）の読み込みがエラーになった場合にのみエラーメッセージを表示するようなケース。<br><br>sealed class/interfaceパターンが向いている場合:<br>UIが複数の明確な「フェーズ」を持ち、各フェーズでUIの表示内容や振る舞いが大きく異なる場合。<br>例えば、オンボーディング、フォーム入力（複数のステップがある場合）、データが完全に揃わないと何も表示できない画面など。<br>エラーが発生した場合に、それまでのデータは破棄してエラー状態に完全に遷移するような振る舞いをしたい場合（今回のオンボーディングの例ではLoadFailedがそれに当たります）。</pre>



<p>ベンリダナー&#8230;</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="has-text-align-right">Android Studio Narwhal 2025.1.1, built on June 19, 2025</p>



<p></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 002. Jetpack Navigation 3</title>
		<link>https://bps-e.com/dev/android-008-002/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Wed, 25 Jun 2025 18:00:10 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Narwhal]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=1681</guid>

					<description><![CDATA[なんか概念がガラッと変わりそうです全然追えてないのでとりあえずのリンク集&#8230;いつのまにかにalpha版になっていましたが、まだ依存関係や仕様はぐちゃぐちゃな雰囲気なのでもうちょっと泳がしておいてもいいかも ・C [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>なんか概念がガラッと変わりそうです<br>全然追えてないのでとりあえずのリンク集&#8230;<br>いつのまにかにalpha版になっていましたが、まだ依存関係や仕様はぐちゃぐちゃな雰囲気なのでもうちょっと泳がしておいてもいいかも</p>



<p>・Core Concepts<br><a href="https://www.youtube.com/watch?v=opLYavQHBB8&amp;list=WL&amp;index=2&amp;t=283s&amp;ab_channel=AndroidDevelopers">https://www.youtube.com/watch?v=opLYavQHBB8&amp;list=WL&amp;index=2&amp;t=283s&amp;ab_channel=AndroidDevelopers</a></p>



<p>Navigation 3を理解するために見ておいた方がよい、良い内容でした！</p>



<p>・Developer</p>




<a rel="noopener" href="https://developer.android.com/jetpack/androidx/releases/navigation3" title="navigation3  |  Jetpack  |  Android Developers" class="blogcard-wrap external-blogcard-wrap a-wrap cf" target="_blank"><div class="blogcard external-blogcard eb-left cf"><div class="blogcard-label external-blogcard-label"><span class="fa"></span></div><figure class="blogcard-thumbnail external-blogcard-thumbnail"><img loading="lazy" decoding="async" src="https://developer.android.com/static/images/social/android-developers.png" alt="" class="blogcard-thumb-image external-blogcard-thumb-image" width="160" height="90" /></figure><div class="blogcard-content external-blogcard-content"><div class="blogcard-title external-blogcard-title">navigation3  |  Jetpack  |  Android Developers</div><div class="blogcard-snippet external-blogcard-snippet"></div></div><div class="blogcard-footer external-blogcard-footer cf"><div class="blogcard-site external-blogcard-site"><div class="blogcard-favicon external-blogcard-favicon"><img loading="lazy" decoding="async" src="https://www.google.com/s2/favicons?domain=https://developer.android.com/jetpack/androidx/releases/navigation3" alt="" class="blogcard-favicon-image external-blogcard-favicon-image" width="16" height="16" /></div><div class="blogcard-domain external-blogcard-domain">developer.android.com</div></div></div></div></a>



<p>・Developer Guide</p>




<a rel="noopener" href="https://developer.android.com/guide/navigation/navigation-3" title="Navigation 3  |  App architecture  |  Android Developers" class="blogcard-wrap external-blogcard-wrap a-wrap cf" target="_blank"><div class="blogcard external-blogcard eb-left cf"><div class="blogcard-label external-blogcard-label"><span class="fa"></span></div><figure class="blogcard-thumbnail external-blogcard-thumbnail"><img loading="lazy" decoding="async" src="https://developer.android.com/static/images/social/android-developers.png" alt="" class="blogcard-thumb-image external-blogcard-thumb-image" width="160" height="90" /></figure><div class="blogcard-content external-blogcard-content"><div class="blogcard-title external-blogcard-title">Navigation 3  |  App architecture  |  Android Developers</div><div class="blogcard-snippet external-blogcard-snippet"></div></div><div class="blogcard-footer external-blogcard-footer cf"><div class="blogcard-site external-blogcard-site"><div class="blogcard-favicon external-blogcard-favicon"><img loading="lazy" decoding="async" src="https://www.google.com/s2/favicons?domain=https://developer.android.com/guide/navigation/navigation-3" alt="" class="blogcard-favicon-image external-blogcard-favicon-image" width="16" height="16" /></div><div class="blogcard-domain external-blogcard-domain">developer.android.com</div></div></div></div></a>



<p>・Sample</p>




<a rel="noopener" href="https://github.com/android/nav3-recipes" title="GitHub - android/nav3-recipes: Implement common use cases with Jetpack Navigation 3" class="blogcard-wrap external-blogcard-wrap a-wrap cf" target="_blank"><div class="blogcard external-blogcard eb-left cf"><div class="blogcard-label external-blogcard-label"><span class="fa"></span></div><figure class="blogcard-thumbnail external-blogcard-thumbnail"><img loading="lazy" decoding="async" src="https://opengraph.githubassets.com/1bafaaa33de428c27bff7b5767ec52e0238259245fc5a1a4c8f99521c7ff7b39/android/nav3-recipes" alt="" class="blogcard-thumb-image external-blogcard-thumb-image" width="160" height="90" /></figure><div class="blogcard-content external-blogcard-content"><div class="blogcard-title external-blogcard-title">GitHub - android/nav3-recipes: Implement common use cases with Jetpack Navigation 3</div><div class="blogcard-snippet external-blogcard-snippet">Implement common use cases with Jetpack Navigation 3 - android/nav3-recipes</div></div><div class="blogcard-footer external-blogcard-footer cf"><div class="blogcard-site external-blogcard-site"><div class="blogcard-favicon external-blogcard-favicon"><img loading="lazy" decoding="async" src="https://www.google.com/s2/favicons?domain=https://github.com/android/nav3-recipes" alt="" class="blogcard-favicon-image external-blogcard-favicon-image" width="16" height="16" /></div><div class="blogcard-domain external-blogcard-domain">github.com</div></div></div></div></a>



<p></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 003. 触覚フィードバック(バイブレーション)</title>
		<link>https://bps-e.com/dev/android-007-002-2/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Tue, 06 May 2025 01:08:38 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Meerkat]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=1668</guid>

					<description><![CDATA[androidでバイブレーションの実装って実はやったことなかった今やってる案件の仕様にあったような記憶だったけど、仕様書には明記されてない🤔試してみたのでせっかくだから残しておきます イベントに触覚フィードバックを追加す [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>androidでバイブレーションの実装って実はやったことなかった<br>今やってる案件の仕様にあったような記憶だったけど、仕様書には明記されてない🤔<br>試してみたのでせっかくだから残しておきます</p>




<a rel="noopener" href="https://developer.android.com/develop/ui/views/haptics/haptic-feedback?hl=ja" title="イベントに触覚フィードバックを追加する  |  Views  |  Android Developers" class="blogcard-wrap external-blogcard-wrap a-wrap cf" target="_blank"><div class="blogcard external-blogcard eb-left cf"><div class="blogcard-label external-blogcard-label"><span class="fa"></span></div><figure class="blogcard-thumbnail external-blogcard-thumbnail"><img loading="lazy" decoding="async" src="https://developer.android.com/static/images/social/android-developers.png?hl=ja" alt="" class="blogcard-thumb-image external-blogcard-thumb-image" width="160" height="90" /></figure><div class="blogcard-content external-blogcard-content"><div class="blogcard-title external-blogcard-title">イベントに触覚フィードバックを追加する  |  Views  |  Android Developers</div><div class="blogcard-snippet external-blogcard-snippet"></div></div><div class="blogcard-footer external-blogcard-footer cf"><div class="blogcard-site external-blogcard-site"><div class="blogcard-favicon external-blogcard-favicon"><img loading="lazy" decoding="async" src="https://www.google.com/s2/favicons?domain=https://developer.android.com/develop/ui/views/haptics/haptic-feedback?hl=ja" alt="" class="blogcard-favicon-image external-blogcard-favicon-image" width="16" height="16" /></div><div class="blogcard-domain external-blogcard-domain">developer.android.com</div></div></div></div></a>



<p>ちなみにOS設定の音とバイブレーション でバイブレーションとハプティクスをOFFとかにしていると動作しないので確認時注意（テスト機OFFってた😅）</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain" data-file="AndroidManifest.xml"><code>&lt;uses-permission android:name=&quot;android.permission.VIBRATE&quot;/&gt;</code></pre></div>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>// ボタンやキーのフィードバックの場合はperformHapticFeedbackで実装がよい
val view = LocalView.current
if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.S) {
    view.performHapticFeedback(HapticFeedbackConstants.CONFIRM)
}</code></pre></div>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>// ワンショット
val context = LocalContext.current
//val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
val vibrator = context.getSystemService(Vibrator::class.java)
if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.O) {
    val vibrationEffect = VibrationEffect.createOneShot(100, DEFAULT_AMPLITUDE)
    vibrator.vibrate(vibrationEffect)
} else {
     vibrator.vibrate(100)
}</code></pre></div>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>// VibratorManager
@SuppressLint(&quot;ServiceCast&quot;)
if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.S) {
    val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
    // 100ms振動 / 100ms止める, 振動強度 0 - 255, 繰り返さない/繰り返す -1 / 0 配列の長さまで指定可能
    val vibrationEffect = VibrationEffect.createWaveform(longArrayOf(100L, 100L), intArrayOf(200, 0), -1)
    val combinedVibration = CombinedVibration.createParallel(vibrationEffect)
    vibratorManager.vibrate(combinedVibration)
}</code></pre></div>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="has-text-align-right">Android Studio Meerkat 2024.3.1 Patch 2 built on April 16, 2025</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 002. ConstraintLayout</title>
		<link>https://bps-e.com/dev/android-007-002/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Fri, 18 Apr 2025 19:17:54 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Meerkat]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=1657</guid>

					<description><![CDATA[Rowで左側のコンポーネントの幅が可変で右側が固定で、左寄せレイアウトがRowできないorzRowのModifireでできれば楽なのに&#8230;https://developer.android.com/develo [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="887" height="663" src="https://bps-e.com/dev/wp-content/uploads/2025/04/image.png" alt="" class="wp-image-1658" srcset="https://bps-e.com/dev/wp-content/uploads/2025/04/image.png 887w, https://bps-e.com/dev/wp-content/uploads/2025/04/image-300x224.png 300w, https://bps-e.com/dev/wp-content/uploads/2025/04/image-768x574.png 768w" sizes="(max-width: 887px) 100vw, 887px" /></figure>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>@Composable
fun Screen(modifier: Modifier = Modifier) {
    Column(
        modifier.fillMaxSize().padding(44.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Content1(&quot;1234567890&quot;)
        // TextがfillWidthになってbuttonが画面外に...
        Content1(&quot;1234567890&quot;.repeat(10))

        // 違う、そうじゃない
        Content2(&quot;1234567890&quot;)
        Content2(&quot;1234567890&quot;.repeat(10))

        // Good!
        Content3(&quot;1234567890&quot;)
        Content3(&quot;1234567890&quot;.repeat(10))
    }
}

@Composable
fun Content1(text: String) {
    Row(
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Text(
            text,
            maxLines = 1,
            overflow = TextOverflow.Ellipsis
        )
        Button(onClick = {}) {
            Text(&quot;ok&quot;)
        }
    }
}

@Composable
fun Content2(text: String) {
    Row(
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Text(
            text,
            Modifier.weight(1f),
            maxLines = 1,
            overflow = TextOverflow.Ellipsis
        )
        Button(onClick = {}) {
            Text(&quot;ok&quot;)
        }
    }
}

@Composable
fun Content3(text: String) {
    ConstraintLayout {
        val (left, right) = createRefs()

        Text(
            text,
            Modifier.constrainAs(left) {
                start.linkTo(parent.start)
                end.linkTo(right.start, margin = 8.dp)
                width = Dimension.preferredWrapContent
                centerVerticallyTo(parent)
            },
            maxLines = 1,
            overflow = TextOverflow.Ellipsis
        )

        Button(
            onClick = {},
            Modifier.constrainAs(right) {
                end.linkTo(parent.end)
                centerVerticallyTo(parent)
            }
        ) {
            Text(&quot;ok&quot;)
        }
    }
}</code></pre></div>



<p>Rowで左側のコンポーネントの幅が可変で右側が固定で、左寄せレイアウトがRowできないorz<br>RowのModifireでできれば楽なのに&#8230;<br><a href="https://developer.android.com/develop/ui/compose/layouts/constraintlayout">https://developer.android.com/develop/ui/compose/layouts/constraintlayout</a></p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>[versions]
constraintlayoutCompose = &quot;1.1.1&quot;

[libraries]
androidx-constraintlayout-compose = { group = &quot;androidx.constraintlayout&quot;, name = &quot;constraintlayout-compose&quot;, version.ref = &quot;constraintlayoutCompose&quot; }</code></pre></div>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="has-text-align-right">Android Studio Meerkat 2024.3.1 Patch 1 built on March 13, 2025</p>



<p></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 021. Modifier.animateContentSizeとElevated</title>
		<link>https://bps-e.com/dev/android-005-021/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Tue, 17 Sep 2024 15:40:56 +0000</pubDate>
				<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Koala]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Material 3]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=1572</guid>

					<description><![CDATA[コンポーネントのサイズ変更時のアニメーションですが、Modifierのセット順でElevatedCardなどの影が表示されない罠があったので覚書き paddingつけないと表示されないとか説明してるサイトもあったけどそん [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>コンポーネントのサイズ変更時のアニメーションですが、Modifierのセット順でElevatedCardなどの影が表示されない罠があったので覚書き</p>



<p>paddingつけないと表示されないとか説明してるサイトもあったけどそんなことはない<br>size指定前にanimateContentSizeを指定すればok</p>



<p><a rel="noopener" href="https://developer.android.com/develop/ui/compose/animation/quick-guid" target="_blank">https://developer.android.com/develop/ui/compose/animation/quick-guid</a><br><a rel="noopener" href="https://developer.android.com/reference/kotlin/androidx/compose/animation/package-summary#(androidx.compose.ui.Modifier).animateContentSize(androidx.compose.animation.core.FiniteAnimationSpec,kotlin.Function2)" target="_blank">https://developer.android.com/reference/kotlin/androidx/compose/animation/package-summary#(androidx.compose.ui.Modifier).animateContentSize(androidx.compose.animation.core.FiniteAnimationSpec,kotlin.Function2)</a><br><a rel="noopener" href="https://developer.android.com/develop/ui/compose/components/card?elevated#elevated" target="_blank">https://developer.android.com/develop/ui/compose/components/card?elevated#elevated</a></p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>@Composable
fun Screen(modifier: Modifier = Modifier) {
    var expanded by remember { mutableStateOf(false) }

    Column(
        modifier.padding(44.dp),
    ) {
        ElevatedCard(
            onClick = { expanded = !expanded },
            elevation = CardDefaults.cardElevation(
                defaultElevation = 8.dp
            ),
        ) {
            Column(
                Modifier
                    .animateContentSize(
                        spring(
                            stiffness = Spring.StiffnessHigh,
                            visibilityThreshold = IntSize.VisibilityThreshold
                        )
                    )
                    .fillMaxWidth()
                    .padding(horizontal = 12.dp, vertical = 8.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Text(&quot;ElevatedCard&quot;)
                    Icon(
                        Icons.Default.ArrowDropDown,
                        &quot;&quot;,
                        Modifier
                            .then(
                                if (expanded) Modifier.rotate(180f)
                                else Modifier
                            )
                    )
                }
                if (expanded) Text(&quot;あ&quot;.repeat(100))
            }
        }
    }
}</code></pre></div>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="has-text-align-right">Android Studio Koala Feature Drop 2024.1.2 built on August 23, 2024</p>



<p></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 020. Firebase In-app Messaging</title>
		<link>https://bps-e.com/dev/android-005-020/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Mon, 16 Sep 2024 13:19:14 +0000</pubDate>
				<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Koala]]></category>
		<category><![CDATA[Kotlin]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=1558</guid>

					<description><![CDATA[なんか使う要件がありそうなので事前に調査してみましたといってもカスタムUIを使わないのであれば、実装する内容はほぼなしhttps://firebase.google.com/docs/in-app-messaging/g [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>なんか使う要件がありそうなので事前に調査してみました<br>といってもカスタムUIを使わないのであれば、実装する内容はほぼなし<br><a rel="noopener" href="https://firebase.google.com/docs/in-app-messaging/get-started?hl=ja&amp;platform=android" target="_blank">https://firebase.google.com/docs/in-app-messaging/get-started?hl=ja&amp;platform=android</a></p>



<p>Firebaseの説明を見た感じでは依存関係を追加して、Firebase側でキャンペーンを作るだけ<br><code>FirebaseApp.initializeApp()</code>すら不要</p>



<p>ただ罠がありました<br>・テストキャンペーンがつくれない？次へボタンが非活性のままで先に進めず&#8230;<br>・Google Cloud ConsoleでFirebase In-App Messaging APIを有効にするのとAPIキーの設定で有効にしないと動作しない</p>



<p></p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>[versions]
firebaseBom = &quot;33.2.0&quot;

[libraries]
firebase-bom = { group = &quot;com.google.firebase&quot;, name = &quot;firebase-bom&quot;, version.ref = &quot;firebaseBom&quot; }
firebase-analytics = { group = &quot;com.google.firebase&quot;, name = &quot;firebase-analytics&quot; }
firebase-inappmessaging-display = { group = &quot;com.google.firebase&quot;, name = &quot;firebase-inappmessaging-display&quot; }</code></pre></div>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>dependencies {
    implementation(platform(libs.firebase.bom))
    implementation(libs.firebase.analytics)
    implementation(libs.firebase.inappmessaging.display)
}</code></pre></div>



<p>何故か公式ドキュメントに説明がないっぽい発火イベント<br>analyticsのイベントが参考になるかも<br><a href="https://support.google.com/analytics/answer/9234069?hl=ja">https://support.google.com/analytics/answer/9234069?hl=ja</a></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Event</th><th></th><th>動作確認済み</th></tr></thead><tbody><tr><td>on_foreground</td><td>アプリがフォアグラウンドになったタイミング</td><td>○</td></tr><tr><td>app_launch</td><td>アプリが起動したタイミング</td><td>&#8211;</td></tr><tr><td>app_exception</td><td>アプリでエラーが発生したタイミング</td><td>&#8211;</td></tr><tr><td>app_open</td><td>アプリが初めて開かれたタイミング（インストール直後など）</td><td>&#8211;</td></tr><tr><td>app_remove</td><td>アプリがアンインストールされたタイミング</td><td>&#8211;</td></tr><tr><td>fiam_action</td><td>In-App Message内のボタンなどがタップされたタイミング</td><td>○</td></tr><tr><td>fiam_dismiss</td><td>In-App Messageが閉じられたタイミング</td><td>○</td></tr><tr><td>fiam_impression</td><td>In-App Messageが表示されたタイミング</td><td>○</td></tr><tr><td>screen_view</td><td>特定の画面が表示されたタイミング</td><td>○</td></tr><tr><td>session_start</td><td>アプリの操作がない状態でセッション タイムアウト時間が経過した後に、最小セッション継続時間を超えるアプリの使用があった</td><td>○</td></tr><tr><td>custom※</td><td>Firebase.inAppMessaging.triggerEventで指定したタイミング</td><td>○</td></tr></tbody></table></figure>



<p>※ customキーは直接指定したいキーをFirebase Consoleで入力</p>



<p>同じキーで登録した場合は新しい方が先にでて、次に同じ表示タイミングが呼ばれたら次のが表示されるようです</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>// 表示したいタイミングでtriggerEventを呼ぶだけ
Firebase.inAppMessaging.triggerEvent(&quot;xxx&quot;)</code></pre></div>



<div style="height:7px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><span id="toc1">Button Action</span></h2>



<p>ボタンの実行をカスタマイズしたい場合は実行するURLを実装していないURLスキーム形式に変更すれば、ボタンを押してブラウザが起動されないので、それを利用します</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain" data-file="MainActivity.kt"><code>val fbInAppMessagingClickListener = object : FirebaseInAppMessagingClickListener {
        override fun messageClicked(
            inAppMessage: InAppMessage, action: Action
        ) {
            // hoge://url?https://hoge.com/
            val url = URI(action.actionUrl).query
            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url + &quot;?id=xxx&quot;))
            startActivity(intent)
        }
    }
}

override fun onStart() {
    super.onStart()
    FirebaseInAppMessaging.getInstance().addClickListener(fbInAppMessagingClickListener)
}

override fun onStop() {
    super.onStop()
    FirebaseInAppMessaging.getInstance().removeClickListener(fbInAppMessagingClickListener)
}</code></pre></div>



<h2 class="wp-block-heading"><span id="toc2">カスタムUI</span></h2>



<p>カスタムUIを実装すると、当然ではありますがFirebase ConsoleでUIが指定できなくなるそうです</p>



<p>オフィシャルの説明はGitHubのサンプルをポイって感じ<br><a href="https://github.com/firebase/firebase-android-sdk/tree/master/firebase-inappmessaging-display">https://github.com/firebase/firebase-android-sdk/tree/master/firebase-inappmessaging-display</a></p>



<p>試せるタイミングがきたらここらへん見ながら試そうと思います<br><a href="https://blog.shinonome.io/firebsae-messaging/">https://blog.shinonome.io/firebsae-messaging/</a><br><a href="https://techlife.cookpad.com/entry/2019/10/25/120000">https://techlife.cookpad.com/entry/2019/10/25/120000</a></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="has-text-align-right">Android Studio Koala Feature Drop 2024.1.2 built on August 23, 2024</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 019. プルダウンリフレッシュ(PullToRefreshBox)</title>
		<link>https://bps-e.com/dev/android-005-019/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Mon, 09 Sep 2024 17:41:27 +0000</pubDate>
				<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Koala]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Material 3]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=1549</guid>

					<description><![CDATA[https://bps-e.com/dev/android-003-039/ で試したプルダウンリフレッシュを使うタイミングが来そうだったので、今の環境で確認してたらCompose BOM 2024.09でMateria [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p><a href="https://bps-e.com/dev/android-003-039/">https://bps-e.com/dev/android-003-039/</a> で試したプルダウンリフレッシュを使うタイミングが来そうだったので、今の環境で確認してたらCompose BOM 2024.09でMaterial3向けのPullToRefreshBoxが追加されていることに気が付きましたので試しました<br><a rel="noopener" href="https://developer.android.com/reference/kotlin/androidx/compose/material3/pulltorefresh/package-summary" target="_blank">https://developer.android.com/reference/kotlin/androidx/compose/material3/pulltorefresh/package-summary</a></p>



<p>thresholdを指定したい場合などIndicatorをカスタマイズする必要がなければ、もっと簡略化したコードで実装できます</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain"><code>@OptIn(ExperimentalMaterial3Api::class)
@Preview(showBackground = true)
@Composable
fun Screen(modifier: Modifier = Modifier) {
    var isRefreshing by remember { mutableStateOf(false) }
    val state = rememberPullToRefreshState()
    val scope = rememberCoroutineScope()

    PullToRefreshBox(
        isRefreshing,
        onRefresh = {
            isRefreshing = true
            scope.launch {
                delay(2_000)
                isRefreshing = false
            }
        },
        modifier = modifier,
        state = state,
        indicator = {
            Indicator(
                modifier = Modifier.align(Alignment.TopCenter),
                isRefreshing = isRefreshing,
                state = state,
                threshold = 100.dp
            )
        }
    ) {
        Column(
            Modifier
                .fillMaxSize()
                .verticalScroll(rememberScrollState()),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(&quot;PullToRefreshBox&quot;)
        }
    }
}</code></pre></div>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="has-text-align-right">Android Studio Koala Feature Drop 2024.1.2 built on August 23, 2024</p>



<p></p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
