Jetpack Compose 初期+簡素UIコード解説〜基本概念

初期コード解説

① Activityを作る

class MainActivity : ComponentActivity() {

これは

「MainActivityという画面を作ります」

という意味です。

ComponentActivity

Androidの画面の基本クラスです。

昔は

AppCompatActivity

を使うことが多かったですが、

Jetpack Composeでは ComponentActivity が標準です。

② onCreate()

override fun onCreate(savedInstanceState: Bundle?) {

画面が最初に表示されるときに呼ばれる関数です。

例えば

  • 初期化
  • ボタンの設定
  • 画面を作る

などをここで行います。

③ 親クラスの初期化

super.onCreate(savedInstanceState)

これは

「ComponentActivityの初期化もちゃんと実行してください」

という意味です。

これを書かないとActivityが正常に動きません。

④ Edge-to-Edge

enableEdgeToEdge()

Android15以降で推奨されている設定です。

画面を

┌───────────────┐
│ステータスバー│
├───────────────┤
│ │
│ アプリ │
│ │
├───────────────┤
│ナビゲーション│
└───────────────┘

ではなく

┌───────────────┐
│ステータスバー│
│ │
│ アプリ │
│ │
│ │
│ナビゲーション│
└───────────────┘

のように、

画面いっぱいまで描画する設定です。

iPhoneのSafe Areaに少し似ています。

⑤ Compose開始

setContent {

ここが一番重要です。

これは

「ここからComposeで画面を書きます」

という意味です。

昔のAndroidなら

setContentView(R.layout.activity_main)

でした。

ComposeではXMLを使わないため、

setContent { ... }

の中に画面を書きます。

⑥ テーマ

Teat1Theme {

これはアプリ全体のデザインです。

例えば

  • フォント
  • 角丸
  • Material Design

などをまとめています。

SwiftUIなら

Environment

.tint()

に少し近いイメージです。

テーマを外しても画面は表示できますが、

Material Designらしい見た目ではなくなります。

⑦ Scaffold

Scaffold(

Scaffoldは

「画面の骨組み」

です。

例えば

┌────────────────────┐
│TopAppBar │
├────────────────────┤
│ │
│ │
│ コンテンツ │
│ │
├────────────────────┤
│BottomBar │
└────────────────────┘

を簡単に作れるコンポーネントです。

後から

  • TopAppBar
  • BottomBar
  • FAB(丸いボタン)
  • Snackbar

などを追加できます。

⑧ Modifier.fillMaxSize()

modifier = Modifier.fillMaxSize()

Modifierは

「見た目や配置を変更するもの」

です。

ここでは

fillMaxSize()

なので

画面いっぱいに広げる

という意味になります。

例えば

200×200

ではなく

画面全体

になります。

⑨ innerPadding

{ innerPadding ->

Scaffoldは

例えば

TopAppBar

BottomBar

の高さを考慮して

この分だけ余白を空けてね

という値を渡してくれます。

それが

innerPadding

です。

⑩ Greeting

Greeting(

これは自分で作ったComposable関数です。

例えば

@Composable
fun Greeting(name: String)

が別の場所にあります。

SwiftUIでいう

GreetingView()

のようなものです。

⑪ 引数

name = "Android",

Greetingへ

"Android"

という文字列を渡しています。

中では

Text("Hello $name")

のように使われます。

⑫ Padding

modifier = Modifier.padding(innerPadding)

ここはとても重要です。

Scaffoldから受け取った余白を適用しています。

もし書かなければ

┌──────────────┐
│TopAppBar │
│Hello Android │ ← 重なる
└──────────────┘

になってしまいます。

書くと

┌──────────────┐
│TopAppBar │
├──────────────┤
│Hello Android │
└──────────────┘

となり、バーの下からコンテンツが始まります。

簡素UIのコード解説

完成イメージはこんな感じです。

┌──────────────────────────┐
│ │
│ │
│ [ 画像 ] │
│ │
│ [ボタン1] [ボタン2] │
│ │
└──────────────────────────┘

① 画像を用意する

app > src > main > res > drawable

の中に画像を置きます。

例えば

logo.png

という名前にします。

ファイル名は 小文字・数字・アンダースコアのみ にしてください。

logo.png
app_icon.webp
title_image.png

② MainActivity

setContent の中を次のようにします。

setContent {
Teat1Theme {
MainScreen()
}
}

これで画面を MainScreen() に任せます。


③ MainScreenを作る

④ 必要なimport

Android Studioならほとんど自動で追加してくれますが、主にこれらを使っています。

import自動化の設定

コードの見方

まず画面全体を作っています。

Column(...)

Columnは





へ並べるレイアウトです。

画面中央に配置

verticalArrangement = Arrangement.Center

これが


中央

へ移動させています。

横方向も中央

horizontalAlignment = Alignment.CenterHorizontally

これで

← 中央 →

になります。

画像表示

Image(
painter = painterResource(id = R.drawable.logo),
...
)

これが

res/drawable/logo.png

を表示しています。

SwiftUIなら

Image("logo")

とほぼ同じイメージです。

サイズ指定

Modifier.size(180.dp)

180×180dpで表示します。

Spacer

Spacer(
modifier = Modifier.height(32.dp)
)

これは

画像

(32dp空ける)

ボタン

という余白です。

SwiftUIなら

Spacer()

ではなく

.padding(.bottom, 32)

に近い役割です。

横並び

Row

Rowは

□□□□

と横に並べます。

SwiftUIなら

HStack

です。

ボタン間隔

Arrangement.spacedBy(16.dp)

[ボタン1]    [ボタン2]

のように16dp空きます。

上記コードに画面遷移を追加

完成イメージはこんな感じになります。

MainScreen
┌──────────────────┐
│ │
│ 画像 │
│ │
│ [ボタン1][ボタン2]│
└──────────────────┘

│ ボタン1

SettingsScreen
┌──────────────────┐
│ │
│ [ON/OFFスイッチ] │
│ │
│ test1 │
│┌────────────────┐│
││入力してください││
│└────────────────┘│
└──────────────────┘

① Navigation Composeを追加

build.gradle.kts (Module: app) に追加します。

同期(Sync Now)してください。

② MainActivity

setContent {} を次のように変更します。

③ Navigationを作成

新しく AppNavigation.kt を作ります。

ここでは

  • "main" → 最初の画面
  • "settings" → 遷移先

を登録しています。

④ MainScreenを変更

引数に NavController を追加します。

ポイントはここです。

navController.navigate("settings")

この一行だけで画面遷移できます。

⑤ SettingsScreenを作る

⑥ 必要なimport

Android Studioがほぼ自動で追加してくれます。

このコードで初めて出てくるもの

remember

var checked by remember {
mutableStateOf(false)
}

これは

「値を覚えておいてください」

というComposeの仕組みです。

例えば

false

true

false

と変更されても画面が更新されます。

SwiftUIなら

@State private var checked = false

とほぼ同じ役割です。

mutableStateOf

mutableStateOf("")

画面更新の対象になる変数です。

普通の

var text = ""

では画面は更新されません。

Composeでは

var text by remember {
mutableStateOf("")
}

とすることで、値が変わるたびにUIが自動で再描画されます。

OutlinedTextField

OutlinedTextField()

これは枠付きのテキスト入力欄です。

表示は

test1
┌────────────────────┐
│入力してください │
└────────────────────┘

となります。

戻るボタンとテキストやスイッチの状態を保存するコードの追加

① DataStoreを追加

build.gradle.kts

Syncしてください。

② DataStoreManager.kt

新しく作成します。

これだけで保存と読み込みができます。

③ SettingsScreen

引数を変更します。

DataStoreを取得

Flowを監視

Composeでは

val checked by dataStore.checkedFlow.collectAsState(false)

val text by dataStore.textFlow.collectAsState("")

これだけで

保存値

UI

が自動同期されます。

これがComposeの便利なところです。

④ TopAppBarを追加

画面全体をScaffoldで囲みます。

戻るボタンは

navController.popBackStack()

だけです。

⑤ Switch

保存すると

DataStore

Flow

画面更新

が全部自動です。

⑥ TextField

入力するたび保存されます。

⑦ Navigation修正

MainScreenは変更ありません。

もう少しComposeらしく書く方法

ここまでで十分動作しますが、Composeでは**CoroutineScope(Dispatchers.IO)を毎回作る書き方はあまり一般的ではありません**。

通常は

val scope = rememberCoroutineScope()

として、

scope.launch {
dataStore.saveChecked(it)
}

のように書くことが多いです。

上記のDataStoreManagerの解説

この DataStoreManager は最初は少し難しく感じますが、**「設定ファイルを読み書きするクラス」**だと思うと理解しやすいです。

まず全体像から見てみましょう。

SettingsScreen

│ saveChecked(true)

DataStoreManager


DataStore(settings.preferences_pb)


checkedFlow


SettingsScreen

つまり、

  • SettingsScreen が保存をお願いする
  • DataStoreManager がDataStoreへ保存
  • DataStoreが変更される
  • checkedFlow が変更を通知
  • 画面が自動更新

という流れです。

① DataStoreを作る

private val Context.dataStore by preferencesDataStore("settings")

これが一番分かりにくいですね(笑)

実は

Context.dataStore

という新しいプロパティを追加しています。

イメージすると

val context: Context

context.dataStore

が使えるようになります。

例えば

context.dataStore.edit { }

のように書けるようになります。

"settings"

preferencesDataStore("settings")

これは保存ファイル名です。

実際には内部で

settings.preferences_pb

というファイルになります。

SwiftUIなら

UserDefaults.standard

に近い存在です。

② クラス

class DataStoreManager(private val context: Context)

ここは

Contextを受け取ってDataStoreを操作するクラス

という意味です。

例えば

val manager = DataStoreManager(context)

とすると

manager

の中で

context.dataStore

が使えます。

③ companion object

companion object {

これは

クラス共通の定数置き場

です。

Javaなら

public static

Swiftなら

static let

に相当します。

④ キーを作る

val CHECKED = booleanPreferencesKey("checked")

これは

checked

という名前で保存するという意味です。

例えばDataStoreの中は

checked = true

になります。

もう一つは

val TEXT = stringPreferencesKey("text")

なので

text = "こんにちは"

のように保存されます。

つまり

DataStore

checked = true

text = Hello

という辞書(Dictionary)みたいなものになります。

⑤ checkedFlow

val checkedFlow =
context.dataStore.data.map {
it[CHECKED] ?: false
}

ここは一番重要です。

まず

context.dataStore.data

DataStoreの内容

を監視しています。

つまり

変更があれば教えてね

という状態です。

次の

map {

欲しい値だけ取り出す

という意味です。

例えばDataStoreが

checked = true

text = abc

だったら

checkedだけ欲しい

となります。

it 説明リンク

it[CHECKED]

it

DataStore全体

です。

つまり

it

├ checked
└ text

になります。

そこから

it[CHECKED]

checkedだけ取得

しています。

?: false

it[CHECKED] ?: false

これは

もし存在しなければfalse

という意味です。

保存したことが無い最初の起動では

checked

はありません。

なので

false

を返します。

⑥ textFlow

こちらも同じです。

val textFlow =
context.dataStore.data.map {
it[TEXT] ?: ""
}

最初だけ

空文字

になります。

⑦ suspend

suspend fun saveChecked(...)

これは

時間の掛かる処理

という意味です。

保存処理は

  • ファイルを開く
  • 書き込む
  • 閉じる

という作業なので、

普通の関数ではなく

suspend

になります。

Swiftなら

async

とかなり似ています。

⑧ edit

context.dataStore.edit {

これは

設定を書き換えます

という意味です。

中のit

it[CHECKED] = value

この

it

Preferences

です。

つまり

設定一覧

になります。

例えば

変更前

checked = false

text = Hello

変更後

checked = true

text = Hello

になります。

⑨ saveText

it[TEXT] = value

こちらも同じです。

例えば

value = "Android"

なら

checked = true

text = Android

になります。

全体の流れ

例えばスイッチをONにすると

Switch


saveChecked(true)


DataStore.edit


checked = true


checkedFlowが変更を検知


Composeが再描画


SwitchがONになる

MVVMの概念

まず、現在の問題点

今の SettingsScreen はこんな仕事をしています。

SettingsScreen

├ UIを表示
├ DataStoreを作る
├ 保存する
├ 読み込む
└ 状態を管理する

つまり…

仕事が多すぎます。

本来UIは

「表示するだけ」

にしたいです。

MVVMとは?

MVVMは役割を分けます。

          ユーザー


SettingsScreen
(View)


SettingsViewModel
(状態管理)


DataStoreManager
(データ保存)

役割はこのようになります。

View(画面)

・表示するだけ
・ボタンが押されたことを知らせる

ViewModel

・画面の状態を持つ
・保存する
・読み込む
・UIへ通知する

Model

今回は

DataStoreManager

です。

保存専門です。

ディレクトリ構成

私は最初からこの構成にすることをおすすめします。

com.example.test

MainActivity.kt

navigation/
AppNavigation.kt

ui/
MainScreen.kt
SettingsScreen.kt

viewmodel/
SettingsViewModel.kt

data/
DataStoreManager.kt

SwiftUIでもフォルダを分けていましたよね。

それとほぼ同じです。

ViewModelを作る

まず

viewmodel/

フォルダを作ります。

その中に

SettingsViewModel.kt

このコードの仕事

例えば

fun setChecked(value: Boolean)

画面から

ONになりました!

と言われたら

DataStoreへ保存

するだけです。

つまり

View



ViewModel



DataStore

です。

viewModelScope

これもComposeで超重要です。

以前は

CoroutineScope(Dispatchers.IO).launch {

}

と書いていました。

でもViewModelでは

viewModelScope.launch {

}

だけです。

理由は

ViewModelが破棄されると

Coroutineも自動終了

するからです。

メモリリークを防げます。

stateIn

これもよく出てきます。

DataStoreは

Flow

でした。

でもUIは

State

が欲しいです。

なので

Flow



StateFlow

へ変換しています。

これが

stateIn(...)

です。

SettingsScreen

すると画面はかなりシンプルになります。

画面は

表示だけ

になりました。

保存のことを知りません。

処理の流れ

例えば

スイッチをONにすると

Switch


viewModel.setChecked(true)


DataStore.saveChecked(true)


DataStore更新


checkedFlow更新


checked StateFlow更新


画面更新

すごくきれいですね。

Repositoryの概念

Repositoryとは?

一言でいうと

「データの窓口」

です。

現在の構成はこうでした。

SettingsScreen


SettingsViewModel


DataStoreManager

ViewModelが直接DataStoreを触っていますね。

これの問題

例えば将来、

設定を

  • DataStore
  • Room
  • Web API

の3か所から取得したいとします。

するとViewModelは

SettingsViewModel

├ DataStore
├ Room
└ API

全部知ることになります。

これではViewModelの仕事が増えすぎます。

Repositoryを入れる

そこでRepositoryを1枚挟みます。

SettingsScreen


SettingsViewModel


SettingsRepository

┌──────┴─────────┐
▼ ▼
DataStore Room


API

ViewModelは

Repositoryだけ知っていればOK

になります。

Repositoryの役割

Repositoryは

データはどこから来たか

を隠す

役目です。

例えば

viewModel.setChecked(true)

repository.saveChecked(true)

Repositoryが

DataStoreへ保存

するのか

Roomへ保存

するのか

APIへ送信

するのか

ViewModelは知りません。

フォルダ構成

この辺からAndroidらしくなります。

data/

DataStoreManager.kt

repository/

SettingsRepository.kt

viewmodel/

SettingsViewModel.kt

ui/

SettingsScreen.kt

Repositoryを書いてみる

まず

repository/

を作ります。

中に

SettingsRepository.kt
class SettingsRepository(

private val dataStore: DataStoreManager

) {

val checkedFlow = dataStore.checkedFlow

val textFlow = dataStore.textFlow

suspend fun saveChecked(value: Boolean) {

dataStore.saveChecked(value)

}

suspend fun saveText(value: String) {

dataStore.saveText(value)

}

}

え?

これだけ?

と思いますよね(笑)

最初はこれだけです。

ViewModel変更

今までは

private val dataStore: DataStoreManager

でした。

これを

private val repository: SettingsRepository

へ変更します。

読み込みも

repository.checkedFlow

になります。

保存も

repository.saveChecked(value)

になります。

DataStoreを交換してもViewModelは変わらない

例えば将来

DataStoreではなく

Roomへ変更したい。

Repositoryだけ

suspend fun saveChecked(value: Boolean){

roomDao.save(value)

}

へ変更します。

ViewModelは

viewModel.setChecked(true)

のままです。

UIも変更なし。

Repositoryが一番活躍する例

例えば

プロフィール画面。

名前

メール

年齢

を表示します。

データは

Room

にあります。

でも

ネットに新しいデータがあるか確認



あれば更新



Roomへ保存



Roomから読む

という流れにしたい。

Repositoryなら

Repository



APIから取得



Roomへ保存



Roomから取得



ViewModelへ返す

と全部まとめられます。

ViewModelは

repository.loadProfile()

しか知りません。

SwiftUIとの比較

SwiftUIでも

View



ViewModel



Repository



API

という構成を採用しているプロジェクトは多いです。

なので考え方はほぼ同じです。

「これだけならRepositoryいらないのでは?」

その通りです。

今回のサンプルでは

DataStoreManager

だけでも十分です。

しかし実際のアプリでは、

  • 設定をクラウドと同期する
  • ローカルキャッシュを持つ
  • API通信に失敗したらローカルデータを返す
  • 複数のデータソースを組み合わせる

といったことが増えてきます。

そのときRepositoryがあると、ViewModelは「データの取得・保存をRepositoryに依頼する」だけで済みます。

Hilt(依存性注入)

まず「依存性注入(DI)」って何?

名前が難しいですが、実はやっていることはシンプルです。

今までのコードでは、自分で必要なものを作っていました。

val dataStore = DataStoreManager(context)

val repository = SettingsRepository(dataStore)

val viewModel = SettingsViewModel(repository)

図にすると

自分で作る

DataStoreManager


SettingsRepository


SettingsViewModel

これを**依存性注入(Dependency Injection:DI)**と呼ばれる仕組みで自動化します。

Hiltを使うと…

自分では何も作りません。

val viewModel: SettingsViewModel = hiltViewModel()

これだけ。

Hiltが裏で

DataStoreManager



SettingsRepository



SettingsViewModel

全部作ってくれます。

なぜ便利なの?

例えばRepositoryが大きくなると…

class SettingsRepository(

private val dataStore: DataStoreManager,

private val api: ApiService,

private val room: SettingsDao,

private val logger: Logger,

private val analytics: Analytics

)

ViewModelを作るだけでも

SettingsViewModel(

SettingsRepository(

DataStoreManager(context),

ApiService(),

Room.databaseBuilder(...),

Logger(),

Analytics()

)

)

になってしまいます。

これ、大変ですよね(笑)

Hiltなら

ViewModelください



はい!



ViewModel



Repository



DataStore



API



Room

全部勝手に準備してくれます。

Hiltの仕組み

Hiltは

「どうやって作るか」

だけ教えてあげます。

① Application

まず

@HiltAndroidApp
class MyApplication : Application()

を作ります。

これは

このアプリはHiltを使います!

という宣言です。

AndroidManifest.xml

<application
android:name=".MyApplication"

も追加します。

② build.gradle

依存関係を追加します。

implementation("com.google.dagger:hilt-android:2.xx")
ksp("com.google.dagger:hilt-compiler:2.xx")

ComposeではKSP版を使うことが多いです。

③ DataStoreManager

ここが面白いです。

今までは

class DataStoreManager(

private val context: Context

)

でした。

Hiltでは

class DataStoreManager @Inject constructor(

@ApplicationContext
private val context: Context

)

になります。

たった

@Inject

が付いただけ。

意味は

「Hilt、このクラスは作っていいですよ。」

です。

④ Repository

こちらも

class SettingsRepository @Inject constructor(

private val dataStore: DataStoreManager

)

になります。

するとHiltは

Repository作るには



DataStoreが必要



DataStoreも作れる



OK

となります。

⑤ ViewModel

ここも

@HiltViewModel
class SettingsViewModel @Inject constructor(

private val repository: SettingsRepository

) : ViewModel()

になります。

ここまで来ると

ViewModel



Repository



DataStore

全部自動生成できます。

⑥ Activity

Activityには

@AndroidEntryPoint
class MainActivity : ComponentActivity()

を付けます。

これで

val viewModel: SettingsViewModel by viewModels()

が使えます。

Composeなら

val viewModel: SettingsViewModel = hiltViewModel()

です。

全体の流れ

今までは

MainActivity



new ViewModel



new Repository



new DataStore

でした。

Hiltでは

MainActivity



ViewModelください



Hilt



DataStore作成



Repository作成



ViewModel作成



返却

になります。

SwiftUIとの比較

SwiftUIにも似た考え方があります。

例えば

@EnvironmentObject
var settings: SettingsViewModel

これも

必要なものを自分で作らず、「注入」してもらう

という点では、Hiltと近い発想です。

ただし、HiltはViewModelだけでなく、RepositoryやDataStoreなどの依存関係までまとめて管理できるのが大きな違いです。

「@」がたくさん出てきた…

Android初心者の多くがここで戸惑います(笑)

Hiltでは

@HiltAndroidApp
@AndroidEntryPoint
@Inject
@HiltViewModel
@ApplicationContext

とアノテーション(@が付くもの)が増えます。

最初は「おまじない」のように見えますが、それぞれ役割があります。

アノテーション役割
@HiltAndroidAppアプリ全体でHiltを有効にする
@AndroidEntryPointこのActivityやFragmentでHiltを使う
@InjectこのクラスやコンストラクタはHiltが生成できる
@HiltViewModelこのViewModelはHiltが管理する
@ApplicationContextアプリ全体のContextを注入する

Room

まずSQLiteとは?

Androidには昔から SQLite というデータベースが入っています。

例えばメッセージアプリなら、

Message

id | user | text
--------------------------
1 | A | こんにちは
2 | B | おはよう
3 | A | 元気?

このような表(テーブル)としてデータを保存できます。

昔はSQLiteを直接操作していました。

db.execSQL(...)
db.rawQuery(...)

しかし、

  • SQLを書く必要がある
  • 型安全ではない
  • コードが長くなる

という欠点がありました。

Roomとは?

Roomは一言でいうと

SQLiteを簡単・安全に扱うためのライブラリ

です。

つまり

Compose

ViewModel

Repository

Room

SQLite

RoomがSQLiteを包んでくれています。

SwiftUIでいうと

Core Data や SwiftData に近い存在だと思うとイメージしやすいです。

DataStoreとの違い

ここが重要です。

DataStoreRoom
設定保存データ保存
bool
String
一覧データ
検索
並び替え
テーブル
SQL
メッセージ保存
ゲームセーブ

例えば

ダークモード ON

ならDataStore。

でも

メッセージ1000件

ならRoomです。

Roomの基本は3つ

Roomは次の3つだけ覚えれば始められます。

Entity



DAO



Database

① Entity

Entityは

テーブル定義

です。

例えば

@Entity
data class Message(

@PrimaryKey(autoGenerate = true)
val id: Int = 0,

val user: String,

val text: String

)

これだけで

SQLiteには

Message

id

user

text

というテーブルが作られます。

Swiftなら

struct Message

に近い存在です。

② DAO

DAOは

データの操作方法

です。

例えば

@Dao
interface MessageDao {

@Insert
suspend fun insert(
message: Message
)

@Query("SELECT * FROM Message")
fun getAll(): Flow<List<Message>>

}

これだけで

dao.insert(...)

dao.getAll()

が使えます。

SQLは

SELECT * FROM Message

だけ書けばOKです。

③ Database

最後に

@Database(

entities = [Message::class],

version = 1

)

abstract class AppDatabase : RoomDatabase() {

abstract fun messageDao(): MessageDao

}

を書きます。

これで

AppDatabase



MessageDao



Message

がつながります。

Repository

ここからがMVVMらしいところです。

Repositoryは

class MessageRepository(

private val dao: MessageDao

)

となります。

保存は

suspend fun addMessage(

message: Message

){

dao.insert(message)

}

読み込みは

val messages = dao.getAll()

だけです。

ViewModel

ViewModelは

class MessageViewModel(

repository: MessageRepository

)

になります。

val messages =
repository.messages

画面は

viewModel.messages

だけ見ればOK。

Compose

画面では

val messages by
viewModel.messages.collectAsState()

として、

LazyColumn {

items(messages){

Text(it.text)

}

}

これだけで

こんにちは

おはよう

元気?

が表示されます。

全体の流れ

メッセージを送信すると

Button



ViewModel



Repository



DAO



Room



SQLite

になります。

逆に読み込むと

SQLite



Room



DAO



Repository



ViewModel



Compose

になります。

コメント

タイトルとURLをコピーしました