Android アプリ開発の基礎:アーキテクチャから Jetpack までの要点解説

1. Android システムの概要

Android プラットフォームは階層化されたアーキテクチャを持ち、各レイヤーが特定の役割を担っています。

1.1 システム構造

  • アプリケーション層: ユーザーが直接操作するアプリ群(ブラウザ、メール等)
  • フレームワーク層: アプリ開発に必要な機能を提供する API の集合体
  • ネイティブ実行ライブラリ層: C/C++ で記述された高性能な機能サポート(SQLite, OpenGL ES 等)
  • Linux カーネル層: ハードウェアとのインタフェースを担当(ディスプレイ、バッテリー管理、ドライバなど)

1.2 開発環境設定

ビルドシステムとして Gradle を採用しており、ローカルへのキャッシュ設定を行うことでプロジェクト構築の高速化が可能です。SDK の管理と依存関係の解決はこのレイヤーで行われます。

1.3 デバイスへの展開

物理端末での動作確認には、設定メニュー内のバージョン情報で「ビルド番号」を連続タップし、開発者オプションを有効化する手順が必要です。USB デバッグモードの起動により PC と連携できます。

1.4 プロジェクト構成要素

以下のディレクトリとファイルが主な構成要素です。

  • drawable: リサイズ可能な画像資源
  • mipmap: アプリアイコン用の高解像度画像
  • values: カラー、スタイル、文字列定数などの定義
  • layout: UI のレイアウト定義
  • xml: 設定ファイルやデータ抽出ルールの定義
  • build.gradle: ビルド設定と依存ライブラリの宣言
  • AndroidManifest.xml: アプリの全体的な構成情報を記述

重要な属性について:

<!-- AndroidManifest.xml におけるセキュリティ設定 -->
<activity android:name=".ExampleActivity"
          android:exported="true">
</activity>

android:exported は API レベル 31 (Android 12) 以降、インテントフィルターを持つコンポーネントに必須です。true は他アプリからのアクセスを許可し、false は限定します。

2. アクティビティとその挙動

Activity は画面表示の単位数となり、ユーザーインターフェースの中心となります。

2.1 Basic Implementation

新しいクラスを作成し、AppCompatActivity を継承することで基本的な画面実装が可能になります。

package com.example.androiddemo

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.widget.Button
import android.widget.Toast

class DemoActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_layout)

        // トースト表示ボタン
        val toastBtn: Button = findViewById(R.id.btn_show_toast)
        toastBtn.setOnClickListener {
            Toast.makeText(applicationContext, "処理完了", Toast.LENGTH_SHORT).show()
        }

        // 終了ボタン
        val finishBtn: Button = findViewById(R.id.btn_exit)
        finishBtn.setOnClickListener {
            finish()
        }
    }

    // メニュー項目作成
    override fun onCreateOptionsMenu(menu: android.view.Menu): Boolean {
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    // メニュー選択時の反応
    override fun onOptionsItemSelected(item: android.view.MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_save -> {
                Toast.makeText(this, "保存しました", Toast.LENGTH_SHORT).show()
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }
}

マニフェストファイルでの登録が必要です。

<application ...>
    <activity
        android:name=".DemoActivity"
        android:label="@string/app_name"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

2.2 インテントによる遷移

アクティビティ間の移動には明示的と暗黙的な方法があります。

明示的インテント: クラスを指定して起動します。

val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("KEY_DATA", "message_content")
startActivity(intent)

ファクトリメソッド利用: コード内側にロジックを閉じ込めると呼び出し側が簡潔になります。

companion object {
    @JvmStatic
    fun launch(context: Context, msg: String) {
        val i = Intent(context, DetailActivity::class.java)
        i.putExtra("MSG_PARAM", msg)
        context.startActivity(i)
    }
}

暗黙的インテント: 動作タイプに基づいてシステムが適切なアプリを選択します。

// ブラウザ開く
val i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse("https://www.google.com")
startActivity(i)

// 電話発呼
val callIntent = Intent(Intent.ACTION_DIAL)
callIntent.data = Uri.parse("tel:123456789")
startActivity(callIntent)

2.3 ライフサイクルとバックスタック

Android はタスクスタック方式を採用しています。新しい画面が開かれるとそこに追加され、戻る操作で削除されます。

  • Running: 前面に表示され、優先的にメモリを保持。onResume() が呼び出される状態。
  • Paused: 一部不可視だが完全ではありません。半透明のダイアログなどが重なった場合など。onPause() が呼ばれます。
  • Stopped: 完全に非表示。onStop() が呼ばれ、メモリ不足時は解放対象となる可能性があります。
  • Destroyed: スタックから外れ、プロセスが終了。onDestroy() が最終的に実行されます。

onSaveInstanceState() はデータの一時的な保存に用いられ、設定変更時の再構築時に役立ちます。

2.4 起動モード

  • standard: 毎回新しいインスタンスを作成(デフォルト)
  • singleTop: すでにスタック頂上に同じものがある場合は新規作成せず、既存のものを再利用
  • singleTask: タスク全体で一つのインスタンスのみ存在。その下に他のものがあればクリアされる
  • singleInstance: 専用のスタックを確保し、この画面だけが孤立して存在する

3. UI コンポーネントとレイアウト

3.1 基本ウィジェット

画面上の表示要素は View クラスのサブクラスで構成されます。

<TextView
    android:id="@+id/disp_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Sample Text"
    android:textSize="16sp" />

EditText は入力フィールド、ImageView は画像表示に使用されます。

<ProgressBar
    android:id="@+id/loading_bar"
    style="?android:attr/progressBarStyleHorizontal"
    android:max="100"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

ダイアログ表示には AlertDialog を使用します。

AlertDialog.Builder(this).apply {
    setTitle("確認")
    setMessage("本当に実行しますか?")
    setPositiveButton("はい") { _, _ -> /* ロジック */ }
    setNegativeButton("いいえ", null)
}.show()

3.2 レイアウト管理

LinearLayout: 横並びまたは縦並び。重み付け (layout_weight) を用いて空間配分が可能です。

RelativeLayout: 他の View や親基準での相対位置指定。複雑な配置に適していますが、オーバーレイレイアウトの方が推奨される場合があります。

FrameLayout: 重ね合わせ配置。全ての要素が左上に配置されるため、重ねた表示に使われます。

3.3 リスト表示の最適化

大量データを表示するにはリストビュー系のコンポーネントが必須です。

ListView と Adapter

データモデルとアダプターを介して View と結びつけます。

data class FruitItem(val name: String, val resId: Int)

class FruitListAdapter(private val context: Context, private val items: List<FruitItem>) 
    : ArrayAdapter<FruitItem>(context, R.layout.item_fruit, items) {
    
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val holder: ViewHolder
        val view: View
        
        if (convertView == null) {
            view = LayoutInflater.from(context).inflate(R.layout.item_fruit, parent, false)
            holder = ViewHolder(view.findViewById(R.id.img_item), view.findViewById(R.id.txt_name))
            view.tag = holder
        } else {
            view = convertView
            holder = view.tag as ViewHolder
        }
        
        val currentItem = getItem(position)
        if (currentItem != null) {
            holder.image.setImageResource(currentItem.resId)
            holder.text.text = currentItem.name
        }
        return view
    }

    private inner class ViewHolder(val image: ImageView, val text: TextView)
}

RecyclerView

よりモダンで効率的なリスト管理です。Dependency に追加が必要です。

// build.gradle.kts
implementation("androidx.recyclerview:recyclerview:1.3.0")

アダプターの作成では ViewHolder パターンが標準的です。

class ProductAdapter(val products: List<String>) : 
    RecyclerView.Adapter<ProductAdapter.Holder>() {
    
    inner class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val title: TextView = itemView.findViewById(R.id.title_text)
    }

    override fun onBindViewHolder(holder: Holder, position: Int) {
        holder.title.text = products[position]
    }

    override fun getItemCount(): Int = products.size
    
    // 内部クリックイベントの実装例
    inner class ItemClickListener {
        fun onItemClick(pos: Int) { /* 処理 */ }
    }
}

3.4 カスタムビュー

複合的な UI を部品化します。LinearLayout を継承し、内部でレイアウトを読み込むパターンが一般的です。

4. フラグメントの利用

フラグメントはアクティビティの一部として振る舞う UI モジュールです。

4.1 静的・動的な実装

レイアウト内にタグで定義するか、コード上で動的に追加できます。

private fun switchFragment(fragment: Fragment) {
    supportFragmentManager.beginTransaction().apply {
        replace(R.id.container_id, fragment)
        addToBackStack(null) // 戻るボタンの動作を保証
        commit()
    }
}

4.2 通信手法

  • Activity から Fragment へ: findFragmentById などを使用して直接参照。
  • Fragment から Activity へ: requireActivity() 経由で親コンテキストを取得。

ライフサイクルにおいて、onAttach(), onCreateView(), onDestroyView(), onDetach() 等のメソッドが順次呼び出されます。

5. 背景プロセスとデータ共有

5.1 サービス (Service)

ユーザー操作に依存しないバックグラウンドタスクを実行します。

class DownloadService : Service() {
    override fun onBind(intent: Intent?): IBinder? = null // Started Service として起動する場合

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 作業開始
        return START_STICKY
    }
}

バインド型サービスの場合は IBinder を返却し、アクティビティと接続します。

5.2 ブロードキャスト (BroadcastReceiver)

システムイベントやアプリ間通知を受け取ります。

動的登録: コンポーネント生存中にのみ有効。

private var receiver: TimeReceiver? = null

override fun onResume() {
    super.onResume()
    val filter = IntentFilter("ACTION_TIME_UPDATE")
    receiver = TimeReceiver()
    registerReceiver(receiver, filter)
}

override fun onPause() {
    super.onPause()
    receiver?.let { unregisterReceiver(it) }
    receiver = null
}

静的登録: マニフェストにて定義。Android 8.0 以降、多くの隠式ブロードキャストの受信制限があります。

5.3 コンテンツプロバイダー

異なるアプリ間でのデータ共有を安全に行うための中間層です。ContentResolver を介して CRUD 操作を行います。

6. 永続化とネットワーク

6.1 データ保存

  • ファイル: openFileOutput() で内部ストレージに直接保存。
  • SharedPreferences: キー値ペア形式の軽量設定保存。Boolean, Int 等に対応。
  • Room (SQLite): オブジェクト指向データベース抽象化ライブラリ。
val prefs = getPreferences(MODE_PRIVATE)
prefs.edit().putString("user_key", "alice").apply()

6.2 ネットワーク通信

WebView を埋め込むことで Web ブラウズ機能を統合可能です。

HTTP リクエストには OkHttpRetrofit が多用されます。

interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: Int): UserResponse
}

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .build()
    
val service = retrofit.create(ApiService::class.java)

明文 HTTP トラフィックを許可する場合は Network Security Config の設定が必要です。

7. モダン UI と Jetpack コンポーネント

7.1 Material Design

最新のデザインガイドラインに従った UI 構築を支えるツールキットです。

  • Toolbar: アクションバーの代替として使用。
  • FloatingActionButton: 主要アクションを浮動させる。
  • DrawerLayout: ナビゲーションスライドパネル。
  • CoordinatorLayout: 子ビュー間の協調動作を制御。

7.2 Jetpack Architecture

ライフサイクルを意識したアーキテクチャ構成を支援します。

  • ViewModel: 画面データを保持し、回転などの設定変更時に破棄されない。by viewModels()`delegate を使用できる。
  • LiveData: UI がアクティブな時のみ通知を送るデータオブザーバー。MutableLiveData で値を設定し、公開用には不変版を使用する。
class CounterVM : ViewModel() {
    private val _count = MutableLiveData(0)
    val count: LiveData<Int> get() = _count

    fun increment() {
        _count.value = (_count.value ?: 0) + 1
    }
}

// Activity 側
viewModel.count.observe(viewLifecycleOwner) { newValue ->
    textView.text = newValue.toString()
}
  • Room: SQLite の簡易化。
  • WorkManager: 保証されたバックグラウンドタスク実行。

タグ: Android Kotlin Gradle Activity Fragment

5月30日 08:34 投稿