<?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>Giraffe</title>
	<atom:link href="https://bps-e.com/dev/category/android/giraffe/feed/" rel="self" type="application/rss+xml" />
	<link>https://bps-e.com/dev</link>
	<description>android アプリ開発 kotlin + jetpack compose + material 3</description>
	<lastBuildDate>Mon, 09 Sep 2024 14:35:41 +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>Giraffe</title>
	<link>https://bps-e.com/dev</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>[Android] 039. プルダウンリフレッシュ(PullRefreshIndicator)</title>
		<link>https://bps-e.com/dev/android-003-039/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Thu, 12 Oct 2023 17:28:21 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Giraffe]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Material 3]]></category>
		<category><![CDATA[PullRefreshIndicator]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=1151</guid>

					<description><![CDATA[X(Twitter)とかであるリストを上から下にスワイプしてリロードする時にIndicatorを表示するUIの実装です Accompanistにその機能があったのですがJetpack Composeに取り込まれてPull [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>X(Twitter)とかであるリストを上から下にスワイプしてリロードする時にIndicatorを表示するUIの実装です</p>



<p>Accompanistにその機能があったのですがJetpack Composeに取り込まれてPullRefreshIndicatorとなりました<br><a rel="noopener" href="https://google.github.io/accompanist/swiperefresh/" target="_blank">https://google.github.io/accompanist/swiperefresh/</a></p>



<p>PullRefreshIndicatorは無印のmaterialに実装されていてmaterial3には未実装ですがmaterial3のコントロールでも使用できます</p>



<p>使用する場合はandroidx.compose.material:materialをimplementationしてください</p>



<p>とりあえずサクッと試します</p>



<p>スクロールが縦の場合に動作します<br>Accompanistの例にもあるようにmaterial3のLazyColumnでも動作しました</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>var _refreshing = mutableStateOf(false)
suspend fun test() = coroutineScope {
    _refreshing.value = true
    delay(3_000)
    _refreshing.value = false
}

@OptIn(ExperimentalMaterialApi::class)
@Composable AppContent() {
    val coroutineScope = rememberCoroutineScope()
    val refreshing by remember { _refreshing }
    val pullRefreshState = rememberPullRefreshState(refreshing, {
        coroutineScope.launch {
            test()
        }
    })

    Box(
        Modifier
            .fillMaxSize()
            .pullRefresh(pullRefreshState)
            .verticalScroll(rememberScrollState())
            //.horizontalScroll(rememberScrollState())
    ) {
        PullRefreshIndicator(
            refreshing,
            pullRefreshState,
            Modifier.align(Alignment.TopCenter)
        )
    }
}</code></pre></div>



<div class="wp-block-columns is-layout-flex wp-container-core-columns-is-layout-28f84493 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-video"><video controls src="https://bps-e.com/dev/wp-content/uploads/2023/10/Screen_recording_20231013_023742.webm"></video></figure>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow"></div>
</div>



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



<p class="has-text-align-right">Android Studio Dolphin 2021.3.1 Patch 1 built on September 30, 2022</p>
]]></content:encoded>
					
		
		<enclosure url="https://bps-e.com/dev/wp-content/uploads/2023/10/Screen_recording_20231013_023742.webm" length="0" type="video/webm" />

			</item>
		<item>
		<title>[Android] 038. 画像のピンチイン/アウト</title>
		<link>https://bps-e.com/dev/android-003-038/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Mon, 02 Oct 2023 19:57:52 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Giraffe]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Material 3]]></category>
		<category><![CDATA[animateFloatAsState]]></category>
		<category><![CDATA[rememberTransformableState]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=1130</guid>

					<description><![CDATA[よくある画像のピンチイン/アウトの実装はJetpack Composeならそれほど頑張らなくてもデベロッパーの説明でほぼ実装できます デベロッパーの以下の説明ですhttps://developer.android.com [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>よくある画像のピンチイン/アウトの実装はJetpack Composeならそれほど頑張らなくてもデベロッパーの説明でほぼ実装できます</p>



<p>デベロッパーの以下の説明です<br><a href="https://developer.android.com/jetpack/compose/gestures?hl=ja#multitouch">https://developer.android.com/jetpack/compose/gestures?hl=ja#multitouch</a></p>



<p>アニメーションについては以下で確認<br><a rel="noopener" href="https://developer.android.com/jetpack/compose/animation/quick-guide?authuser=1" target="_blank">https://developer.android.com/jetpack/compose/animation/quick-guide?authuser=1</a></p>



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



<p>ここでは回転の処理を省いて拡大の制限とタブルタップで拡大縮小処理をアニメーションする実装を追加してます<br>ドラッグ移動はscaleを配慮した値に補正してます<br>また画像はローカルに持ってくるの面倒だったのでCoilでWeb上のものを参照して使用してます<br>背景色は確認時にわかりやすくするためつけてますが実際は透明で良いと思います</p>



<p>動作確認をエミュレータでする場合でピンチイン/アウトはCtrlキー押しながらマウス操作です</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>@Composable
fun TransformableImage() {
    val scaleMax = 5.0f
    var scale by remember { mutableStateOf(1.0f) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    val state = rememberTransformableState { zoomChange, offsetChange, _ -&gt;
        val scale_ = scale * zoomChange
        if (scale_ &gt;= 1.0f && scale_ &lt;= scaleMax) scale = scale_
        offset += Offset(offsetChange.x * scale, offsetChange.y * scale)
    }

    var animating by remember { mutableStateOf(false) }
    val zoom by animateFloatAsState(
        label = &quot;&quot;,
        targetValue = if (scale &lt;= scaleMax / 2) {
            if (animating) scaleMax else scale
        } else {
            if (animating) 1.0f else scale
        },
        animationSpec = tween(durationMillis = if (animating) 300 else 0),
        finishedListener = {
            if (animating) {
                animating = false
                scale = it
            }
        }
    )

    Box(
        Modifier
            .pointerInput(Unit) {
                detectTapGestures(
                    onPress = { /* Called when the gesture starts */ },
                    onDoubleTap = {
                        animating = true
                        offset = Offset.Zero
                    },
                    onLongPress = { /* Called on Long Press */ },
                    onTap = { /* Called on Tap */ }
                )
            }
            .graphicsLayer(
                scaleX = if (animating) zoom else scale,
                scaleY = if (animating) zoom else scale,
                translationX = offset.x,
                translationY = offset.y
            )
            .transformable(state = state)
            .background(Color.Blue)
            .fillMaxSize()
    ) {
        // &lt;uses-permission android:name=&quot;android.permission.INTERNET&quot; /&gt;
        val url = &quot;https://...&quot;
        Image(
            painter = rememberAsyncImagePainter(url), // io.coil-kt:coil-compose
            contentDescription = null,
            modifier = Modifier.fillMaxSize(),
            contentScale = ContentScale.FillWidth
        )
    }
}</code></pre></div>



<p>サムネ画像をクリックで表示する場合のアニメーションはGoogleのFileみたいにscalIn/OutよりslideIn/Outの方がいいかも<br>画面遷移はnavigationよりBox上に重ねて表示/非表示の方が実装が楽です</p>



<p>←にslideInしたい場合はwidth -> widthを指定 <br>※ →は初期値なので未指定かwidth -> -width<br>slideOutの初期値は←で途中で止まってしまうのでwidth -> -widthを実装<br>今回は←にslideInしたので→にslideOut</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>var visible by remember { mutableStateOf(false) }
AnimatedVisibility(
    visible = visible,
    enter = slideInHorizontally(tween(50)) { width -&gt; width },
    exit = slideOutHorizontally(tween(50)) { width -&gt; width },
) {
...
}</code></pre></div>



<p></p>



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



<p class="has-text-align-right">Android Studio Giraffe 2022.3.1 built on June 29, 2023</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 037. TextFieldのPadding</title>
		<link>https://bps-e.com/dev/android-003-037/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Sun, 01 Oct 2023 16:47:18 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Giraffe]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Material 3]]></category>
		<category><![CDATA[Padding]]></category>
		<category><![CDATA[TextField]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=1123</guid>

					<description><![CDATA[TextFieldのPaddingをModifierで指定しても背景のViewのPaddingが変更できません TextFieldの実装を見るとわかるのですが内部でBasicTextFieldを使用していて背景のView [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>TextFieldのPaddingをModifierで指定しても背景のViewのPaddingが変更できません</p>



<p>TextFieldの実装を見るとわかるのですが内部でBasicTextFieldを使用していて背景のViewはDecorationBoxで実装されています</p>



<p>Paddingを調整する方法は以下があります<br>・背景色とアンダーラインを透明にしてPaddingの16.dpを活かして配置する<br>　※ paddingは変更できないがカーソルの描画領域のスペースも確保すべきなので見た目で対処<br>・BasicTextFieldで実装しdecorationBoxを独自に実装<br>・BasicTextFieldで実装しTextFieldDefaults.DecorationBoxのcontentPaddingで指定</p>



<p>TextFieldのまま/上記３点の実装した場合は以下のようになります<br>※ BasicTextFieldの配置は左に16.dpのPaddingを指定してます</p>



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



<div style="height:var(--wp--preset--spacing--50)" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><span id="toc1">背景色とアンダーラインを透明</span></h3>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>var text by remember { mutableStateOf(&quot;テキスト&quot;) }
TextField(
    value = text,
    onValueChange = { text = it },
    colors = TextFieldDefaults.colors(
        focusedContainerColor = Color.Transparent,
        unfocusedContainerColor = Color.Transparent,
        focusedIndicatorColor = Color.Transparent,
        unfocusedIndicatorColor = Color.Transparent,
    )
)</code></pre></div>



<h3 class="wp-block-heading"><span id="toc2">BasicTextFieldで実装しdecorationBoxを独自に実装</span></h3>



<p>DecorationBox相当の装飾は下線のみ実装</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>val drawLine = true
BasicTextField(
    value = text,
    onValueChange = { text = it },
    textStyle = LocalTextStyle.current.copy(
        color = MaterialTheme.colorScheme.onSurface
    ),
) { innerTextField -&gt;
    Column {
        innerTextField()
        if (drawLine) {
            Spacer(Modifier.height(1.dp))
            Divider(thickness = 1.dp, color = MaterialTheme.colorScheme.onSurface)
        }
    }
}</code></pre></div>



<h3 class="wp-block-heading"><span id="toc3">BasicTextFieldで実装しTextFieldDefaults.DecorationBoxのcontentPaddingで指定</span></h3>



<p>DecorationBoxのshapeが目立つので四角に変更してます</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>val enabled = true
val singleLine = true
val visualTransformation = VisualTransformation.None
val interactionSource = remember { MutableInteractionSource() }
BasicTextField(
    value = text,
    onValueChange = { text = it },
    textStyle = LocalTextStyle.current.copy(
        color = MaterialTheme.colorScheme.onSurface
    ),
    enabled = enabled,
    singleLine = singleLine,
    visualTransformation = visualTransformation,
    interactionSource = interactionSource,
) { innerTextField -&gt;
    TextFieldDefaults.DecorationBox(
        value = text,
        innerTextField = innerTextField,
        enabled = enabled,
        singleLine = singleLine,
        visualTransformation = VisualTransformation.None,
        interactionSource = interactionSource,
        shape = RectangleShape, // TextFieldDefaults.shape
        contentPadding = PaddingValues(0.dp)
    )
}</code></pre></div>



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



<p class="has-text-align-right">Android Studio Giraffe 2022.3.1 built on June 29, 2023</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 036. Pager</title>
		<link>https://bps-e.com/dev/android-003-036/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Thu, 28 Sep 2023 07:06:40 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Giraffe]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Material 3]]></category>
		<category><![CDATA[未分類]]></category>
		<category><![CDATA[HorizontalPager]]></category>
		<category><![CDATA[VerticalPage]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=1096</guid>

					<description><![CDATA[androidx.compose.foundation1.4.0でHorizontalPager/VerticalPagerが追加されていましたhttps://developer.android.com/jetpack/ [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>androidx.compose.foundation1.4.0でHorizontalPager/VerticalPagerが追加されていました<br><a rel="noopener" href="https://developer.android.com/jetpack/androidx/releases/compose-foundation?authuser=1" target="_blank">https://developer.android.com/jetpack/androidx/releases/compose-foundation?authuser=1</a><br>ひさびさにデベロッパーの説明だけで簡単に実装できます<br>※ AccompanistのPagerを使う必要はありません</p>



<p>デベロッパーの説明<br><a rel="noopener" href="https://developer.android.com/jetpack/compose/layouts/pager" target="_blank">https://developer.android.com/jetpack/compose/layouts/pager</a></p>



<p>ちょっとした罠ではじめIgnoreで確認してたのですが、Ignoreは何故かandroidx.compose.foundationが標準で取り込まれていないようでHorizontalPager/VerticalPagerが出てこなかったです<br>Giraffeでは何も設定しなくても使用できます<br>※ compose-bomは最新にしてください</p>



<p>デベロッパーの内容だけだと面白くないのでAndroidアプリのGoogle ToDoみたくTabと組み合わせてみます</p>



<p>ページ数の変更はPageStateのpageCountで変更できなくてBardさんに嘘つかれたりしながら試行錯誤したけど単純に以下な実装で問題なし<br>あとタブからのページ遷移のアニメーションが早すぎなので調整してみました</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PagerScreen(modifier: Modifier = Modifier) {
    val coroutineScope = rememberCoroutineScope()
    var tabIndex by remember { mutableStateOf(0) }
    val category = remember { mutableStateListOf&lt;String&gt;() }
    var pagerState = rememberPagerState(pageCount = {
        if (category.size == 0) 1
        else category.size
    })

    LaunchedEffect(pagerState) {
        snapshotFlow { pagerState.currentPage }.collect { page -&gt;
            tabIndex = page
        }
    }

    Column(modifier) {
        ScrollableTabRow(selectedTabIndex = tabIndex) {
            category.forEachIndexed { index, title -&gt;
                LeadingIconTab(
                    selected = tabIndex == index,
                    onClick = {
                        tabIndex = index
                        coroutineScope.launch {
                            //pagerState.scrollToPage(index)
                            pagerState.animateScrollToPage(index, animationSpec = tween(400))
                        }
                    },
                    text = {
                        val max = LocalConfiguration.current.screenWidthDp.dp * (1F / 3F)
                        Text(title, overflow = TextOverflow.Ellipsis, maxLines = 1, modifier = Modifier.widthIn(max = max))
                    },
                    icon = {}
                )
            }
            // 固定Tab
            LeadingIconTab(
                selected = false,
                onClick = {
                    category.add(&quot;ページ ${category.size + 1}&quot;)
                },
                text = { Text(&quot;＋ タブ追加&quot;) },
                icon = {}
            )
        }

        HorizontalPager(
            state = pagerState,
        ) { page -&gt;
            if (category.size == 0) {
                Text(&quot;まだ何も登録されていません&quot;, modifier = Modifier.fillMaxSize())
            }
            else {
                Text(&quot;${category[page]}&quot;, modifier = Modifier.fillMaxSize())
            }
        }
    }
}</code></pre></div>



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



<p class="has-text-align-right">Android Studio Giraffe 2022.3.1 built on June 29, 2023</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 035. イベント消費型UIイベント(StateFlow)</title>
		<link>https://bps-e.com/dev/android-003-035/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Wed, 27 Sep 2023 04:46:21 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Giraffe]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[StateFlow]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=1089</guid>

					<description><![CDATA[発生したイベントをキューに貯めて順次処理して行きたい場合でViewModelを使用時の使えそうな実装パターンとしてイベント消費型UIイベントがあります デベロッパーの説明だけでは足らない気もするので対応してみます以下のサ [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>発生したイベントをキューに貯めて順次処理して行きたい場合でViewModelを使用時の使えそうな実装パターンとしてイベント消費型UIイベントがあります</p>



<p>デベロッパーの説明だけでは足らない気もするので対応してみます<br>以下のサイトの内容の改造版みたいな感じです<br><a rel="noopener" href="https://star-zero.medium.com/viewmodel%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%81%AE%E5%AE%9F%E8%A3%85-74dd814deb97" target="_blank">https://star-zero.medium.com/viewmodel%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%81%AE%E5%AE%9F%E8%A3%85-74dd814deb97</a></p>



<p>デベロッパーの説明<br><a href="https://developer.android.com/topic/architecture/ui-layer/events?hl=ja&amp;authuser=1#consuming-trigger-updates">https://developer.android.com/topic/architecture/ui-layer/events?hl=ja&amp;authuser=1#consuming-trigger-updates</a></p>



<div style="height:var(--wp--preset--spacing--50)" aria-hidden="true" class="wp-block-spacer"></div>



<p>Eventクラスは消費時に判別するためのユニークなIDをつけます<br>ダミーのイベント発生のstubとイベント消費のためのconsumeEventを実装します</p>



<p>ダミーのイベントはテストのため連続で５つ発生するようにしてます</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>@Stable
sealed class Event {
    val id = UUID.randomUUID().toString()
    data class Message(val message: String) : Event()
}

class AppViewModel : ViewModel() {
    private val _uiEvents = MutableStateFlow&lt;List&lt;Event&gt;&gt;(emptyList())
    val uiEvents = _uiEvents.asStateFlow()

    fun stub() = viewModelScope.launch {
        val message = listOf(
            &quot;おはよう&quot;,
            &quot;こんにちわ&quot;,
            &quot;こんばんわ&quot;,
        )

        repeat(5) {
            val msg = message.random()
            _uiEvents.update {
                it + Event.Message(msg)
            }
        }
    }

    fun consumeEvent(event: Event) {
        _uiEvents.update {
            e -&gt; e.filterNot { it.id == event.id }
        }
    }
}</code></pre></div>



<p>LaunchedEffectでuiEventsに変化(追加/消費)が発生した時に最初に追加されたイベントを1つ取得して処理していきます<br>ここではイベント発生後の処理の完了を待つ変わりにdelayしてます</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>@Composable
fun AppContent(viewModel: AppViewModel = viewModel()) {
    val uiEvents by viewModel.uiEvents.collectAsState()
    var message by remember { mutableStateOf(&quot;&quot;) }

    LaunchedEffect(uiEvents) {
        if (uiEvents.isNotEmpty()) {
            when (val event = uiEvents.first()) {
                is Event.Message -&gt; {
                    message = &quot;${event.message}(${event.id})&quot;
                    Log.d(&quot;App&quot;, message)
                    launch {
                        delay(1_000)
                        viewModel.consumeEvent(event)
                    }
                }
            }
        }
    }

    Column {
        Button(onClick = { viewModel.stub() }) {
            Text(&quot;Start&quot;)
        }
        Text(message)
    }
}</code></pre></div>



<div style="height:var(--wp--preset--spacing--50)" aria-hidden="true" class="wp-block-spacer"></div>



<p>古典的なやり方だとキュークラス実装したり再起処理実装したりとごちゃごちゃやってたけど簡単にスッキリとした実装となりました</p>



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



<p class="has-text-align-right">Android Studio Giraffe 2022.3.1 built on June 29, 2023</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 034. App Quality Insights(Firebase Crashlytics)</title>
		<link>https://bps-e.com/dev/android-003-034/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Thu, 21 Sep 2023 20:20:28 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Giraffe]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[App Quality Insights]]></category>
		<category><![CDATA[Firebase Crashlytics]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=999</guid>

					<description><![CDATA[Android Studioから使えと暗黙のメッセージが出てるような気のするApp Quality Insightsを使ってみます App Quality Insightsは昨年のElectric Eelで追加された機能 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Android Studioから使えと暗黙のメッセージが出てるような気のするApp Quality Insightsを使ってみます</p>



<figure class="wp-block-image size-full"><img decoding="async" width="209" height="239" src="https://bps-e.com/dev/wp-content/uploads/2023/09/image-3.png" alt="" class="wp-image-1000"/></figure>



<div style="height:var(--wp--preset--spacing--50)" aria-hidden="true" class="wp-block-spacer"></div>



<p>App Quality Insightsは昨年のElectric Eelで追加された機能でFirebase Crashlyticsを使ってスタックトレースを見る機能です</p>



<p>App Quality Insightsのdeveloperの説明は以下<br><a rel="noopener" href="https://developer.android.com/studio/debug/app-quality-insights?hl=ja" target="_blank">https://developer.android.com/studio/debug/app-quality-insights?hl=ja</a></p>



<p>今回はAndroid StudioからFirebase Crashlyticsを追加してApp Quality Insightsで確認の流れです</p>



<p>※ IguanaでPlay ConsoleのAndroid Vitalsで収集された内容もApp Quality Insightsで確認できるようになるそうです<br>Firebase Crashlyticsを使用する必要がなくなるのではなく両方使うとより不具合の調査がしやすくなるようです<br><a rel="noopener" href="https://developer.android.com/distribute/best-practices/launch/debug-crashes?hl=ja&amp;authuser=1" target="_blank">https://developer.android.com/distribute/best-practices/launch/debug-crashes?hl=ja&amp;authuser=1</a></p>



<h2 class="wp-block-heading"><span id="toc1">事前準備</span></h2>



<p>開発用のGoogleアカウントを用意しときます<br>これはFirebaseで使います<br>ついでにそのアカウントでAndroid StudioにSign inしておきましょう<br>またChromeを既定のブラウザにしてそのアカウントでChromeにログインしておくと作業がスムーズです</p>



<h2 class="wp-block-heading"><span id="toc2">Firebase設定</span></h2>



<p>Firebaseにプロジェクトの追加や依存関係設定などはFirebaseのドキュメントでは手動が推奨されてますがAndroid StudioのFirebase Assistantで設定していきます<br><a rel="noopener" href="https://firebase.google.com/docs/android/setup?authuser=0&amp;hl=ja" target="_blank">https://firebase.google.com/docs/android/setup?authuser=0&amp;hl=ja</a></p>



<p>Android StudioのメニューのTool &#8211; Firebaseを実行してCrashlyticsのGet started with Firebase Crashlyticsを選択します</p>



<figure class="wp-block-image size-full is-resized"><img fetchpriority="high" decoding="async" src="https://bps-e.com/dev/wp-content/uploads/2023/09/image-5.png" alt="" class="wp-image-1002" style="object-fit:cover;aspect-ratio:400/634" width="400" height="634"/></figure>



<div style="height:var(--wp--preset--spacing--50)" aria-hidden="true" class="wp-block-spacer"></div>



<p>あとはステップの内容にそって進めていきます</p>



<figure class="wp-block-image size-large is-resized"><img decoding="async" src="https://bps-e.com/dev/wp-content/uploads/2023/09/image-7-485x1024.png" alt="" class="wp-image-1004" style="width:400px" width="400" srcset="https://bps-e.com/dev/wp-content/uploads/2023/09/image-7-485x1024.png 485w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-7-142x300.png 142w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-7.png 554w" sizes="(max-width: 485px) 100vw, 485px" /></figure>



<div style="height:var(--wp--preset--spacing--50)" aria-hidden="true" class="wp-block-spacer"></div>



<p>① Connect to Firebase</p>



<p>Firebaseのログインが開発用のアカウントであることを確認してからプロジェクトを作成ボタンを押します<br>Chromeのログインユーザでは無いので注意してください</p>



<figure class="wp-block-image size-full is-resized"><img decoding="async" src="https://bps-e.com/dev/wp-content/uploads/2023/09/image-9.png" alt="" class="wp-image-1006" style="width:400px" width="400" srcset="https://bps-e.com/dev/wp-content/uploads/2023/09/image-9.png 923w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-9-300x283.png 300w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-9-768x725.png 768w" sizes="(max-width: 923px) 100vw, 923px" /></figure>



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



<p>Firebaseのプロジェクト名はAndroid Studioのプロジェクト名と同じものが表示されていると思うので必要なら変えてから続行します</p>



<p>次のステップでこのプロジェクトでGoogleアナリティクスを有効にするの設定がでますが、今回はCrashlytics単体を試すので無効にして続行を押します</p>



<p>新しいプロジェクトの準備ができましたが表示されたら続行を押します</p>



<p>Firebase Android アプリがFirebaseに作成されました。が表示されたら接続を押します</p>



<h2 class="wp-block-heading"><span id="toc3">Android Studio側での設定</span></h2>



<p>②Add the Crashlytics SDK and plugin to your app</p>



<p>ボタンを押すだけで自動で依存関係の設定をしてくれます<br>google-services.jsonも自動で配置してくれるので楽です<br>ただlibs.version.tomlには対応してないので必要なら手動で設定してください</p>



<p>③Force a test crash to finish setup</p>



<p>ここではJetpack Compose環境なのでサンプルコードを参考に以下を実装してクラッシュを発生させます</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>Column {
    Button(onClick = { throw RuntimeException(&quot;Test Crash&quot;) }) {
        Text(&quot;Test Crash&quot;)
    }
}</code></pre></div>



<h2 class="wp-block-heading"><span id="toc4">App Quality Insightsで確認</span></h2>



<p>メニュー一番右のLast refreshedを実行するとクラッシュ内容が表示されています</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="271" src="https://bps-e.com/dev/wp-content/uploads/2023/09/image-10-1024x271.png" alt="" class="wp-image-1008" srcset="https://bps-e.com/dev/wp-content/uploads/2023/09/image-10-1024x271.png 1024w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-10-300x79.png 300w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-10-768x203.png 768w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-10-1536x406.png 1536w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-10.png 1597w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Stack Traceの上のView on FirebaseのリンクからFirebase上での内容も確認できます</p>



<figure class="wp-block-image size-large is-resized"><img decoding="async" src="https://bps-e.com/dev/wp-content/uploads/2023/09/image-11-1024x992.png" alt="" class="wp-image-1009" style="width:400px" width="400" srcset="https://bps-e.com/dev/wp-content/uploads/2023/09/image-11-1024x992.png 1024w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-11-300x291.png 300w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-11-768x744.png 768w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-11.png 1137w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



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



<p class="has-text-align-right">Android Studio Giraffe 2022.3.1 Patch 1 built on August 17, 2023</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 033. ポケモン図鑑アプリ(PokeApi)</title>
		<link>https://bps-e.com/dev/android-003-033/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Fri, 15 Sep 2023 14:13:01 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Giraffe]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Material 3]]></category>
		<category><![CDATA[Paging3]]></category>
		<category><![CDATA[PokeApi]]></category>
		<category><![CDATA[RemoteMediator]]></category>
		<category><![CDATA[Room]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=978</guid>

					<description><![CDATA[RemoteMediator(Paging)の流れで実装してみました最適な実装のアプリではなくあくまでRemoteMediator+Pagingを使用した実装の検証的なものです ポケモンやってないのであんま詳しくないけど [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p><a href="https://bps-e.com/dev/android-003-031/" data-type="post" data-id="973">RemoteMediator(Paging)</a>の流れで実装してみました<br>最適な実装のアプリではなくあくまでRemoteMediator+Pagingを使用した実装の検証的なものです</p>



<p>ポケモンやってないのであんま詳しくないけど追加ダウンロードコンテンツ発売前後の最悪のタイミングで、PokeApiに追加データがどのような形で実装されるかわからないので以降正常に動くかはわからないです</p>



<p>アプリの基本構成は<a href="https://bps-e.com/dev/android-003-031/" data-type="post" data-id="973">RemoteMediator(Paging)</a>で必要な機能を追加していく形です</p>



<p>API仕様が変わったのかエラーがでるので、一旦非公開にしました<br><s>ソースは以下</s><br><a rel="noopener" href="https://github.com/bps-e/Pokepedia" target="_blank"><s>https://github.com/bps-e/Pokepedia</s></a></p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="473" height="1024" src="https://bps-e.com/dev/wp-content/uploads/2023/09/image-2-473x1024.png" alt="" class="wp-image-991" style="width:289px;height:626px" srcset="https://bps-e.com/dev/wp-content/uploads/2023/09/image-2-473x1024.png 473w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-2-138x300.png 138w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-2-768x1664.png 768w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-2-709x1536.png 709w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-2-945x2048.png 945w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-2.png 1080w" sizes="(max-width: 473px) 100vw, 473px" /></figure>



<h2 class="wp-block-heading">PokeApiの実行の流れ</h2>



<p>基本的にはpokemonでポケモンのリストと詳細urlを取得してデータに紐づいたurlを辿ってデータを取得していきます</p>



<p>pokemon -&gt; pokemon/(name) -&gt; pokemon-species/(name)</p>



<p>type(くさ/どく&#8230;)、ability(しんりょく/せいでんき&#8230;)、stat(HP/こうげき&#8230;)、move(技)などの言語別表記などの詳細は別途APIの実行が必要です<br>※genus(ねずみポケモン/こねずみポケモン&#8230;)はポケモンに紐づいているデータなので言語別表記はpokemon-speciesに含まれています</p>



<p>全データ取得しようとしたら膨大なリクエスト数となってしまうので必要なタイミングで必要なだけが良いと思います</p>



<p>リストはレコード数指定して取得できるけどリクエスト回数を減らすため一括の方がよいかもしれません</p>



<p>まあそもそもRemoteMediator+Pagingを使用してリクエストを送ってるのでスクロールしないとデータが得れなく最近のポケモンを見たい場合は絶望&#8230;のダメな仕様<br>実際にちゃんとしたアプリを作る場合は別途データをダウンロードしておいてアプリにdbプリインしとくが正解ですがそれでは企画倒れ</p>



<p>データ構造もおおよそPokeApiに準拠してちょっと複雑な処理になってますがPagingでシンプルにデータをUIに渡したいのであればUIに渡すデータ(テーブル)に日本語に変換済みのデータを保存してUIでの処理を減らす方向の方がよいと思います(今回はあえて日本語変換をUI側で実装した場合を試してます)</p>



<p>画像の表示はurlから取得したものを使用するためCoilを使用してます</p>



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



<p class="has-text-align-right">Android Studio Giraffe 2022.3.1 Patch 1 built on August 17, 2023</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 032. Room(DB) Relation vs TypeConverter</title>
		<link>https://bps-e.com/dev/android-003-032/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Thu, 14 Sep 2023 01:39:03 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Giraffe]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Relation]]></category>
		<category><![CDATA[Room]]></category>
		<category><![CDATA[TypeConverter]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=983</guid>

					<description><![CDATA[List&#60;T>やMap&#60;T>はDBの値として直接格納できないのでどのように対応するか的な話です List&#60;T>やMap&#60;T>をJsonの文字列に変換してStringとして保存する方法と個別にDBの [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>List&lt;T>やMap&lt;T>はDBの値として直接格納できないのでどのように対応するか的な話です</p>



<p>List&lt;T>やMap&lt;T>をJsonの文字列に変換してStringとして保存する方法と<br>個別にDBのTableを作ってRelationで連結して取得する方法があると思います</p>



<p>前者は実装が楽だけどデータ構造によってはデータ量が膨れ上がる、後者は実装量が増えてめんどくさいけどDBでよくある方法です</p>



<p>TypeConverterの説明は以下<br><a href="https://developer.android.com/training/data-storage/room/referencing-data?hl=ja">https://developer.android.com/training/data-storage/room/referencing-data?hl=ja</a></p>



<p>RoomでRelationのdeveloperの説明は以下<br><a rel="noopener" href="https://developer.android.com/training/data-storage/room/relationships?hl=ja" target="_blank">https://developer.android.com/training/data-storage/room/relationships?hl=ja</a></p>



<p>PokeApiのStat(HP/こうげき&#8230;などの名称データ)を例に見ていきます<br>Statの内容はkeyとなるnameのリストとid/言語ごとのnameのリストなどの詳細データです</p>



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



<p>RoomDBを定義時にTypeConverterを指定するだけです<br>Jsonはkotlinx.serialization.Jsonを使ってます</p>



<p>※ListTypeConverterは使わないけどついでに定義</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>@Entity(tableName = &quot;stat&quot;)
data class Stat(
    @PrimaryKey val id: Int,
    val name: String,
    // language, name
    val names: Map&lt;String, String&gt;,
)
@Dao
interface StatDao {
    @Query(&quot;SELECT * FROM stat&quot;)
    override fun getAll(): Flow&lt;List&lt;Stat&gt;&gt;
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(list: List&lt;Stat&gt;)
}

class ListTypeConverter {
    @TypeConverter
    fun fromList(list: List&lt;String&gt;): String {
        return Json.encodeToString(list)
    }
    @TypeConverter
    fun toList(str: String): List&lt;String&gt; {
        return Json.decodeFromString&lt;List&lt;String&gt;&gt;(str)
    }
}
class MapTypeConverter {
    @TypeConverter
    fun fromMap(map: Map&lt;String, String&gt;): String {
        return Json.encodeToString(map)
    }
    @TypeConverter
    fun toMap(str: String): Map&lt;String, String&gt; {
        return Json.decodeFromString&lt;Map&lt;String, String&gt;&gt;(str)
    }
}

@Database(entities = [Stat::class], version = 1, exportSchema = false)
@TypeConverters(ListTypeConverter::class, MapTypeConverter::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun dao(): StatDao
}</code></pre></div>



<h2 class="wp-block-heading"><span id="toc2">Relation</span></h2>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>@Entity(tableName = &quot;stat_name&quot;)
data class StatName(
    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = &quot;id&quot;) val count: Int = 0,
    @ColumnInfo(name = &quot;name_id&quot;) val id: Int,
    val language: String,
    val name: String,
)
@Entity(tableName = &quot;stat&quot;)
data class StatEntry(
    @PrimaryKey val id: Int,
    val name: String,
)

data class Stat(
    @Embedded val stat: StatEntry,
    @Relation(
        parentColumn = &quot;id&quot;,
        entityColumn = &quot;name_id&quot;,
        entity = StatName::class
    )
    val names: List&lt;StatName&gt;,
)

@Dao
interface StatNameDao {
    @Transaction
    @Query(&quot;SELECT * FROM stat_name&quot;)
    override fun getAll(): Flow&lt;List&lt;StatName&gt;&gt;
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(list: List&lt;StatName&gt;)
}
@Dao
interface StatDao {
    @Query(&quot;SELECT * FROM stat&quot;)
    override fun getAll(): Flow&lt;List&lt;Stat&gt;&gt;
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(list: List&lt;StatEntry&gt;)
}

@Database(entities = [Stat::class, StatName::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun stadDao(): StatDao
    abstract fun stadNameDao(): StatNameDao
}</code></pre></div>



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



<p class="has-text-align-right">Android Studio Giraffe 2022.3.1 Patch 1 built on August 17, 2023</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 031. RemoteMediator(Paging)</title>
		<link>https://bps-e.com/dev/android-003-031/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Tue, 12 Sep 2023 04:33:16 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Giraffe]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Material 3]]></category>
		<category><![CDATA[Hilt]]></category>
		<category><![CDATA[Paging3]]></category>
		<category><![CDATA[RemoteMediator]]></category>
		<category><![CDATA[Room]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=973</guid>

					<description><![CDATA[PagingするたびローカルDBにデータがなかったらWebApiで取得してローカルDBに保存する処理をRemoteMediatorを使って実装してみました 使い方は簡単でPager作成時にRemoteMediatorを指 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>PagingするたびローカルDBにデータがなかったらWebApiで取得してローカルDBに保存する処理をRemoteMediatorを使って実装してみました</p>



<p>使い方は簡単でPager作成時にRemoteMediatorを指定するだけです</p>



<p>developerの説明は以下<br><a rel="noopener" href="https://developer.android.com/topic/libraries/architecture/paging/v3-network-db?hl=ja" target="_blank">https://developer.android.com/topic/libraries/architecture/paging/v3-network-db?hl=ja</a></p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" src="https://bps-e.com/dev/wp-content/uploads/2023/09/image-1.png" alt="" class="wp-image-974" style="aspect-ratio:562/306" width="562" height="306" srcset="https://bps-e.com/dev/wp-content/uploads/2023/09/image-1.png 739w, https://bps-e.com/dev/wp-content/uploads/2023/09/image-1-300x164.png 300w" sizes="(max-width: 562px) 100vw, 562px" /></figure>



<div style="height:var(--wp--preset--spacing--50)" aria-hidden="true" class="wp-block-spacer"></div>



<p>まずは最もシンプルな構成で実装してみます<br>※ 図のPagingDataAdapterは実装しません</p>



<p>処理の見通しをわかりやすくするためPokeApiのポケモンリストだけを取得する内容としてます<br>そこそこの図鑑の実装は別途やります</p>



<h2 class="wp-block-heading"><span id="toc1">依存関係の設定</span></h2>



<p>WebApiは<a href="https://bps-e.com/dev/android-003-030/">PokeApi</a>を使用します<br>以下からPokeApiとHttpClientの.aarをダウンロードして(project)/app/libsに置いてください</p>



<p><a rel="noopener" href="https://github.com/bps-e/PokeApi/releases" target="_blank">https://github.com/bps-e/PokeApi/releases</a><br><a href="https://github.com/bps-e/HttpClient/releases">https://github.com/bps-e/HttpClient/releases</a></p>



<p>今回はJetPack Compose + Hilt + Room + Paging3の構成でkotlinx-serialization.Jsonを使用するので以下を設定します<br>またHiltはα版ですがkspで使用します</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-plain" data-file="lib.versions.toml"><code>[versions]
com-google-devtools-ksp = &quot;1.9.0-1.0.13&quot;
androidx-room = &quot;2.6.0-beta01&quot;
androidx-paging = &quot;3.2.0&quot;
dagger-hilt = &quot;2.48&quot;

[libraries]
kotlinx-serialization-json = { group = &quot;org.jetbrains.kotlinx&quot;, name = &quot;kotlinx-serialization-json&quot;, version = &quot;1.6.0&quot; }
androidx-room-ktx = { group = &quot;androidx.room&quot;, name = &quot;room-ktx&quot;, version.ref = &quot;androidx-room&quot; }
androidx-room-runtime = { group = &quot;androidx.room&quot;, name = &quot;room-runtime&quot;, version.ref = &quot;androidx-room&quot; }
androidx-room-compiler = { group = &quot;androidx.room&quot;, name = &quot;room-compiler&quot;, version.ref = &quot;androidx-room&quot; }
androidx-room-paging = { group = &quot;androidx.room&quot;, name = &quot;room-paging&quot;, version.ref = &quot;androidx-room&quot; }
androidx-paging-runtime-ktx = { group = &quot;androidx.paging&quot;, name = &quot;paging-runtime-ktx&quot;, version.ref = &quot;androidx-paging&quot; }
androidx-paging-compose = { group = &quot;androidx.paging&quot;, name = &quot;paging-compose&quot;, version.ref = &quot;androidx-paging&quot; }
hilt-android = { group = &quot;com.google.dagger&quot;, name = &quot;hilt-android&quot;, version.ref = &quot;dagger-hilt&quot; }
hilt-compiler = { group = &quot;com.google.dagger&quot;, name = &quot;hilt-compiler&quot;, version.ref = &quot;dagger-hilt&quot; }
androidx-hilt-navigation-compose = { group = &quot;androidx.hilt&quot;, name = &quot;hilt-navigation-compose&quot;, version = &quot;1.1.0-alpha01&quot; }

[plugins]
com-google-devtools-ksp = {id = &quot;com.google.devtools.ksp&quot;, version.ref = &quot;com-google-devtools-ksp&quot; }
com-google-dagger-hilt-android = { id = &quot;com.google.dagger.hilt.android&quot;, version.ref = &quot;dagger-hilt&quot; }</code></pre></div>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-file="build.gradle.kts(Project)" data-lang="Kotlin"><code>plugins {
    kotlin(&quot;plugin.serialization&quot;) version &quot;1.9.0&quot;
    alias(libs.plugins.com.google.devtools.ksp) apply false
    alias(libs.plugins.com.google.dagger.hilt.android) apply false
}</code></pre></div>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-file="build.gradle.kts(Module)" data-lang="Kotlin"><code>plugins {
    id(&quot;kotlinx-serialization&quot;)
    alias(libs.plugins.com.google.devtools.ksp)
    alias(libs.plugins.com.google.dagger.hilt.android)
}

dependencies {
    implementation(libs.kotlinx.serialization.json)
    implementation(libs.androidx.room.ktx)
    implementation(libs.androidx.room.runtime)
    implementation(libs.androidx.room.paging)
    implementation(libs.androidx.paging.runtime.ktx)
    implementation(libs.androidx.paging.compose)
    implementation(libs.hilt.android)
    ksp(libs.hilt.compiler)
    implementation(libs.androidx.hilt.navigation.compose)
    ksp(libs.androidx.room.compiler)
    implementation(fileTree(mapOf(&quot;dir&quot; to &quot;libs&quot;, &quot;include&quot; to listOf(&quot;*.jar&quot;, &quot;*.aar&quot;))))
}</code></pre></div>



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



<p>まずはApplicationクラス作ってHiltを有効にする</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>/**
 * ```AndroidManifest.xml
 * &lt;application
 *     android:name=&quot;.Application&quot;
 * ```
 */
@HiltAndroidApp
class Application : android.app.Application() {
    override fun onCreate() {
        super.onCreate()
    }
}</code></pre></div>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    ...
}</code></pre></div>



<p>Roomまわり</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>const val TABLE_POKEMON = &quot;pokemon&quot;
@Entity(tableName = TABLE_POKEMON)
data class Pokemon(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val name: String,
)

@Dao
interface PokemonDao {
    @Query(&quot;SELECT * FROM $TABLE_POKEMON&quot;)
    fun pagingSource(): PagingSource&lt;Int, Pokemon&gt;
    @Query(&quot;SELECT COUNT(*) FROM $TABLE_POKEMON&quot;)
    suspend fun count(): Long
    @Query(&quot;DELETE FROM $TABLE_POKEMON&quot;)
    suspend fun deleteAll()
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(data: Pokemon)
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(list: List&lt;Pokemon&gt;)
}

@Database(entities = [Pokemon::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun dao(): PokemonDao
}

class PokemonRepository @Inject constructor(
    private val pager: Pager&lt;Int, Pokemon&gt;,
) {
    fun getPagingStream() = pager.flow
}</code></pre></div>



<p>RemoteMediator</p>



<p>MediatorResult.Success(true)を指定すると読み込みを終了します<br>ここではdbのサイズがPokeApiのcount以上が終了条件です</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>const val PAGE_SIZE = 20

@OptIn(ExperimentalPagingApi::class)
class PokemonRemoteMediator @Inject constructor(
    private val db: AppDatabase,
) : RemoteMediator&lt;Int, Pokemon&gt;() {
    override suspend fun initialize(): InitializeAction {
        return InitializeAction.SKIP_INITIAL_REFRESH
    }

    override suspend fun load(
        loadType: LoadType,
        state: PagingState&lt;Int, Pokemon&gt;,
    ): MediatorResult {
        return try {
            val dao = db.dao()
            val count = dao.count()
            val offset = when (loadType) {
                LoadType.REFRESH -&gt; if (count == 0L) 0L else return MediatorResult.Success(true)
                LoadType.PREPEND -&gt; return MediatorResult.Success(true)
                LoadType.APPEND -&gt; count
            }

            var poke_count = 0
            val result = mutableListOf&lt;Pokemon&gt;()
            PokeApi.Pokemon(limit = PAGE_SIZE, offset = offset.toInt(), onError = {
                Log.e(&quot;App&quot;, it.localizedMessage ?: &quot;&quot;)
            }) { poke -&gt;
                poke_count = poke.count
                result.addAll(poke.results.map {
                    Pokemon(name = it.name)
                })
            }

            if (result.isNotEmpty()) {
                db.withTransaction {
                    dao.insert(result)
                }
            }

            return MediatorResult.Success(poke_count &lt; count + PAGE_SIZE)
        }
        catch (e: Exception) {
            MediatorResult.Error(e)
        }
    }
}</code></pre></div>



<p>DIまわり</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>@Module
@InstallIn(ViewModelComponent::class)
object PokemonModule {
    @Provides
    fun provideDatabase(
        @ApplicationContext context: Context
    ) = Room.databaseBuilder(context, AppDatabase::class.java, &quot;app.db&quot;).build()

    @Provides
    fun provideDao(db: AppDatabase) = db.dao()

    @OptIn(ExperimentalPagingApi::class)
    @Provides
    fun providePager(
        db: AppDatabase,
    ): Pager&lt;Int, Pokemon&gt; {
        return Pager(
            config = PagingConfig(pageSize = PAGE_SIZE),
            remoteMediator = PokemonRemoteMediator(
                db = db
            ),
            pagingSourceFactory = {
                db.dao().pagingSource()
            },
        )
    }

    @Provides
    fun provideRepository(pager: Pager&lt;Int, Pokemon&gt;) = PokemonRepository(pager)
}</code></pre></div>



<p>UIまわり</p>



<p>リストを下にスクロールしていくとRemoteMediatorが実行されてリストが追加されていきます</p>



<div class="hcb_wrap"><pre class="prism line-numbers lang-kt" data-lang="Kotlin"><code>@HiltViewModel
class PokeViewModel @Inject constructor(
    private val repo: PokemonRepository,
) : ViewModel() {
    val pokemon = repo.getPagingStream().cachedIn(viewModelScope)
}

@Composable
fun AppScreen(
    modifier: Modifier = Modifier,
    viewModel: PokeViewModel = hiltViewModel(),
) {
    val items = viewModel.pokemon.collectAsLazyPagingItems()

    LazyColumn(modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
        items(
            count = items.itemCount,
            key = items.itemKey { it.id },
        ) { index -&gt;
            val poke = items[index]
            poke?.apply {
                ListItem(headlineContent = {
                    Text(&quot;$index. $name&quot;)
                })
                Divider(color = Color.LightGray)
            }
        }
    }
}</code></pre></div>



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



<p class="has-text-align-right">Android Studio Giraffe 2022.3.1 Patch 1 built on August 17, 2023</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[Android] 030. PokeApi(pokeapi.co)</title>
		<link>https://bps-e.com/dev/android-003-030/</link>
		
		<dc:creator><![CDATA[bps-e]]></dc:creator>
		<pubDate>Sun, 10 Sep 2023 07:20:01 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Giraffe]]></category>
		<category><![CDATA[Jetpack Compose]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[Material 3]]></category>
		<category><![CDATA[PokeApi(pokeapi.co)]]></category>
		<guid isPermaLink="false">https://bps-e.com/dev/?p=966</guid>

					<description><![CDATA[WebApiを使用してサンプルを作成する時にPokeApi(https://pokeapi.co/)を使うのが流行ってる？っぽいので作ってみました API仕様が変わったのかエラーがでるので、一旦非公開にしました作ったもの [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>WebApiを使用してサンプルを作成する時にPokeApi(<a rel="noopener" href="https://pokeapi.co/" target="_blank">https://pokeapi.co/</a>)を使うのが流行ってる？っぽいので作ってみました</p>



<p>API仕様が変わったのかエラーがでるので、一旦非公開にしました<br><s>作ったものはAndroid LibraryにしてGitHubにあげておきました<br><a rel="noopener" href="https://github.com/bps-e/PokeApi" target="_blank">https://github.com/bps-e/PokeApi</a></s></p>



<p>内容は以前作った<a href="https://bps-e.com/dev/android-003-014/" data-type="link" data-id="https://bps-e.com/dev/android-003-014/">HttpClient</a>をちょっと拡張したものでPokeApiにリクエストを送ってkotlinx-serialization-jsonでパースしたものを返すです</p>



<p>内容は大したことないですがJsonの定義と定義の確認がすごく面倒だったです<br>というわけで全部実装はしんどいので最低限のものに対応してます(pokemon/pokemon-species/ability/type/move/stat)</p>



<p>PokeApiの使い方は用途にもよりますが基本は実行の流れの基本は対象データのリストを取得して各項目の配列数かキーとurlの組み合わせを使って詳細を取得するです</p>



<p>簡単な動作確認はテストプロジェクトでできます</p>



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



<p class="has-text-align-right">Android Studio Giraffe 2022.3.1 built on June 29, 2023</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
