<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ja"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://niusounds.github.io/Droid-Metal/feed.xml" rel="self" type="application/atom+xml" /><link href="https://niusounds.github.io/Droid-Metal/" rel="alternate" type="text/html" hreflang="ja" /><updated>2026-04-07T20:35:50+09:00</updated><id>https://niusounds.github.io/Droid-Metal/feed.xml</id><title type="html">Droid Metal</title><subtitle>コードが静かに走る夜、画面の向こうで世界が変わる。AndroidとAIの最前線を、重い音とともに刻む。</subtitle><author><name>niusounds</name></author><entry><title type="html">デバイス向き検出を LiveData から StateFlow + Compose へ完全移行する</title><link href="https://niusounds.github.io/Droid-Metal/2026/04/07/android-orientation-stateflow-compose/" rel="alternate" type="text/html" title="デバイス向き検出を LiveData から StateFlow + Compose へ完全移行する" /><published>2026-04-07T18:00:00+09:00</published><updated>2026-04-07T18:00:00+09:00</updated><id>https://niusounds.github.io/Droid-Metal/2026/04/07/android-orientation-stateflow-compose</id><content type="html" xml:base="https://niusounds.github.io/Droid-Metal/2026/04/07/android-orientation-stateflow-compose/"><![CDATA[<h2 id="はじめに">はじめに</h2>

<p>Android でデバイスの向き（方位角・ピッチ・ロール）を取得するには、加速度センサーと磁気センサーを組み合わせる方法が定番です。かつては <code class="language-plaintext highlighter-rouge">LiveData</code> を継承した独自クラスで実装するパターンが広く使われていましたが、2026 年現在のプロジェクトでは <code class="language-plaintext highlighter-rouge">StateFlow</code> + Kotlin coroutines を使う構成が主流です。</p>

<p>本記事では古い <code class="language-plaintext highlighter-rouge">LiveData</code> ベースの実装を出発点に、現代的な Flow ベースに移行するアプローチと、最終的に Jetpack Compose の UI へ流し込む全体像を解説します。</p>

<hr />

<h2 id="なぜ-livedata-から-stateflow-へ移行するのか">なぜ LiveData から StateFlow へ移行するのか</h2>

<p><code class="language-plaintext highlighter-rouge">LiveData</code> は Android のライフサイクルと自動的に連携できる点で優れていましたが、以下の理由で現代のコードベースでは扱いにくくなっています。</p>

<ul>
  <li><strong>ViewModel 外への依存</strong>: <code class="language-plaintext highlighter-rouge">LiveData.observe()</code> は <code class="language-plaintext highlighter-rouge">LifecycleOwner</code> を必要とするため、ドメイン層やリポジトリで使うと依存関係が逆転する</li>
  <li><strong>バックプレッシャー非対応</strong>: センサーは高頻度でイベントを発火するが、<code class="language-plaintext highlighter-rouge">LiveData</code> は間引き（debounce / throttle）を標準で持たない</li>
  <li><strong>Coroutines との親和性</strong>: <code class="language-plaintext highlighter-rouge">StateFlow</code> は <code class="language-plaintext highlighter-rouge">suspend</code> 関数や <code class="language-plaintext highlighter-rouge">Flow</code> 演算子と自然につながる</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">StateFlow</code> に移行することで、センサーのデータをパイプライン上で加工しやすくなり、ViewModel までの責務境界も明確になります。</p>

<hr />

<h2 id="センサー管理クラスを-flow-ベースで作る">センサー管理クラスを Flow ベースで作る</h2>

<p>まず、センサー購読を <code class="language-plaintext highlighter-rouge">callbackFlow</code> でラップするユーティリティを作ります。</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nn">android.content.Context</span>
<span class="k">import</span> <span class="nn">android.hardware.Sensor</span>
<span class="k">import</span> <span class="nn">android.hardware.SensorEvent</span>
<span class="k">import</span> <span class="nn">android.hardware.SensorEventListener</span>
<span class="k">import</span> <span class="nn">android.hardware.SensorManager</span>
<span class="k">import</span> <span class="nn">kotlinx.coroutines.channels.awaitClose</span>
<span class="k">import</span> <span class="nn">kotlinx.coroutines.flow.Flow</span>
<span class="k">import</span> <span class="nn">kotlinx.coroutines.flow.callbackFlow</span>
<span class="k">import</span> <span class="nn">kotlinx.coroutines.flow.conflate</span>

<span class="k">fun</span> <span class="nc">Context</span><span class="p">.</span><span class="nf">sensorFlow</span><span class="p">(</span>
    <span class="n">sensorType</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span>
    <span class="n">samplingPeriodUs</span><span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="nc">SensorManager</span><span class="p">.</span><span class="nc">SENSOR_DELAY_UI</span><span class="p">,</span>
<span class="p">):</span> <span class="nc">Flow</span><span class="p">&lt;</span><span class="nc">SensorEvent</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">callbackFlow</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">manager</span> <span class="p">=</span> <span class="nf">getSystemService</span><span class="p">(</span><span class="nc">SensorManager</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">sensor</span> <span class="p">=</span> <span class="n">manager</span><span class="p">.</span><span class="nf">getDefaultSensor</span><span class="p">(</span><span class="n">sensorType</span><span class="p">)</span>
        <span class="o">?:</span> <span class="nf">run</span> <span class="p">{</span> <span class="nf">close</span><span class="p">();</span> <span class="k">return</span><span class="nd">@callbackFlow</span> <span class="p">}</span>

    <span class="kd">val</span> <span class="py">listener</span> <span class="p">=</span> <span class="kd">object</span> <span class="err">: </span><span class="nc">SensorEventListener</span> <span class="p">{</span>
        <span class="k">override</span> <span class="k">fun</span> <span class="nf">onSensorChanged</span><span class="p">(</span><span class="n">event</span><span class="p">:</span> <span class="nc">SensorEvent</span><span class="p">)</span> <span class="p">{</span>
            <span class="nf">trySend</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="k">override</span> <span class="k">fun</span> <span class="nf">onAccuracyChanged</span><span class="p">(</span><span class="n">sensor</span><span class="p">:</span> <span class="nc">Sensor</span><span class="p">,</span> <span class="n">accuracy</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{}</span>
    <span class="p">}</span>

    <span class="n">manager</span><span class="p">.</span><span class="nf">registerListener</span><span class="p">(</span><span class="n">listener</span><span class="p">,</span> <span class="n">sensor</span><span class="p">,</span> <span class="n">samplingPeriodUs</span><span class="p">)</span>

    <span class="nf">awaitClose</span> <span class="p">{</span> <span class="n">manager</span><span class="p">.</span><span class="nf">unregisterListener</span><span class="p">(</span><span class="n">listener</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}.</span><span class="nf">conflate</span><span class="p">()</span> <span class="c1">// 処理が間に合わない場合は最新値のみ保持</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">callbackFlow</code> の <code class="language-plaintext highlighter-rouge">awaitClose</code> ブロックがコルーチンキャンセル時に自動で <code class="language-plaintext highlighter-rouge">unregisterListener</code> を呼ぶため、ライフサイクル漏れを起こしません。<code class="language-plaintext highlighter-rouge">conflate()</code> を付けることで、高頻度のセンサーイベントを UI 描画が追いつく範囲に自然に間引きできます。</p>

<hr />

<h2 id="向きデータを表す型と計算ロジック">向きデータを表す型と計算ロジック</h2>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nn">android.hardware.SensorManager</span>

<span class="kd">data class</span> <span class="nc">DeviceOrientation</span><span class="p">(</span>
    <span class="cm">/** 方位角（北=0, 東= π/2）ラジアン */</span>
    <span class="kd">val</span> <span class="py">azimuth</span><span class="p">:</span> <span class="nc">Float</span><span class="p">,</span>
    <span class="cm">/** ピッチ（前傾き）ラジアン */</span>
    <span class="kd">val</span> <span class="py">pitch</span><span class="p">:</span> <span class="nc">Float</span><span class="p">,</span>
    <span class="cm">/** ロール（横傾き）ラジアン */</span>
    <span class="kd">val</span> <span class="py">roll</span><span class="p">:</span> <span class="nc">Float</span><span class="p">,</span>
<span class="p">)</span>

<span class="k">fun</span> <span class="nf">computeOrientation</span><span class="p">(</span>
    <span class="n">accelerometer</span><span class="p">:</span> <span class="nc">FloatArray</span><span class="p">,</span>
    <span class="n">magnetometer</span><span class="p">:</span> <span class="nc">FloatArray</span><span class="p">,</span>
<span class="p">):</span> <span class="nc">DeviceOrientation</span><span class="p">?</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">rotationMatrix</span> <span class="p">=</span> <span class="nc">FloatArray</span><span class="p">(</span><span class="mi">9</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">orientationAngles</span> <span class="p">=</span> <span class="nc">FloatArray</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>

    <span class="kd">val</span> <span class="py">success</span> <span class="p">=</span> <span class="nc">SensorManager</span><span class="p">.</span><span class="nf">getRotationMatrix</span><span class="p">(</span>
        <span class="n">rotationMatrix</span><span class="p">,</span> <span class="k">null</span><span class="p">,</span> <span class="n">accelerometer</span><span class="p">,</span> <span class="n">magnetometer</span>
    <span class="p">)</span>
    <span class="k">if</span> <span class="p">(!</span><span class="n">success</span><span class="p">)</span> <span class="k">return</span> <span class="k">null</span>

    <span class="nc">SensorManager</span><span class="p">.</span><span class="nf">getOrientation</span><span class="p">(</span><span class="n">rotationMatrix</span><span class="p">,</span> <span class="n">orientationAngles</span><span class="p">)</span>
    <span class="k">return</span> <span class="nc">DeviceOrientation</span><span class="p">(</span>
        <span class="n">azimuth</span> <span class="p">=</span> <span class="n">orientationAngles</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
        <span class="n">pitch</span>   <span class="p">=</span> <span class="n">orientationAngles</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span>
        <span class="n">roll</span>    <span class="p">=</span> <span class="n">orientationAngles</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span>
    <span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="viewmodel-で-stateflow-に統合する">ViewModel で StateFlow に統合する</h2>

<p>2 つのセンサーフローを <code class="language-plaintext highlighter-rouge">combine</code> してまとめ、<code class="language-plaintext highlighter-rouge">StateFlow</code> に変換します。</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nn">android.app.Application</span>
<span class="k">import</span> <span class="nn">android.hardware.Sensor</span>
<span class="k">import</span> <span class="nn">android.hardware.SensorManager</span>
<span class="k">import</span> <span class="nn">androidx.lifecycle.AndroidViewModel</span>
<span class="k">import</span> <span class="nn">kotlinx.coroutines.flow.SharingStarted</span>
<span class="k">import</span> <span class="nn">kotlinx.coroutines.flow.combine</span>
<span class="k">import</span> <span class="nn">kotlinx.coroutines.flow.filterNotNull</span>
<span class="k">import</span> <span class="nn">kotlinx.coroutines.flow.map</span>
<span class="k">import</span> <span class="nn">kotlinx.coroutines.flow.stateIn</span>
<span class="k">import</span> <span class="nn">androidx.lifecycle.viewModelScope</span>

<span class="kd">class</span> <span class="nc">OrientationViewModel</span><span class="p">(</span><span class="n">app</span><span class="p">:</span> <span class="nc">Application</span><span class="p">)</span> <span class="p">:</span> <span class="nc">AndroidViewModel</span><span class="p">(</span><span class="n">app</span><span class="p">)</span> <span class="p">{</span>

    <span class="k">private</span> <span class="kd">val</span> <span class="py">accelFlow</span> <span class="p">=</span> <span class="n">app</span><span class="p">.</span><span class="nf">sensorFlow</span><span class="p">(</span><span class="nc">Sensor</span><span class="p">.</span><span class="nc">TYPE_ACCELEROMETER</span><span class="p">)</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">magnetFlow</span> <span class="p">=</span> <span class="n">app</span><span class="p">.</span><span class="nf">sensorFlow</span><span class="p">(</span><span class="nc">Sensor</span><span class="p">.</span><span class="nc">TYPE_MAGNETIC_FIELD</span><span class="p">)</span>

    <span class="kd">val</span> <span class="py">orientation</span> <span class="p">=</span> <span class="nf">combine</span><span class="p">(</span><span class="n">accelFlow</span><span class="p">,</span> <span class="n">magnetFlow</span><span class="p">)</span> <span class="p">{</span> <span class="n">accelEvent</span><span class="p">,</span> <span class="n">magEvent</span> <span class="p">-&gt;</span>
        <span class="nf">computeOrientation</span><span class="p">(</span><span class="n">accelEvent</span><span class="p">.</span><span class="n">values</span><span class="p">.</span><span class="nf">clone</span><span class="p">(),</span> <span class="n">magEvent</span><span class="p">.</span><span class="n">values</span><span class="p">.</span><span class="nf">clone</span><span class="p">())</span>
    <span class="p">}</span>
        <span class="p">.</span><span class="nf">filterNotNull</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">stateIn</span><span class="p">(</span>
            <span class="n">scope</span> <span class="p">=</span> <span class="n">viewModelScope</span><span class="p">,</span>
            <span class="n">started</span> <span class="p">=</span> <span class="nc">SharingStarted</span><span class="p">.</span><span class="nc">WhileSubscribed</span><span class="p">(</span><span class="mi">5_000</span><span class="p">),</span>
            <span class="n">initialValue</span> <span class="p">=</span> <span class="nc">DeviceOrientation</span><span class="p">(</span><span class="mf">0f</span><span class="p">,</span> <span class="mf">0f</span><span class="p">,</span> <span class="mf">0f</span><span class="p">),</span>
        <span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">SharingStarted.WhileSubscribed(5_000)</code> によって、画面が 5 秒以上バックグラウンドに入ったらセンサー購読を自動停止します。画面が戻れば再開します。</p>

<hr />

<h2 id="jetpack-compose-の-ui-へ流し込む">Jetpack Compose の UI へ流し込む</h2>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nn">androidx.compose.runtime.Composable</span>
<span class="k">import</span> <span class="nn">androidx.compose.runtime.getValue</span>
<span class="k">import</span> <span class="nn">androidx.lifecycle.compose.collectAsStateWithLifecycle</span>
<span class="k">import</span> <span class="nn">androidx.lifecycle.viewmodel.compose.viewModel</span>
<span class="k">import</span> <span class="nn">androidx.compose.material3.Text</span>
<span class="k">import</span> <span class="nn">kotlin.math.toDegrees</span>
<span class="k">import</span> <span class="nn">androidx.compose.foundation.layout.Column</span>
<span class="k">import</span> <span class="nn">androidx.compose.foundation.layout.padding</span>
<span class="k">import</span> <span class="nn">androidx.compose.ui.Modifier</span>
<span class="k">import</span> <span class="nn">androidx.compose.ui.unit.dp</span>

<span class="nd">@Composable</span>
<span class="k">fun</span> <span class="nf">OrientationScreen</span><span class="p">(</span>
    <span class="n">vm</span><span class="p">:</span> <span class="nc">OrientationViewModel</span> <span class="p">=</span> <span class="nf">viewModel</span><span class="p">(),</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">orientation</span> <span class="k">by</span> <span class="n">vm</span><span class="p">.</span><span class="n">orientation</span><span class="p">.</span><span class="nf">collectAsStateWithLifecycle</span><span class="p">()</span>

    <span class="nc">Column</span><span class="p">(</span><span class="n">modifier</span> <span class="p">=</span> <span class="nc">Modifier</span><span class="p">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">16</span><span class="p">.</span><span class="n">dp</span><span class="p">))</span> <span class="p">{</span>
        <span class="nc">Text</span><span class="p">(</span><span class="n">text</span> <span class="p">=</span> <span class="s">"方位角: %.1f°"</span><span class="p">.</span><span class="nf">format</span><span class="p">(</span><span class="n">orientation</span><span class="p">.</span><span class="n">azimuth</span><span class="p">.</span><span class="nf">toDegrees</span><span class="p">()))</span>
        <span class="nc">Text</span><span class="p">(</span><span class="n">text</span> <span class="p">=</span> <span class="s">"ピッチ:  %.1f°"</span><span class="p">.</span><span class="nf">format</span><span class="p">(</span><span class="n">orientation</span><span class="p">.</span><span class="n">pitch</span><span class="p">.</span><span class="nf">toDegrees</span><span class="p">()))</span>
        <span class="nc">Text</span><span class="p">(</span><span class="n">text</span> <span class="p">=</span> <span class="s">"ロール:  %.1f°"</span><span class="p">.</span><span class="nf">format</span><span class="p">(</span><span class="n">orientation</span><span class="p">.</span><span class="n">roll</span><span class="p">.</span><span class="nf">toDegrees</span><span class="p">()))</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">private</span> <span class="k">fun</span> <span class="nc">Float</span><span class="p">.</span><span class="nf">toDegrees</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Math</span><span class="p">.</span><span class="nf">toDegrees</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nf">toDouble</span><span class="p">()).</span><span class="nf">toFloat</span><span class="p">()</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">collectAsStateWithLifecycle()</code> は <code class="language-plaintext highlighter-rouge">Lifecycle.State.STARTED</code> 以上の場合のみ収集します。画面が非表示になれば自動的にコレクションを停止し、ViewModel 側の <code class="language-plaintext highlighter-rouge">WhileSubscribed</code> と組み合わさることでセンサーも停止します。</p>

<hr />

<h2 id="まとめ">まとめ</h2>

<table>
  <thead>
    <tr>
      <th>比較項目</th>
      <th>LiveData ベース（旧）</th>
      <th>StateFlow ベース（現代）</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ドメイン層での利用</td>
      <td>LifecycleOwner 依存で困難</td>
      <td>依存なし、自由に使える</td>
    </tr>
    <tr>
      <td>センサー停止タイミング</td>
      <td>onInactive に依存</td>
      <td>WhileSubscribed で自動制御</td>
    </tr>
    <tr>
      <td>複数センサーの統合</td>
      <td>MediatorLiveData が必要</td>
      <td><code class="language-plaintext highlighter-rouge">combine</code> で宣言的に書ける</td>
    </tr>
    <tr>
      <td>Compose との統合</td>
      <td><code class="language-plaintext highlighter-rouge">observeAsState</code></td>
      <td><code class="language-plaintext highlighter-rouge">collectAsStateWithLifecycle</code></td>
    </tr>
  </tbody>
</table>

<p>LiveData が「ライフサイクル問題を解決した」なら、StateFlow は「ライフサイクルへの依存そのものをなくした」と言えます。センサー処理のような高頻度イベントは特に Flow の恩恵が大きく、ドメイン層まで一気通貫でコードを整理できます。</p>

<hr />

<p><em>元記事: <a href="https://qiita.com/niusounds/items/54f09c2e4c012de398a8">Kotlin&amp;LiveDataで端末の向きを取得する処理を書いてみた</a></em></p>]]></content><author><name>niusounds</name></author><category term="Android" /><category term="Kotlin" /><category term="StateFlow" /><category term="Coroutines" /><category term="JetpackCompose" /><category term="Sensors" /><summary type="html"><![CDATA[加速度センサー・磁気センサーで端末の向きを取得するコードをFlow + coroutines + Jetpack Composeで書いてみた]]></summary></entry><entry><title type="html">AsyncTask、Loader、Rx、Coroutinesを全部通って見えた Android非同期設計の地雷</title><link href="https://niusounds.github.io/Droid-Metal/2026/04/06/android-async-design-landmines/" rel="alternate" type="text/html" title="AsyncTask、Loader、Rx、Coroutinesを全部通って見えた Android非同期設計の地雷" /><published>2026-04-06T09:00:00+09:00</published><updated>2026-04-06T09:00:00+09:00</updated><id>https://niusounds.github.io/Droid-Metal/2026/04/06/android-async-design-landmines</id><content type="html" xml:base="https://niusounds.github.io/Droid-Metal/2026/04/06/android-async-design-landmines/"><![CDATA[<h2 id="はじめに">はじめに</h2>
<p>Android 1.5の頃からアプリを作っていると、非同期APIは何度も主役交代してきました。AsyncTask、Loader、Rx、Coroutines。道具は進化しましたが、障害対応の現場で見る事故の根本原因は驚くほど似ています。</p>

<p>本記事では、各時代で実際に踏みがちな地雷を振り返りながら、2026年時点でも有効だった実務ルールに絞って整理します。ポイントは「どのAPIを使うか」より「キャンセル・責務・エラーハンドリングをどう設計するか」です。</p>

<h2 id="asynctask時代の地雷">AsyncTask時代の地雷</h2>
<p>AsyncTaskの時代は、とにかく画面回転とライフサイクルの相性で苦しみました。最も多かったのは、非同期処理中に画面が破棄され、結果をUIへ反映しようとしてクラッシュする問題です。</p>

<h3 id="典型的な事故">典型的な事故</h3>
<ul>
  <li>Activity参照を内部クラスで握り続ける</li>
  <li>画面回転後に古いインスタンスへコールバックしてアプリがクラッシュ</li>
  <li>キャンセルを呼んでも中断されず、二重更新が起きる</li>
</ul>

<h3 id="学んだこと">学んだこと</h3>
<p>「処理がバックグラウンドで動ける」ことと「UIを安全に変更できる」ことは別問題です。UIのライフサイクルとの接続点を設計しない限り、どんな非同期APIでも事故は起きます。</p>

<h2 id="loader時代の地雷">Loader時代の地雷</h2>
<p>Loaderはライフサイクル連携を前進させましたが、今度は責務の分散が問題になりました。データ取得ロジックがFragmentやActivityに寄りすぎると、見通しが急激に悪化します。</p>

<h3 id="典型的な事故-1">典型的な事故</h3>
<ul>
  <li>LoaderCallbacksに業務ロジックが混ざり、テストが難しい</li>
  <li>画面ごとに似たLoader実装が増殖し、修正漏れが発生</li>
  <li>キャッシュ戦略が画面単位でバラバラになる</li>
</ul>

<h3 id="学んだこと-1">学んだこと</h3>
<p>ライフサイクルに強い仕組みだけでは足りません。責務をUI層から分離し、ユースケース単位で再利用できる構造が必要です。</p>

<h2 id="reactivex時代の地雷">ReactiveX時代の地雷</h2>
<p>ReactiveX(Rx)は表現力が高く、複雑な非同期フローを綺麗に書けました。ただし、チームで運用するとDispose漏れとScheduler設計のばらつきが頻発しました。</p>

<h3 id="典型的な事故-2">典型的な事故</h3>
<ul>
  <li><code class="language-plaintext highlighter-rouge">subscribe()</code> が散在して購読ライフサイクルを追えない</li>
  <li><code class="language-plaintext highlighter-rouge">CompositeDisposable</code> の解放漏れで画面離脱後も処理継続</li>
  <li><code class="language-plaintext highlighter-rouge">observeOn</code> / <code class="language-plaintext highlighter-rouge">subscribeOn</code> の設計が統一されずデバッグ困難</li>
</ul>

<h3 id="学んだこと-2">学んだこと</h3>
<p>Rxの難しさは演算子ではなく運用ルールです。購読開始・終了の責務を定型化しないと、実装者依存の品質になります。</p>

<h2 id="coroutines時代の地雷">Coroutines時代の地雷</h2>
<p>Coroutinesは読みやすくなりましたが、地雷が消えたわけではありません。特に初期導入時は「軽く書ける」ことが裏目に出ます。</p>

<h3 id="典型的な事故-3">典型的な事故</h3>
<ul>
  <li><code class="language-plaintext highlighter-rouge">GlobalScope</code> の使用でライフサイクル外に処理が残る</li>
  <li><code class="language-plaintext highlighter-rouge">launch</code> と <code class="language-plaintext highlighter-rouge">async</code> を混在させ、例外伝播が意図通りにならない</li>
  <li><code class="language-plaintext highlighter-rouge">withContext</code> の乱用で責務境界が曖昧になる</li>
</ul>

<h3 id="学んだこと-3">学んだこと</h3>
<p>Coroutinesは「正しくキャンセルが伝播する設計」を作って初めて安全になります。書きやすさは設計品質の代替にはなりません。</p>

<h2 id="非同期処理の難しさ">非同期処理の難しさ</h2>
<p>どのような形で非同期処理をするにせよ、以下のことが地雷の要因になります。</p>

<ol>
  <li>非同期処理のキャンセルタイミングについてきちんとした考慮がされていない</li>
  <li>UI層とデータ層の非同期責務が混ざっている</li>
  <li>エラー分類とリトライ方針がユースケースごとに不統一</li>
</ol>

<p>この3つを放置すると、どのAPIでも「たまに落ちる」「再現しづらい」不具合の原因になります。</p>

<h2 id="まとめ">まとめ</h2>
<p>非同期APIは進化し続けていますが、地雷の本体は道具ではなく設計です。私が長年の移行で得た結論は次の3点です。</p>

<ol>
  <li>非同期処理のキャンセル戦略を決める</li>
  <li>UI層から非同期責務を剥がし、境界を固定する</li>
  <li>スレッド境界とオブジェクトの参照関係を意識する</li>
</ol>

<p>「新しいAPIに移行したのに不具合が減らない」と感じるチームでは、非同期処理の設計を見直すことが大切です。</p>]]></content><author><name>niusounds</name></author><category term="Android" /><category term="Asynchronous" /><category term="Coroutines" /><category term="RxJava" /><category term="Architecture" /><summary type="html"><![CDATA[AsyncTaskからCoroutinesまで渡り歩いて分かった、Android非同期設計で繰り返される地雷を実体験ベースで整理。APIの世代が変わっても残る失敗と、2026年時点の実務ルールを解説します。]]></summary></entry><entry><title type="html">Androidマルチモジュール化の判断基準と移行ロードマップ: 分けるべき時、分けすぎる罠、段階移行の実践</title><link href="https://niusounds.github.io/Droid-Metal/2026/04/05/android-multi-module-migration-roadmap/" rel="alternate" type="text/html" title="Androidマルチモジュール化の判断基準と移行ロードマップ: 分けるべき時、分けすぎる罠、段階移行の実践" /><published>2026-04-05T14:00:00+09:00</published><updated>2026-04-05T14:00:00+09:00</updated><id>https://niusounds.github.io/Droid-Metal/2026/04/05/android-multi-module-migration-roadmap</id><content type="html" xml:base="https://niusounds.github.io/Droid-Metal/2026/04/05/android-multi-module-migration-roadmap/"><![CDATA[<h2 id="はじめに">はじめに</h2>
<p>Androidアプリの規模が大きくなると、ほぼ必ず「マルチモジュール化するべきか」という議論にぶつかります。私自身、単一モジュールで育ったアプリを段階移行した案件と、最初から過剰分割して失敗した案件の両方を経験しました。</p>

<p>結論から言うと、マルチモジュール化は「正義」ではなく「投資」です。投資なので、回収できる条件を満たした時だけ実施するのが最適解です。本記事では、実務で使っている判断基準と、破綻しにくい移行ロードマップをまとめます。</p>

<h2 id="こういうケースなら分割の考えどきかもしれない">こういうケースなら分割の考えどきかもしれない</h2>
<p>以下のような状態なら、マルチモジュール化を検討してみる価値があります。</p>

<ul>
  <li>ビルド時間が開発体験を継続的に毀損している</li>
  <li>チームが並行開発しづらく、コンフリクトが常態化している</li>
  <li>機能境界が曖昧で、影響範囲の見積もりが外れ続けている</li>
</ul>

<h3 id="指標の目安">指標の目安</h3>
<ul>
  <li>クリーンビルドが10分以上、日中インクリメンタルでも待機が頻発</li>
  <li>1つのPRで無関係ファイルに波及する修正が多い</li>
  <li>メンバー同士が独立した機能の開発をしているはずなのにコンフリクトが頻繁に発生する</li>
</ul>

<p>逆に、ビルド時間も保守性も問題ない段階で分割だけを先行すると、ほぼ確実にコスト先行になります。特に小規模チームでは、モジュール間の調整コストが純粋な実装速度を上回りやすいです。</p>

<h2 id="分割しすぎの罠">分割しすぎの罠</h2>
<p>最も多い失敗は、責務ではなく「雰囲気」でモジュールを切ることです。例えば、UI部品を細粒度で分けすぎた結果、変更1件で5モジュールのリリース調整が必要になるケースは珍しくありません。</p>

<h3 id="典型的なアンチパターン">典型的なアンチパターン</h3>
<ul>
  <li>抽象化だけが増え、実装コストに見合う効果が出ない</li>
  <li>新しいコードを追加する際、coreに入れるべきかfeatureに入れるべきか、coreに入れるとしたらどこに入れるか、毎回迷う</li>
  <li>モジュールを分けているのに複数のモジュールに対し同時修正が必要な状態になっている</li>
  <li>共通機能モジュール側にfeatureモジュール都合の実装や命名が入り込み、再利用性が下がる</li>
</ul>

<h3 id="失敗を避ける原則">失敗を避ける原則</h3>
<ul>
  <li>モジュール分割の単位を明文化し、チームで合意する</li>
  <li>共通化することにこだわりすぎず、「3回重複したら共通化」くらいにする</li>
  <li>モジュール数ではなく、変更の閉じ込め率を評価指標にする</li>
</ul>

<h2 id="段階的移行ロードマップ">段階的移行ロードマップ</h2>

<h3 id="1-可視化フェーズ">1) 可視化フェーズ</h3>
<p>まず依存関係とビルド時間を可視化します。ここを飛ばすと、移行後に「何が良くなったのか」が測れません。</p>

<ul>
  <li>Gradle Build Scanでボトルネックタスクを特定</li>
  <li>変更頻度の高いパッケージを抽出</li>
  <li>依存サイクルの有無を確認</li>
</ul>

<h3 id="2-境界確定フェーズ">2) 境界確定フェーズ</h3>
<p>次に、コードを動かす前に境界を合意します。</p>

<ul>
  <li>feature, core, data などの責務を明文化</li>
  <li>API公開面を最小化して、内部実装は隠蔽</li>
  <li>依存方向を片方向に固定</li>
</ul>

<h3 id="3-抽出フェーズ">3) 抽出フェーズ</h3>
<p>ここで初めてモジュールを切り出します。最初の対象は「変更が多いが境界が比較的明確」な領域が安全です。</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// settings.gradle.kts</span>
<span class="nf">include</span><span class="p">(</span>
    <span class="s">":app"</span><span class="p">,</span>
    <span class="s">":core:common"</span><span class="p">,</span>
    <span class="s">":core:ui"</span><span class="p">,</span>
    <span class="s">":core:data"</span><span class="p">,</span>
    <span class="s">":feature:auth"</span>
<span class="p">)</span>

<span class="c1">// feature/auth/build.gradle.kts</span>
<span class="nf">plugins</span> <span class="p">{</span>
    <span class="nf">id</span><span class="p">(</span><span class="s">"com.android.library"</span><span class="p">)</span>
    <span class="nf">id</span><span class="p">(</span><span class="s">"org.jetbrains.kotlin.android"</span><span class="p">)</span>
<span class="p">}</span>

<span class="nf">dependencies</span> <span class="p">{</span>
    <span class="nf">implementation</span><span class="p">(</span><span class="nf">project</span><span class="p">(</span><span class="s">":core:common"</span><span class="p">))</span>
    <span class="nf">implementation</span><span class="p">(</span><span class="nf">project</span><span class="p">(</span><span class="s">":core:ui"</span><span class="p">))</span>
    <span class="nf">implementation</span><span class="p">(</span><span class="nf">project</span><span class="p">(</span><span class="s">":core:data"</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>この段階では「完璧な分離」より「壊さず移す」を優先します。必要なら一時的なFacadeを置いて移行期間の差分を吸収します。</p>

<h3 id="4-最適化フェーズ">4) 最適化フェーズ</h3>
<p>移行後に初めて最適化へ進みます。</p>

<ul>
  <li>KSP/KAPTの配置を見直し、重い処理を局所化</li>
  <li>API/implementation依存を整理して再コンパイル範囲を縮小</li>
  <li>不要モジュールの統合も含めて再評価</li>
</ul>

<h2 id="チームで運用したいこと">チームで運用したいこと</h2>
<ul>
  <li>新規機能は原則feature配下に追加し、app直下を汚さない（定型的に書く必要があるコードは除く）</li>
  <li>モジュール公開APIはレビューで明示チェックする（<code class="language-plaintext highlighter-rouge">internal</code>修飾子を適切に使う）</li>
  <li>月次で「分割したが価値が薄いモジュール」を棚卸しする</li>
</ul>

<p>「分割したはいいけど、むしろ統合した方が楽じゃなかった？」ということがないか定期的に振り返り、時にはモジュールを統合する決断をとるのも大事です。</p>

<h2 id="まとめ">まとめ</h2>
<p>マルチモジュール化は、規模拡大に対する万能薬ではありません。効果が出る条件を満たした時にだけ実施し、段階的に進めることで初めて投資対効果が合います。</p>

<p>実務での要点は次の3つです。</p>

<ol>
  <li>分割の開始条件を定量・定性の両面で定める</li>
  <li>分割しすぎを防ぐため、責務境界から設計する</li>
  <li>可視化から始める段階移行で、改善を計測しながら進める</li>
</ol>

<p>これから着手するなら、最初の一歩は「どこを分けるか」ではなく「なぜ分けるか」の合意形成です。ここが揃うと、移行は驚くほど安定します。</p>]]></content><author><name>niusounds</name></author><category term="Android" /><category term="Architecture" /><category term="Gradle" /><category term="Modularization" /><category term="TechLead" /><summary type="html"><![CDATA[Androidアプリをマルチモジュール化する判断基準を、実務での失敗と改善を踏まえて解説。分割しすぎを避けながら、段階的に移行する現実的なロードマップを紹介します。]]></summary></entry><entry><title type="html">AndroidローカルLLMの実践: 2026年版</title><link href="https://niusounds.github.io/Droid-Metal/2026/04/04/android-llm-2026/" rel="alternate" type="text/html" title="AndroidローカルLLMの実践: 2026年版" /><published>2026-04-04T09:00:00+09:00</published><updated>2026-04-04T09:00:00+09:00</updated><id>https://niusounds.github.io/Droid-Metal/2026/04/04/android-llm-2026</id><content type="html" xml:base="https://niusounds.github.io/Droid-Metal/2026/04/04/android-llm-2026/"><![CDATA[<h2 id="概要">概要</h2>
<p>本記事は、Android端末上で動作するローカルLLM（Large Language Model）を構築するための実践ガイドです。オフラインでの推論とプライバシー保護に重点を置き、2026年現在の最新技術を解説します。開発環境のセットアップからモデルの最適化、実際のアプリケーションへの組み込みまで、Androidエンジニアが実務ですぐに使える知見を提供します。</p>

<h2 id="用意するもの">用意するもの</h2>
<ul>
  <li>Android Studio 3.5+（Kotlin対応）</li>
  <li>Java/NDK SDK（APIレベル34以上推奨）</li>
  <li>TensorFlow Lite for Android v2.17+</li>
  <li>ONNX Runtime Mobile v0.18+</li>
  <li>GitHub Actions でのCI/CD設定</li>
</ul>

<h2 id="手順1-オフライン環境でのモデル最適化">手順1: オフライン環境でのモデル最適化</h2>
<p>現行のLLMは推論時にGPU/TPUを必要としますが、AndroidではARM CPUやNPUが主流です。以下のステップでモデルを軽量化できます。</p>

<ol>
  <li><strong>Quantization（8bit整数化）</strong>
    <ul>
      <li>Hugging Face Transformersで<code class="language-plaintext highlighter-rouge">AutoModel.from_pretrained(..., load_in_8bit=True)</code> を用いてモデルを変換。</li>
    </ul>
  </li>
  <li><strong>Pruning &amp; Distillation</strong>
    <ul>
      <li>TensorFlow Model Optimization Toolkit で不要なパラメータを削除し、推論速度を約3倍向上。</li>
    </ul>
  </li>
  <li><strong>Export to TFLite</strong>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python <span class="nt">-m</span> tensorflow_model_optimization.saving.configs.tflite_config <span class="se">\</span>
       <span class="nt">--input_file</span><span class="o">=</span>model/exported/pytorch_model.bin <span class="se">\</span>
       <span class="nt">--output_dir</span><span class="o">=</span>tflite/
</code></pre></div>    </div>
  </li>
</ol>

<h2 id="手順2-androidアプリへの組み込み">手順2: Androidアプリへの組み込み</h2>
<p>最適化済みTFLiteモデルをAndroidアプリに統合するには、以下のコード例が参考になります。</p>

<h3 id="kotlin実装例-推論処理クラス">Kotlin実装例: 推論処理クラス</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">LlmInference</span><span class="p">(</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">interpreter</span><span class="p">:</span> <span class="nc">TFLiteInterpreter</span><span class="p">,</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">inputTensor</span><span class="p">:</span> <span class="nc">Tensor</span><span class="p">,</span>
    <span class="k">private</span> <span class="kd">val</span> <span class="py">outputTensor</span><span class="p">:</span> <span class="nc">Tensor</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">run</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">maxTokens</span><span class="p">:</span> <span class="nc">Int</span><span class="p">):</span> <span class="nc">String</span> <span class="p">{</span>
        <span class="c1">// テキストをtokenize（ここでは簡略化）</span>
        <span class="kd">val</span> <span class="py">encoded</span> <span class="p">=</span> <span class="n">tokenizer</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">add_special_tokens</span> <span class="p">=</span> <span class="k">false</span><span class="p">)</span>
        <span class="n">inputTensor</span><span class="p">.</span><span class="nf">flatBuffer</span><span class="p">().</span><span class="nf">putInt</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">encoded</span><span class="p">.</span><span class="n">length</span><span class="p">)</span>
        <span class="n">inputTensor</span><span class="p">.</span><span class="nf">flatBuffer</span><span class="p">().</span><span class="nf">putFloatArrayOf</span><span class="p">(</span><span class="n">encoded</span><span class="p">.</span><span class="n">indices</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">toFloat</span><span class="p">()</span> <span class="p">})</span>

        <span class="n">interpreter</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span><span class="n">inputTensor</span><span class="p">.</span><span class="n">buffer</span><span class="p">,</span> <span class="n">outputTensor</span><span class="p">.</span><span class="n">buffer</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">tokenizer</span><span class="p">.</span><span class="nf">decode</span><span class="p">(</span><span class="n">outputTensor</span><span class="p">.</span><span class="nf">intValueAt</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span>
    <span class="p">}</span>

    <span class="k">companion</span> <span class="k">object</span> <span class="p">{</span>
        <span class="k">fun</span> <span class="nf">loadModelFromAssets</span><span class="p">(</span>
            <span class="n">context</span><span class="p">:</span> <span class="nc">Context</span><span class="p">,</span>
            <span class="n">modelAssetName</span><span class="p">:</span> <span class="nc">String</span>
        <span class="p">):</span> <span class="nc">LlmInference</span><span class="p">?</span> <span class="p">{</span>
            <span class="kd">val</span> <span class="py">file</span> <span class="p">=</span> <span class="nc">File</span><span class="p">(</span><span class="n">context</span><span class="p">.</span><span class="n">filesDir</span><span class="p">,</span> <span class="s">"lmm.tflite"</span><span class="p">)</span>
            <span class="k">if</span> <span class="p">(!</span><span class="n">file</span><span class="p">.</span><span class="nf">exists</span><span class="p">())</span> <span class="k">return</span> <span class="k">null</span>

            <span class="kd">val</span> <span class="py">builder</span> <span class="p">=</span> <span class="nc">InterpreterBuilder</span><span class="p">(</span><span class="nc">FileInputStream</span><span class="p">(</span><span class="n">file</span><span class="p">),</span> <span class="k">null</span><span class="p">)</span>
            <span class="kd">val</span> <span class="py">interpreter</span><span class="p">:</span> <span class="nc">TFLiteInterpreter</span> <span class="k">by</span> <span class="nf">setter</span> <span class="p">{</span> <span class="n">it</span> <span class="p">}</span>

            <span class="c1">// モデルの入出力情報を取得</span>
            <span class="kd">val</span> <span class="py">inputDetails</span> <span class="p">=</span> <span class="n">interpreter</span><span class="p">.</span><span class="nf">getInputTensor</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="n">shape</span>
            <span class="kd">val</span> <span class="py">outputDetails</span> <span class="p">=</span> <span class="n">interpreter</span><span class="p">.</span><span class="nf">getOutputTensor</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="n">shape</span>

            <span class="k">return</span> <span class="nc">LlmInference</span><span class="p">(</span>
                <span class="n">interpreter</span><span class="p">,</span>
                <span class="nc">Tensor</span><span class="p">(</span><span class="n">inputDetails</span><span class="p">),</span>
                <span class="nc">Tensor</span><span class="p">(</span><span class="n">outputDetails</span><span class="p">)</span>
            <span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="実装上の注意点androidエンジニア向け">実装上の注意点（Androidエンジニア向け）</h2>

<ul>
  <li>
    <p><strong>メモリ管理</strong>  <br />
TFLiteインタプリタは推論中に数MBのヒープを消費します。<code class="language-plaintext highlighter-rouge">process.onCreate()</code> または <code class="language-plaintext highlighter-rouge">onStart()</code> でモデルを事前ロードし、アクティビティ切り替え時に解放することでクラッシュを防ぎましょう。</p>
  </li>
  <li>
    <p><strong>バックグラウンド処理</strong>  <br />
プライバシー保護のため推論を<code class="language-plaintext highlighter-rouge">WorkerThread</code>または<code class="language-plaintext highlighter-rouge">CoroutineScope</code>で非同期実行。メインスレッドでの操作は<code class="language-plaintext highlighter-rouge">runOnUiThread</code>で厳密に制限してください。</p>
  </li>
  <li><strong>省電力対策</strong>  <br />
NPUが利用可能な場合、<code class="language-plaintext highlighter-rouge">NNAPI</code>デリゲートを指定し、GPU/CPUの消費電力を最小化。設定例：
    <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">options</span> <span class="p">=</span> <span class="nc">InterpreterOptions</span><span class="p">().</span><span class="nf">apply</span> <span class="p">{</span>
    <span class="nf">setDelegate</span><span class="p">(</span><span class="nc">NNDelegeate</span><span class="p">())</span>
<span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li><strong>セキュリティ</strong>  <br />
モデルファイルは<code class="language-plaintext highlighter-rouge">android:sharedUserId="android.uid_system"</code>のアプリが所有するディレクトリに配置。外部ストレージへのコピー時は<code class="language-plaintext highlighter-rouge">ContentProvider</code>経由でアクセス権限を明示的に付与。</li>
</ul>

<h2 id="まとめ">まとめ</h2>
<p>2026年現在、Android端末でのローカルLLM実装は「量子化」「剪定」「NPU最適化」の3本柱で実現可能です。Kotlinを用いたTFLiteインターフェースはシンプルかつ高速で、オフライン環境でも数秒以内に応答が得られます。メモリリークやバッテリー消費への対策を事前に施せば、企業システムにも安心して導入できます。</p>

<p>Tags: Android, AI, LocalLLM, 開発</p>]]></content><author><name>niusounds</name></author><category term="Android" /><category term="AI" /><category term="LocalLLM" /><category term="開発" /><summary type="html"><![CDATA[Android端末上で動作するローカルLLM構築の実践ガイド（2026年版）。オフライン推論とプライバシー保護に焦点を当て、最新技術を解説します。]]></summary></entry></feed>