Memo

メモ > 技術 > IDE: AndroidStudio

■アカウント
■Google Play Console https://play.google.com/apps/publish/?hl=ja アプリを公開するためのもの ■Firebase https://console.firebase.google.com/ プッシュなどを利用する場合に使用
■AndroidStudio環境の作成
■公式ページ Download Android Studio and SDK tools | Android Developers https://developer.android.com/studio/ ■インストール参考ページ(Flamingo時点) 以下のとおり作業した 【2023年版】Android Studioのインストール方法(Windows&Mac対応)|Code for Fun https://codeforfun.jp/how-to-install-android-studio-windows-and-mac/ また IntelliJ IDEA + Android Studio でインストールする場合、 1. jetBrains Toolbox で「Android Studio」の「インストール」をクリック 2. ライセンスが表示されるので、同意してインストールする 3. インストールが完了したら開く とする 以降は上記ページの内容と同じ ■インストール参考ページ(Ver 3.1.4 時点) WindowsもMacも、インストールで詰まることは無かった インストール完了後も、何かと追加で色々とダウンロード&インストールさせられる エミュレータも、初回起動時はイメージのダウンロードが必要 Ver 3.1.4 時点では、主に以下の書籍を参考にした 基本からしっかり身につくAndroidアプリ開発入門 https://www.amazon.co.jp/dp/479739580X ■インストール参考ページ(Ver 3.1.2 時点) Android Studioインストール https://akira-watson.com/android/adt-windows.html JDKインストール http://techfun.cc/java/windows-jdk-install.html http://techfun.cc/java/windows-jdk-pathset.html ■インストールメモ(Ver 2.3.3 時点) http://www.oracle.com/technetwork/java/javase/downloads/index.htmlhttp://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html から jdk-8u91-windows-x64.exe をダウンロード&インストール https://developer.android.com/studio/ から android-studio-bundle-143.2821654-windows.exe をダウンロード&インストール Android Studio の起動時にエラーが表示されたことがあったが、JavaとJDKの再インストールで直った。 SDK Manager で、Android 4.3 〜 7.1.1 までインストール。 ■日本語化(Flamingo時点) 日本語化の記事はすぐに見つけられなかった 代わりに、日本語化しようとしたら起動できなくなったという記事は見つかった いったん英語版のまま使っておく AndroidStudioをPleiadesで日本語化しようとしたら起動できなくなった(地獄みた) - Qiita https://qiita.com/celcior0913/items/d68606982e429f8b1a60 ■日本語化(Ver 3.3.2 時点) ※日本語化は Android Studio 終了中に実行しないと反映されないことがあるかも ※毎回バージョンアップが手間なので、Ver 4 からは英語のまま使っている 以下の方法で日本語化できた studio64.exe.vmoptions は初めから config 内にあった Android Studio3.1を日本語化する | HIROMARTBLOG http://hiromart.com/blog/androidstudiojapanese/ Macの場合、以下の手順で日本語化できた MacでAndroid Studioを日本語化する - ソフラボの技術ブログ http://shinsuke789.hatenablog.jp/entry/2018/08/27/130714 ■日本語化(Ver 2.3.3 時点) 以下の方法で日本語化できた。一部は英語のままだけど特に問題はなし Android Studio 2.0 を日本語化してみた | 寿司すき焼き相撲 http://s3wordpress.wpblog.jp/2016/05/18/android-studio-2-0-%E3%82%92%E6%97%A5%E6%9C%AC%E8%AA%9E%E5%8... ■ショートカットの変更 Ctrl+Yは、Redoではなく行削除が割り当てられている 一般的なショートカットと異なるので余計なトラブルのもとだが、設定で変更はできる http://qiita.com/decchi/items/f8603ccccec03a71a4d9 ■エディタの設定 ファイル → 設定 → Editor → 外観 行番号を表示する 空白を表示する ■デバッグ 画面下部の「ログキャット」をクリックすると、デバッグログを確認できる 端末、アプリ、デバッグレベルなどを絞り込んで確認する ■メモリの割り当て ※現状未設定 http://tools.android.com/tech-docs/configuration
■AndroidStudioのバージョンアップ
http://www.youfit.co.jp/archives/885 Ver 2.1.2 → Ver 2.2.3 のとき、この手順でバージョンアップできた Ver 2.2.3 → Ver 3.1.2 のとき、この手順でバージョンアップできた。が、メニューが英語表記に戻った Ver 3.1.2 → Ver 3.2.0 のとき、この手順でバージョンアップできた。が、メニューが英語表記に戻った ■AndroidStudioが起動しなくなった場合 Macで発生 アップデートして使えているかと思ったが、Mac再起動後はAndroidStudioを起動できなくなった エラーなどは何も確認できない 最新の日本語化パッチを当てると起動できるようになった AndroidStudioがアップデート後に起動しない時 - Qiita https://qiita.com/filu_/items/6470ab95b45a4382e34e ■メニューが英語表記に戻った場合 「AndroidStudio環境を作った時のメモ」の「日本語化」の手順を再度行って日本語化する Ver 2.3.3 時点のメモだが、同じ手順で日本語化できた ■Kotlin Gradle plugin version のエラーになった場合 The Android Gradle plugin supports only Kotlin Gradle plugin version 1.2.51 and higher. Project 'HelloKotlin' is using version 1.2.30. と表示された場合、指示に従ってGradleファイルを書き換える C:\Users\Refirio\AndroidStudioProjects\HelloKotlin\build.gradle ext.kotlin_version = '1.2.30' ↓ ext.kotlin_version = '1.2.51' 書き換えたら「再試行」を実行する ■AndroidStudioをベータ版から正式版にしたときのメモ 以下からAndroidStudioをダウンロード&インストール https://developer.android.com/studio/
■Gitの連携
■SourceTreeとの連携 AndroidStduio内でgit操作は可能らしいが、SourceTreeで管理する方法 http://ameblo.jp/hunnyjams/entry-11961454576.html gitのパスは以下のようになる。環境によって変わる可能性はある C:\Program Files\Git\cmd\git.exe AndroidStudioが build/ や app/build/ などに自動でファイルを吐き出すので、 可能なら最初の段階で .gitignore の対象にしておく。一度リポジトリに入れてしまうと、後から外すのは面倒 今は何もしなくてもAndroidStudioが .gitignore を自動作成してくれるみたい? ■.gitignore AndroidStudioが自動で作成するかも。要確認 以下は設定例
*.iml .gradle /local.properties /.idea/caches/build_file_checksums.ser /.idea/libraries /.idea/modules.xml /.idea/workspace.xml .DS_Store /build /captures .externalNativeBuild
以下を参考に作成すると良さそう Androidアプリの.gitignore - Qiita https://qiita.com/jumperson/items/fa66995fb68de2847ffd Android Studioのバージョン管理対象ファイル - ソフトウェアエンジニアリング - Torutk http://www.torutk.com/projects/swe/wiki/Android_Studio%E3%81%AE%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%...
■プロジェクトの作成
■プロジェクトの新規作成(Flamingo時点) 以下の手順で作成できる プロジェクトが作成されたら、何も変更せずにいったんエミュレータと実機での起動を確認しておくといい Welcome画面で「Projects → New Project」を選択 「Empty Activity」を選択して「Next」をクリック 以下のとおり入力 Name: Hello World Package name: net.refirio.helloworld(「Name」から自動で決定されるが、必要に応じて調整する) Save location: C:\Users\refirio\AndroidStudioProjects\HelloWorld(任意の場所を選択できるが、基本的にそのままでいい) Minimum SDK: API 24 Android 7.0 Nougat(必要に応じて調整する) 「Finish」をクリック プロジェクトが作成されるのでしばらく待つ 完了したら「Finish」をクリック ※「言語」の選択欄は表示されなかった もうJavaではなくKotlinでの開発が大前提となっているのか ■プロジェクトの新規作成(Ver 3.3.2 時点) 以下の手順で作成できる プロジェクトが作成されたら、何も変更せずにいったんエミュレータと実機での起動を確認しておくといい 新規 Android Studio プロジェクトの開始 ↓ プロジェクトの選択 「空のアクティビティ」が選択されているので、そのまま「次へ」 ↓ プロジェクトの構成 名前: helloworld パッケージ名: net.refirio.helloworld (名前をもとに自動入力される) 保存ロケーション: C:\Users\refirio\AndroidStudioProjects\helloworld (アプリケーション名をもとに自動入力される) 言語: Kotlin 最小APIレベル: API 19: Android 4.4 (KitKat) 「完了」
■エミュレータでの実行
【2023年版】Android Studioエミュレータの作成方法|Code for Fun https://codeforfun.jp/android-studio-how-to-install-emulator/ ■デバイスの追加 メニュー → Tool → Device Manager からDevice Managerを起動 (ツールバーもしくは右のサイドバーから「Device Manager」をクリックしても起動できる) デフォルトで「Pixel_3a_API_34_extension_level_7_x86_64」が表示されていた (「Create device」から新しくデバイスを登録することもできる) エミュレータとして使用したいデバイスの起動ボタン(横向きの三角)をクリック 少し待つと、下の欄にデバイスが表示された さらに2〜3分ほど待つと、デバイスが起動してAndroidのホーム画面が表示された 試しにChromeを起動してみる インターネットに接続できることを確認する ■言語の設定 ※設定方法は、端末によって多少異なる 画面の上端から下にスワイプを行う 通知が表示されるが、さらに画面の上端から下にスワイプを行う 画面の右下に歯車アイコンが表示されるのでタップする System → Languages → System Languages に遷移し、「Add a language」から言語を追加する(下の方に「日本語」がある) 追加した「日本語」を一番上にすると、画面の表示が日本語になる ■日時の設定 ※未検証 自動的に日本時間になっていない場合、以下から設定する システム → 日付と時刻 ■インターネット接続の確認 Chromeを起動し、インターネットに接続できることを確認しておく ■デバイスでのアプリ起動 ツールバーで実行ボタン(横向きの三角)をクリック 少し待つと、エミュレータの画面にアプリの画面が表示された 確認できたら、ツールバーで停止ボタン(赤の四角)をタップ ■次回からの起動 ツールバーで実行ボタン(横向きの三角)をクリックするだけで起動できた
■実機での実行
実機でテストを行う場合、直接アプリをインストールするために端末をPCへ接続する ただし、そのままでは正常に認識しないためインストールできないことがある この場合、端末メーカーが提供しているWindows用のUSBドライバーをインストールする AndroidデバイスのUSBドライバーを再インストールする方法【人気10選まとめ】 https://jp.imyfone.com/line-tips/how-to-reinstall-the-usb-driver-on-android/ Android端末をPCに接続する 「USBデバッグを許可しますか?」というダイアログが表示される 「このパソコンからのUSBデバッグを常に許可する」にチェックを入れて「OK」をタップ ツールバーの端末リストに、対象の端末名が表示された 対象の端末名を選択した状態で実行する 少し待つと、実機でアプリが起動した ■トラブル 「Application Installation Failed」というダイアログが表示されたが、 「すでに同じパッケージ名のアプリがインストールされている」 という内容だった 上書きして実行するかどうかの旨も書かれていたので、そのまま「OK」をタップ 少し待つと、実機でアプリが起動した
■Kotlin
■Kotlinファースト 今後ツールやライブラリは、まずKotlin向けが最初に作られるとのこと 可能ならKotlinで作るほうが良さそう (ただしその後、JavaやC++向けのものも作られるとのことなので、他の言語が使えなくなるわけではない) Google、Androidにおける「Kotlinファースト」強化を表明。Google I/O 2019 − Publickey https://www.publickey1.jp/blog/19/googleandroidkotlingoogle_io_2019.html Android の Kotlin ファースト アプローチ | Android デベロッパー | Android Developers https://developer.android.com/kotlin/first?hl=ja ■Kotlin詳細 Kotlinについては Kotlin.txt を参照
■アプリの作成(Jetpack Compose)
■Jetpackについて Android Jetpackとは、アプリを作成するためのコンポーネントやツールなどをひとまとめにしたもの AndroidのOSバージョンアップからは切り離されているので、OSのアップデートを待たずに迅速に対応できるようになっている 一例だが、以下のような機能が提供されている appsearch … ユーザー向けにカスタムのアプリ内検索機能を構築する camera … モバイルカメラアプリを構築する compose … 形状とデータの依存関係を記述するコンポーズ可能な関数を使用して、UIをプログラムで定義する Android Jetpack デベロッパー リソース - Android デベロッパー | Android Developers https://developer.android.com/jetpack?hl=ja Google Developers Japan: Android Jetpack を使用してアプリの開発を加速 https://developers-jp.googleblog.com/2018/05/use-android-jetpack-to-accelerate-your.html Android Jetpackってなにもの? - Qiita https://qiita.com/k_masa777/items/c01c1de6ac763ce5c075 ■Jetpack Composeについて Jetpackで提供されるコンポーネントの一つ Kotlinで宣言的にUIを記述できる(SwiftUIのように簡単にレイアウトできるみたい) 従来のようなXMLではなく、setContentブロック内で「コンポーズ可能な関数」を呼び出してレイアウトを作成する コンポーズ可能な関数は、関数名に「@Composable」アノテーションを追加するだけで作成できる まずは以下の内容を一読するといい UI アプリ開発ツールキット Jetpack Compose - Android デベロッパー | Android Developers https://developer.android.com/jetpack/compose?hl=ja Android Compose のチュートリアル | Android デベロッパー | Android Developers https://developer.android.com/jetpack/compose/tutorial?hl=ja 以下も参考になりそう Jetpack Compose入門 はじめの一歩 https://zenn.dev/ko2ic/articles/0a141f9e5a0d39 Jetpack Composeを使ってみた - Qiita https://qiita.com/kota_2402/items/7bbdd87be8024785e25b Jetpack Compose入門 - 縁側プログラミング https://engawapg.net/programming/jetpack-compose/ ■ハローワールド(Flamingo時点) src/main/java/net/refirio/helloworld/MainActivity.kt (Androidビューでは app/java/net.refirio.helloworld/MainActivity.kt にある)
package net.refirio.helloworld import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import net.refirio.helloworld.ui.theme.HelloWorldTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { HelloWorldTheme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { Greeting("Android") } } } } } @Composable fun Greeting(name: String, modifier: Modifier = Modifier) { Text( text = "Hello $name!", modifier = modifier ) } @Preview(showBackground = true) @Composable fun GreetingPreview() { HelloWorldTheme { Greeting("Android") } }
build.gradle.kts
// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id("com.android.application") version "8.1.0" apply false id("org.jetbrains.kotlin.android") version "1.8.10" apply false }
app\build.gradle.kts
plugins { id("com.android.application") id("org.jetbrains.kotlin.android") } android { namespace = "com.example.helloworld" compileSdk = 33 defaultConfig { applicationId = "com.example.helloworld" minSdk = 24 targetSdk = 33 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true } } buildTypes { release { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.4.3" } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } } dependencies { implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") implementation("androidx.activity:activity-compose:1.7.0") implementation(platform("androidx.compose:compose-bom:2023.03.00")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.material3:material3") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00")) androidTestImplementation("androidx.compose.ui:ui-test-junit4") debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-test-manifest") }
app\src\main\AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.HelloWorld" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.HelloWorld"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
■練習用にテーマを外した場合 src/main/java/net/refirio/helloworld/MainActivity.kt
package com.example.helloworld import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MainScreen() } } } @Composable fun MainScreen() { Text("Hello Android!!") } @Preview(showBackground = true) @Composable fun MainScreenPreview() { MainScreen() }
app\src\main\AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
■プレビューの表示をリッチにする showSystemUiを有効にすると、プレビューにUIが表示される
@Preview(showBackground = true) ↓ @Preview(showSystemUi = true)
Compose のツール | Jetpack Compose | Android Developers https://developer.android.com/jetpack/compose/tooling?hl=ja ■Jetpack Composeの基本 ※上で作成した MainScreen() の内容だけを以下に記述する
Text("Hello")
文字列の表示
Column { // テキストを表示 Text("Hello") // 色名を指定してテキストを表示 Text("Hello", color = Color.Red) // 色コードを指定してテキストを表示 Text("Hello", color = Color(0xff66ccaa)) // フォントサイズを指定してテキストを表示 Text("Hello", fontSize = 10.sp) Text("Hello", fontSize = 30.sp) }
コンポーネントの装飾
// テキストを装飾 Text( "Hello", modifier = Modifier .size(120.dp, 80.dp) .offset(20.dp, 20.dp) .background(Color(0xff66cdaa), RoundedCornerShape(20.dp)) .border(2.dp, Color(0xff2f4f4f), RoundedCornerShape(20.dp)) .padding(20.dp) )
画像の表示 あらかじめ、res/drawable 内に画像を配置しておく(ここでは「photo01.jpg」としておく)
Image( painter = painterResource(R.drawable.photo01), contentDescription = "画像の表示サンプル" )
Imageを使えない場合、自動で適切ではないクラスが読み込まれている可能性がある この場合、以下のように読み込むクラスを調整する
import androidx.compose.ui.semantics.Role.Companion.Image ↓ import androidx.compose.foundation.Image
【Jetpack Compose】Image()コンポーザブルが使用できない - Qiita https://qiita.com/antk/items/3b10b5f8843bb8896470 縦に並べる
Column { Text("Hello!") Text("Hello!!") Text("Hello!!!") }
横に並べる
Row { Image( painter = painterResource(id = R.drawable.photo01), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.size(50.dp) ) Image( painter = painterResource(id = R.drawable.photo02), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.size(50.dp) ) Image( painter = painterResource(id = R.drawable.photo03), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.size(50.dp) ) }
入れ子にする
Column { Row { Text("[AAA]") Text("[BBB]") Text("[CCC]") } Row { Text("[DDD]") Text("[EEE]") Text("[FFF]") } }
ボタンの表示
Column { Text("これはボタンのテストです。") Button( onClick = { Log.d("Button", "onClick") } ) { Text("ボタン") } }
ボタンをクリックしてUIを更新
Column { // 「by remember」と「mutableStateOf」により、前回の値を記憶している var count by remember { mutableStateOf(0) } Text("ボタンのタップ回数: $count") Button( onClick = { count++ } ) { Text("カウントアップ") } }
値の保持については ・「by remember」で宣言すると、Composable関数で特定の値を保持できる(値がリセットされない) rememberの後の「{ 〜 }」ブロック内は、初回しか実行されない。つまり初期値をセットできる ・「mutableStateOf」は、値の変更を監視することが可能なMutableStateを返す ・「by remember」で宣言された変数(count)は見た目は普通のintだが、ComposeではStateとして扱われる ただし、この変数を別のStateでない変数に代入すると、代入先の変数は普通の変数となり、値の変更も監視されなくなる という仕組みで実現している 「remember」や「mutableStateOf」については、以下なども参考になる Jetpack Compose入門(11) ボタンクリックでUIを更新する - 縁側プログラミング https://engawapg.net/jetpack-compose/1038/update-ui-on-click-button/ もう雰囲気で使わない。rememberを理解するためのポイント - 縁側プログラミング https://engawapg.net/jetpack-compose/2113/remember-tips/ 「by remember」を使う際、
var count by remember { mutableStateOf(0) }
のコードで以下のようなエラーになることがあった この場合、IDEの機能でimportを何度か行うと解消されるみたい
Type 'TypeVariable(T)' has no method 'getValue(Nothing?, KProperty<*>)' and thus it cannot serve as a delegate
Caused by: org.gradle.api.GradleException: Compilation error. See log for more details
UIを専用の関数にまとめる
@Composable fun MainScreen() { Column { SubContents() SubContents() SubContents() } } @Composable fun SubContents() { Row { Text("[AAA]") Text("[BBB]") Text("[CCC]") } }
■Jetpack Composeで画面遷移 Jetpack Compose入門(15) 画面遷移 - 縁側プログラミング https://engawapg.net/jetpack-compose/1393/screen-transition/ 画面遷移には androidx.navigation.compose パッケージが必要なので導入する 以下のページでバージョンを確認する Navigation | Jetpack | Android Developers https://developer.android.com/jetpack/androidx/releases/navigation?hl=ja 現時点での安定板は「2.5.3」となっていた build.gradle の dependencies 内に以下を追加する
implementation "androidx.navigation:navigation-compose:2.5.3"
追加したら「Sync Now」をクリックする Jetpack Composeで画面遷移させる | mokelab tech sheets https://tech.mokelab.com/android/compose/app/navigation/navigate.html 以下のとおり実装する
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MainScreen() } } } @Composable fun MainScreen() { val navController = rememberNavController() NavHost(navController = navController, startDestination = "FirstScreen") { composable("FirstScreen") { FirstScreen(navController = navController) } composable("SecondScreen") { SecondScreen(navController = navController) } } } @Composable fun FirstScreen(navController: NavController) { Column { Text("スクリーンA") Button(onClick = { navController.navigate("SecondScreen") }) { Text("スクリーンBへ") } } } @Composable fun SecondScreen(navController: NavController) { Column { Text("スクリーンB") Button(onClick = { navController.navigateUp() }) { Text("スクリーンAへ") } } } @Preview(showSystemUi = true) @Composable fun MainScreenPreview() { MainScreen() //FirstScreen() //SecondScreen() }
上のように navController を渡すのは明快ではあるが、テストのことを考えると推奨されないらしい 以下のようにすることが推奨されるらしい Compose を使用したナビゲーション | Jetpack Compose | Android Developers https://developer.android.com/jetpack/compose/navigation?hl=ja
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MainScreen() } } } @Composable fun MainScreen() { val navController = rememberNavController() NavHost(navController = navController, startDestination = "FirstScreen") { composable("FirstScreen") { FirstScreen( onNavigateToSecondScreen = { navController.navigate("SecondScreen") }, onNavigateToThirdScreen = { navController.navigate("ThirdScreen") } ) } composable("SecondScreen") { SecondScreen( onNavigateToThirdScreen = { navController.navigate("ThirdScreen") } ) } composable("ThirdScreen") { ThirdScreen() } } } @Composable fun FirstScreen(onNavigateToSecondScreen: () -> Unit, onNavigateToThirdScreen: () -> Unit) { Column { Text("スクリーン1") Button(onClick = onNavigateToSecondScreen) { Text("スクリーン2へ") } Button(onClick = onNavigateToThirdScreen) { Text("スクリーン3へ") } } } @Composable fun SecondScreen(onNavigateToThirdScreen: () -> Unit) { Column { Text("スクリーン2") Button(onClick = onNavigateToThirdScreen) { Text("スクリーン3へ") } } } @Composable fun ThirdScreen() { Column { Text("スクリーン3") } }
Jetpack Compose入門(15) 画面遷移 - 縁側プログラミング https://engawapg.net/jetpack-compose/1393/screen-transition/ 以下のようにすると、画面遷移の際に引数を受け渡しできる
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MainScreen() } } } @Composable fun MainScreen() { val navController = rememberNavController() NavHost(navController = navController, startDestination = "FirstScreen") { composable("FirstScreen") { FirstScreen( onNavigateToSecondScreen = { navController.navigate("SecondScreen") }, onNavigateToThirdScreen = { navController.navigate("ThirdScreen") }, onNavigateToProfileScreen = { userId, message -> navController.navigate("ProfileScreen/$userId/$message") } ) } composable("SecondScreen") { SecondScreen( onNavigateToThirdScreen = { navController.navigate("ThirdScreen") } ) } composable("ThirdScreen") { ThirdScreen() } composable( "ProfileScreen/{userId}/{message}", arguments = listOf( navArgument("userId") { type = NavType.IntType }, navArgument("message") { type = NavType.StringType } ) ) { backStackEntry -> val userId = backStackEntry.arguments?.getInt("userId") ?: 0 val message = backStackEntry.arguments?.getString("message") ?: "" ProfileScreen( userId, message, onNavigateToFirstScreen = { navController.navigate("FirstScreen") } ) } } } @Composable fun FirstScreen( onNavigateToSecondScreen: () -> Unit, onNavigateToThirdScreen: () -> Unit, onNavigateToProfileScreen: (Int, String) -> Unit ) { Column { Text("スクリーン1") Button(onClick = onNavigateToSecondScreen) { Text("スクリーン2へ") } Button(onClick = onNavigateToThirdScreen) { Text("スクリーン3へ") } Text("プロフィール") Button(onClick = { onNavigateToProfileScreen(1, "テスト1") }) { Text("プロフィール1へ") } Button(onClick = { onNavigateToProfileScreen(2, "テスト2") }) { Text("プロフィール2へ") } Button(onClick = { onNavigateToProfileScreen(3, "テスト3") }) { Text("プロフィール3へ") } } } @Composable fun SecondScreen(onNavigateToThirdScreen: () -> Unit) { Column { Text("スクリーン2") Button(onClick = onNavigateToThirdScreen) { Text("スクリーン3へ") } } } @Composable fun ThirdScreen() { Column { Text("スクリーン3") } } @Composable fun ProfileScreen( userId: Int = 0, message: String = "text", onNavigateToFirstScreen: () -> Unit ) { Column { Text("プロフィール $userId $message") Button(onClick = onNavigateToFirstScreen) { Text("スクリーン1へ") } } }
画面遷移については、以下などを参考に引き続き確認したい Jetpack Composeで画面遷移させる | mokelab tech sheets https://tech.mokelab.com/android/compose/app/navigation/navigate.html JetpackComposeでQiitaのクライアントアプリを作ろう|Masato Ishikawa https://note.com/masato1230/n/n743532de2d84 [Jetpack Compose] NavigationBar と Nested Navigation https://zenn.dev/ykrods/articles/580bc1fda58081 JetpackComposeのNavigation Componentを触ったのでまとめる - Qiita https://qiita.com/b4tchkn/items/55b1892ed725297eefe3 Jetpack Composeにおける画面遷移とは? - dely Tech Blog https://tech.dely.jp/entry/2021/12/17/170000 【シンプルサンプル】AndroidStudio 画面遷移 - Qiita https://qiita.com/kiyoZy/items/259699222ae1fec65a8f Jetpack Compose入門(15) 画面遷移 - 縁側プログラミング https://engawapg.net/jetpack-compose/1393/screen-transition/ ■Jetpack Composeでリスト表示 Jetpack Compose入門(17) リスト - 縁側プログラミング https://engawapg.net/jetpack-compose/1442/list/ 簡単なリスト
val fruits = listOf("リンゴ", "オレンジ", "グレープ", "ピーチ", "ストロベリー") LazyColumn { items(fruits) { fruit -> Text(text = "これは $fruit です。") } }
複数セットのデータを扱うリスト
data class Fruits(val english: String, val japanese: String) val fruits = listOf( Fruits("Apple", "リンゴ"), Fruits("Orange", "オレンジ"), Fruits("Grape", "グレープ"), Fruits("Peach", "ピーチ"), Fruits("Strawberry", "ストロベリー"), ) LazyColumn { items(fruits) { fruit -> Text("${fruit.english}は日本語で${fruit.japanese}です。") } }
タップでトーストを表示
val context = LocalContext.current data class Fruits(val english: String, val japanese: String) val fruits = listOf( Fruits("Apple", "リンゴ"), Fruits("Orange", "オレンジ"), Fruits("Grape", "グレープ"), Fruits("Peach", "ピーチ"), Fruits("Strawberry", "ストロベリー"), ) LazyColumn { itemsIndexed(fruits) { index, fruit -> Text( text = "${index}. ${fruit.english}", modifier = Modifier.clickable { Toast.makeText(context, "日本語で${fruit.japanese}です。", Toast.LENGTH_SHORT).show() } ) } }
■テーマの適用 Jetpack Compose入門(18) テーマカラーの適用 - 縁側プログラミング https://engawapg.net/jetpack-compose/1457/theme/ テーマを適用したコードは以下のとおり (AndroidStudioが生成するデフォルトコードを若干調整したもの) src/main/java/net/refirio/helloworld/MainActivity.kt
package net.refirio.helloworld import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import net.refirio.helloworld.ui.theme.HelloWorldTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { HelloWorldTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { MainScreen() } } } } } @Composable fun MainScreen(modifier: Modifier = Modifier) { Text( text = "Hello!", modifier = modifier ) } @Preview(showSystemUi = true) @Composable fun MainScreenPreview() { HelloWorldTheme { MainScreen() } }
自動的に作成された HelloWorldTheme テーマが適用されている MainScreen定義部分の「fun MainScreen(modifier: Modifier = Modifier) {」でデフォルトのModifierを参照させているが、これは特に修飾が適用されていないもの 例えばMainScreen呼び出し部分で「MainScreen(modifier = Modifier.padding(16.dp))」とすると、Textには16.pdのパディングが設定される app\src\main\AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.HelloWorld" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.HelloWorld"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
2箇所ある「android:theme="@style/Theme.HelloWorld"」部分でテーマを適用している app\src\main\java\net\refirio\helloworld\ui\theme\Theme.kt
package net.refirio.helloworld.ui.theme import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat private val DarkColorScheme = darkColorScheme( primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80 ) private val LightColorScheme = lightColorScheme( primary = Purple40, secondary = PurpleGrey40, tertiary = Pink40 /* Other default colors to override background = Color(0xFFFFFBFE), surface = Color(0xFFFFFBFE), onPrimary = Color.White, onSecondary = Color.White, onTertiary = Color.White, onBackground = Color(0xFF1C1B1F), onSurface = Color(0xFF1C1B1F), */ ) @Composable fun HelloWorldTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, content: @Composable () -> Unit ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } darkTheme -> DarkColorScheme else -> LightColorScheme } val view = LocalView.current if (!view.isInEditMode) { SideEffect { val window = (view.context as Activity).window window.statusBarColor = colorScheme.primary.toArgb() WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme } } MaterialTheme( colorScheme = colorScheme, typography = Typography, content = content ) }
テーマファイルも、Kotlinで書かれたプログラムとなっている lightColorSchemeの内容を調整することで、配色を変更できる…が、そのままだと適用されない さらに以下の部分をコメントアウトもしくは削除すると、色の変更が反映される
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) }
【Jetpack Compose】Material3でテーマカラーを変更する際に陥りがちなミス - Qiita https://qiita.com/Nagumo-7960/items/8699f7670bff4cc7a137 ■画面領域の調整(未解決) 「Jetpack Composeの基本」の内容を試していると、エミュレータ、実機ともに表示が途中で途切れたようになる …が、色々な要素を配置していくと全体が表示された…? 何か専用の指定があるのか、引き続き確認したい
■アプリの作成(Jetpack Compose / 一覧)
基本的に以下の記事を参考にしたが、色々と追加調整が必要だった Jetpack Compose入門 アプリを作る知識-1(一覧作成~Modifier/Scaffold/Surface/Columnなど) https://zenn.dev/ko2ic/articles/32134efcc1f94b material3は試験運用版らしいので、そのための宣言が必要らしい Compose でマテリアル 2 からマテリアル 3 に移行する | Jetpack Compose | Android Developers https://developer.android.com/jetpack/compose/designsystems/material2-material3?hl=ja
@OptIn(ExperimentalMaterial3Api::class)
Scaffoldを使う際は、ConstraintLayoutの指定が必要みたい Jetpack Compose 1.2.0 では Scaffold の content に PaddingValues を必ず設定する - Infinito Nirone 7 https://blog.keithyokoma.dev/entry/2022/08/30/193024 Compose の ConstraintLayout | Jetpack Compose | Android Developers https://developer.android.com/jetpack/compose/layouts/constraintlayout?hl=ja
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"
まとめると、以下でデータの一覧ができた
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { HelloWorldTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { MainScreen() } } } } } @Composable fun MainScreen() { val mocks = (1..100).map { Pair("リストのタイトル$it", "これはリストのメッセージ${it}です。") } ListScreen(mocks) } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ListScreen(contents: List<Pair<String, String>>) { Scaffold( topBar = { TopAppBar( title = { Text("リストのサンプル") } ) } ) { innerPadding -> val context = LocalContext.current ConstraintLayout( modifier = Modifier.padding(innerPadding) ) { LazyColumn { items(contents.size) { index -> val content = contents[index] ListTitle(title = content.first, body = content.second) { Toast.makeText(context, content.second, Toast.LENGTH_SHORT).show() } } } } } } @Composable private fun ListTitle( title: String, body: String, onClick: () -> Unit ) { Surface( modifier = Modifier.clickable { onClick() }, shape = MaterialTheme.shapes.medium ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(all = 8.dp) ) { Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp, vertical = 16.dp), ) { Text(title) Spacer(modifier = Modifier.height(4.dp)) Text(body) } } } }
■アプリの作成(Jetpack Compose / 一覧と詳細)
Jetpack Compose入門 アプリを作る知識-2(一覧から詳細への画面遷移~rememberNavControllerなど) https://zenn.dev/ko2ic/articles/f00dbfd350d521
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { HelloWorldTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { MainScreen() } } } } } @Composable fun MainScreen() { val mocks = (1..100).map { Pair("リストのタイトル$it", "これはリストのメッセージ${it}です。") } val navController = rememberNavController() NavHost(navController = navController, startDestination = "ListScreen") { composable("ListScreen") { ListScreen( mocks, onNavigateToDetailScreen = { title, body -> navController.navigate("DetailScreen/$title/$body") } ) } composable( "DetailScreen/{title}/{body}", arguments = listOf( navArgument("title") { type = NavType.StringType }, navArgument("body") { type = NavType.StringType } ) ) { backStackEntry -> val title = backStackEntry.arguments?.getString("title") ?: "" val body = backStackEntry.arguments?.getString("body") ?: "" DetailScreen( title, body, onNavigateToFirstScreen = { navController.navigateUp() } ) } } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ListScreen(contents: List<Pair<String, String>>, onNavigateToDetailScreen: (String, String) -> Unit) { Scaffold( topBar = { TopAppBar( title = { Text("リストのサンプル") } ) } ) { innerPadding -> val context = LocalContext.current ConstraintLayout( modifier = Modifier.padding(innerPadding) ) { LazyColumn { items(contents.size) { index -> val content = contents[index] ListTitle(title = content.first, body = content.second) { onNavigateToDetailScreen(content.first, content.second) } } } } } } @Composable private fun ListTitle( title: String, body: String, onClick: () -> Unit ) { Surface( modifier = Modifier.clickable { onClick() }, shape = MaterialTheme.shapes.medium ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(all = 8.dp) ) { Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp, vertical = 16.dp), ) { Text(title) Spacer(modifier = Modifier.height(4.dp)) Text(body) } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun DetailScreen( title: String, body: String, onNavigateToFirstScreen: () -> Unit ) { Scaffold( topBar = { TopAppBar( title = { Text("詳細") }, navigationIcon = { IconButton(onClick = onNavigateToFirstScreen) { Icon( imageVector = Icons.Default.ArrowBack, contentDescription = "戻る" ) } } ) } ) { innerPadding -> ConstraintLayout( modifier = Modifier.padding(innerPadding), ) { Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp, vertical = 16.dp), ) { Text("これは詳細画面です。") Text("タイトル: ${title}") Text("本文: ${body}") } } } }
■アプリの作成(Jetpack Compose / 入力内容の表示)
前提知識として、「アプリの作成(Jetpack Compose) > Jetpack Composeの基本」の「ボタンをクリックしてUIを更新」を参照 以下のようにすると「ボタンを押すと、入力されたテキストをそのまま表示」ができる 「onValueChange」の内容は、テキストが変更される度に呼び出される(入力中もどんどん呼ばれる)
@OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen() { var inputText by remember { mutableStateOf(TextFieldValue("")) } var displayedText by remember { mutableStateOf("") } Column( modifier = Modifier .fillMaxSize() .padding(16.dp) ) { OutlinedTextField( value = inputText, onValueChange = { inputText = it }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { displayedText = inputText.text }) { Text("表示") } Spacer(modifier = Modifier.height(16.dp)) Text("入力されたテキスト: " + displayedText) } }
以下のようにすると「リアルタイムに、入力されたテキストをそのまま表示」ができる
@OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen() { var inputText by remember { mutableStateOf(TextFieldValue("")) } Column( modifier = Modifier .fillMaxSize() .padding(16.dp) ) { OutlinedTextField( value = inputText, onValueChange = { inputText = it }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(16.dp)) Text("入力されたテキスト: " + inputText.text) } }
応用として、以下のようにすると「リアルタイムに、入力された西暦を和暦に変換して表示」ができる
@OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen() { var inputText by remember { mutableStateOf(TextFieldValue("")) } Column( modifier = Modifier .fillMaxSize() .padding(16.dp) ) { Text("和暦変換", fontSize = 30.sp) Spacer(modifier = Modifier.height(16.dp)) Text("年月日を入力すると和暦で表示します。") Spacer(modifier = Modifier.height(16.dp)) OutlinedTextField( value = inputText, onValueChange = { inputText = it }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Number ), modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(16.dp)) if (inputText.text.length == 8 && inputText.text.toIntOrNull() != null) { val year = inputText.text.substring(0, 4).toInt() val month = inputText.text.substring(4, 6).toInt() val day = inputText.text.substring(6, 8).toInt() val (wareki_label, wareki_year) = getWareki(year, month, day) Text("和暦は${wareki_label}${wareki_year}年${month}月${day}日です。", fontSize = 20.sp) } else { Text("8桁の数字で入力してください。", fontSize = 20.sp) } } } fun getWareki(year: Int, month: Int, day: Int): Pair<String, Int> { val date = String.format("%04d%02d%02d", year, month, day).toInt() return when { date >= 20190501 -> Pair("令和", year - 2018) date >= 19890108 -> Pair("平成", year - 1988) date >= 19261225 -> Pair("昭和", year - 1925) date >= 19120730 -> Pair("大正", year - 1911) date >= 18680125 -> Pair("明治", year - 1867) else -> Pair("", year) } }
JetpackCompose KeyBoard Options と Actions - Qiita https://qiita.com/kk__777/items/cf124ad92e68b93c2acf
■アプリの作成(Jetpack Compose / データの保存)
※今は「SharedPreferences」ではなく、「DataStore」を使うことが推奨されているらしい ※未検中 アプリ アーキテクチャ: データレイヤー - DataStore - デベロッパー向け Android | Android デベロッパー | Android Developers https://developer.android.com/topic/libraries/architecture/datastore?hl=ja DataStoreを試してみる https://zenn.dev/slowhand/articles/455aa5cd244e90 【Kotlin/Android Studio】DataStoreの使い方!データの保存と取得方法 https://tech.amefure.com/android-datastore build.gradle の dependencies 内に以下を追加する
implementation "androidx.datastore:datastore-preferences:1.0.0"
追加したら「Sync Now」をクリックする プログラムを記述するファイルの最上位で以下を呼び出すことにより、シングルトンで dataStore にアクセスできるようになる
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
なお、上記のコードでインポートすべきライブラリは以下のとおり
import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore
特にPreferencesのインポート時、他の物をインポートしてしまうと、以下のエラーになるので注意
Property delegate must have a 'getValue(Context, KProperty<*>)' method. None of the following functions is suitable: public abstract operator fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> defined in kotlin.properties.ReadOnlyProperty
flow.firstのインポート時も、インポートすべきライブラリは以下なので注意
import kotlinx.coroutines.flow.first
以下のとおり実装する
package com.example.helloworld import android.content.Context import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.example.helloworld.ui.theme.HelloWorldTheme import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import java.io.IOException val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { HelloWorldTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { MainScreen() } } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen() { val context = LocalContext.current var inputText by remember { mutableStateOf(TextFieldValue("")) } Column( modifier = Modifier .fillMaxSize() .padding(16.dp) ) { OutlinedTextField( value = inputText, onValueChange = { inputText = it }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(16.dp)) Row { Button(onClick = { runBlocking(Dispatchers.IO) { putText(context, inputText.text) } }) { Text("保存") } Spacer(modifier = Modifier.width(16.dp)) Button(onClick = { runBlocking(Dispatchers.IO) { inputText = TextFieldValue(getText(context)) } }) { Text("復元") } } } } suspend fun putText(context: Context, text: String) { try { context.dataStore.edit { settings -> settings[stringPreferencesKey("text")] = text } } catch (e: IOException) { Log.d("putText", text) } } suspend fun getText(context: Context): String { var text = "" try { text = context.dataStore.data.first()[stringPreferencesKey("text")].toString() } catch (e: IOException) { Log.d("getText", text) } return text } @Preview(showSystemUi = true) @Composable fun MainScreenPreview() { MainScreen() }
■アプリの作成(Jetpack Compose / JSONを扱う)
【Kotlin】Kotlin Serialization で JSON をパースする - Tatsuro のテックブログ https://tatsurotech.hatenablog.com/entry/kotlin/serialization-basic KotlinでJSONをシリアライズ/デシリアライズする | Konsome Engineering https://engineering.konso.me/articles/kotlin-json-serialization/ 主に上記のページを参照したが、実行しても 「Type mismatch: inferred type is Fruit but SerializationStrategy<TypeVariable(T)> was expected」 というエラーになった 以下のようにすると、エラーにならずに実行できるようになった build.gradle で以下を追加する
plugins { 〜略〜 id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.20' apply false // 追加 }
app/build.gradle で以下を追加する
plugins { 〜略〜 id 'org.jetbrains.kotlin.plugin.serialization' // 追加 } dependencies { 〜略〜 implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2" // 追加
以下のとおりプログラムを作成する
@Composable fun MainScreen() { // クラスのシリアライズとデシリアライズ val apple = Fruit("リンゴ", 100, 1) val json1 = Json.encodeToString(Fruit.serializer(), apple) val fruit = Json.decodeFromString(Fruit.serializer(), json1) println(fruit.name) // 「リンゴ」が出力される // リストのシリアライズとデシリアライズ val list = listOf(Fruit("リンゴ", 100, 1), Fruit("ミカン", 500, 3)) val json2 = Json.encodeToString(ListSerializer(Fruit.serializer()), list) val fruits = Json.decodeFromString(ListSerializer(Fruit.serializer()), json2) println(fruits[0].name) // 「リンゴ」が出力される Text("Hello!") } @Serializable data class Fruit( @SerialName("name") val name: String, @SerialName("value") val value: Int, @SerialName("amount") val amount: Int )
「@Serializable」は省略できないが、「@SerialName("name")」はシリアライズ前後で名前が同じなら省略できる ■シリアライズして保存する 以下のようにすれば、シリアライズしたうえでDataStoreに保存できる
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "data") @Composable fun MainScreen() { val context = LocalContext.current // クラスのシリアライズとデシリアライズ val apple = Fruit("リンゴ", 100, 1) val json1 = Json.encodeToString(Fruit.serializer(), apple) val fruit = Json.decodeFromString(Fruit.serializer(), json1) println(fruit.name) // 「リンゴ」が出力される // リストのシリアライズとデシリアライズ val list = listOf(Fruit("リンゴ", 100, 1), Fruit("ミカン", 500, 3)) val json2 = Json.encodeToString(ListSerializer(Fruit.serializer()), list) val fruits = Json.decodeFromString(ListSerializer(Fruit.serializer()), json2) println(fruits[0].name) // 「リンゴ」が出力される /* // DataStoreから復元する場合 runBlocking(Dispatchers.IO) { val savedJson = getData(context) val savedFruits = Json.decodeFromString(ListSerializer(Fruit.serializer()), savedJson) println(savedFruits[0].name) // 「リンゴ」が出力される } */ // DataStoreに保存する場合 runBlocking(Dispatchers.IO) { putData(context, json2) } Text("Hello!") } @Serializable data class Fruit( val name: String, val value: Int, val amount: Int ) suspend fun putData(context: Context, data: String) { try { context.dataStore.edit { entries -> entries[stringPreferencesKey("data")] = data } } catch (e: IOException) { Log.d("putData", data) } } suspend fun getData(context: Context): String { var data = "" try { data = context.dataStore.data.first()[stringPreferencesKey("data")].toString() } catch (e: IOException) { Log.d("getText", data) } return data }
■アプリの作成(Jetpack Compose / Webサイトを開く)
■WebViewで表示 Jetpack Compose の AndroidView で WebView を利用する https://zenn.dev/kaleidot725/articles/2021-11-13-jc-webview-detaiils 【Android】WebViewを使ってWebページを表示する方法と端末内のブラウザで開く方法 - Qiita https://qiita.com/takagimeow/items/70a548681c20860920bf Jetpack Compose で雑に WebView を使う - Medium https://kaleidot725.medium.com/jetpack-compose-%E3%81%A7%E9%9B%91%E3%81%AB-webview-%E3%82%92%E4%BD%B... Jetpack Composeのプロダクション採用 -イマイチだったこと編- - NIFTY engineering https://engineering.nifty.co.jp/blog/15294 WebViewでアプリ開発するメリット・デメリット - Deha magazine https://deha.co.jp/magazine/webview-pros-cons/ WebViewで「net::ERR_CACHE_MISS」と表示される問題 【Android】 - Kuwapp's Blog https://yusuke-hata.hatenablog.com/entry/2015/07/07/210730 マニフェストファイルを編集し、インターネットに接続できるようにする 追加場所は、ルートであるmanifestの直下でいい manifests/AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
以下のようにプログラムを作成する
@Composable fun MainScreen() { AndroidWebView("https://refirio.org/") } @Composable fun AndroidWebView(url: String) { AndroidView( factory = { context -> WebView(context) }, update = { webView -> webView.webViewClient = WebViewClient() webView.settings.javaScriptEnabled = true // JavaScriptを実行する場合 webView.loadUrl(url) } ) }
なお以下のようにすると、MainScreenから表示先を変更できる
@Composable fun MainScreen() { var webViewTarget by remember { mutableStateOf("https://www.google.co.jp/") } Column { Text("Webページの表示切り替え。") Row { Button(onClick = { webViewTarget = "https://refirio.org/" }) { Text("refirio.org") } Button(onClick = { webViewTarget = "https://freo.jp/" }) { Text("freo.jp") } Button(onClick = { webViewTarget = "https://www.php-labo.net/" }) { Text("php-labo.net") } } AndroidWebView(webViewTarget) } }
■Accompanist WebViewについて AndroidViewではなく、AccompanistのWebView Wrapperを使うという手もあるらしい むしろ、こちらの方が推奨されるみたい…と思ったが、以下のように書かれている。もうメンテナンスされていないらしい > This library is deprecated, and the API is no longer maintained. > We recommend forking the implementation and customising it to your needs. Guide - Accompanist https://google.github.io/accompanist/web/ どれを使うべきか…と思ったが、Accompanistについて以下のように書かれている これはつまり、標準のWebViewで十分になったということか > Jetpack Composは従来の手法と比べると、必要とする機能が足りてないので、それを補うことを目的としたライブラリーグループ。ComposeAPIのラボのようなもの。 > 公式のツールキットに導入されることが目的。導入後は非推奨になり、Accompanistから削除される。 Accompanistの使い所 (Jetpack Compose) https://zenn.dev/nagaoooon/articles/6ea091a436ecb0 公式のブログでも以下のように書かれている 要約すると 「Composeが安定してきたので、機能追加などサポートを終了することにした。」 「現行のソースコードを参考に独自に実装する方がシンプルになる」 「ニーズにあったカスタム実装を作成することを推奨する」 とのこと > Pager Indicator, Placeholder & WebView > Now that Compose is stable, with a robust set of APIs that make creating custom widgets far simpler than in the past with the view system, we have decided to no longer add or support our own set of custom widgets in Accompanist. This includes Pager Indicator, Placeholder & WebView. > Compose makes implementing your own versions of these widgets easy. The main problem we have with implementing custom widgets is that we need to support enough customization for everyone. When you implement a widget yourself, you can implement just what you need and nothing more, which greatly simplifies the implementation. Another reason we are no longer supporting custom widget libraries in Accompanist is that we believe by having them in Accompanist, we are deterring the community from developing their own sets of custom widgets. > Recommendation: We recommend using our implementations to fork or create your own custom implementations that suit your needs. An update on Jetpack Compose Accompanist libraries - August 2023 | by Ben Trengrove | Android Developers | Aug, 2023 | Medium https://medium.com/androiddevelopers/an-update-on-jetpack-compose-accompanist-libraries-august-2023-... 以下にソースコードがある これを参考に、独自実装するのが良さそう accompanist/web/src/main/java/com/google/accompanist/web/WebView.kt at main - google/accompanist https://github.com/google/accompanist/blob/main/web/src/main/java/com/google/accompanist/web/WebView... いったん追加情報を待つべきか…という状態なので、また改めて調査したい 以下、WebView Wrapperに関する内容を含んだメモ [Jetpack Compose]WebViewで開いたページのタイトルを表示する https://zenn.dev/yumemi_inc/articles/2023-02-16-accompanist-webview-title 現状、画面が回転されるとWebViewの内容はリセットされる 以下などを参考に対策できそうか。Formでの入力途中の内容までは残らないか Accompanist の WebView Wrapper メモ2: 状態が画面回転に生き残らない - Qiita https://qiita.com/mangano-ito/items/e17fc127f698fb944837 以下は特殊な操作を行う場合の参考になりそう 【Android】WebViewをJetpack Composeで使った場合の、バックキーで前ページに戻る方法 - Qiita https://qiita.com/tsumuchan/items/83b5ce9d7c27bd78833a 以下は未検証だが試したい [Jetpack Compose]WebViewで特定のリンクをクリックしたらネイティブの画面に遷移させる https://zenn.dev/yumemi_inc/articles/2023-02-04-accompanist-webview-routing ■ブラウザで表示 [Jetpack Compose] Glide経由でコンテンツ一覧を表示してタップされたらブラウザで開く https://zenn.dev/laiso/articles/5a18b8689c13787841d8
@Composable fun MainScreen() { val context = LocalContext.current Column { Text("ブラウザを開く") Button(onClick = { val uri = Uri.parse("https://www.google.co.jp/") val intent = Intent(Intent.ACTION_VIEW, uri) ContextCompat.startActivity(context, intent, null) }) { Text("Google") } } }
■アプリの作成(Jetpack Compose / 起動時に処理を行う)
Jetpack Compose の Launched Effect の動作を調べる https://zenn.dev/kaleidot725/articles/2022-02-11-jetpack-compose-side-effects LaunchedEffect|サンプルで理解するJetpack Composeの副作用の仕組み https://zenn.dev/kaleidot725/books/jetpack-compose-sideeffect-samples/viewer/1-jc-side-effects #76 Jetpack ComposeのLaunchedEffectとFlow | Mokelab Blog https://blog.mokelab.com/76/launchedEffect.html Coroutine:Suspend関数とその仕組み | Y_SUZUKI's Android Log https://android.suzu-sd.com/2022/01/coroutine_suspend/ 以下のようにすると、「Start → End → LaunchedEffect」の順に実行される
@Composable fun MainScreen() { Log.d("TEST", "Start") LaunchedEffect(Unit) { Log.d("TEST", "LaunchedEffect") } Text("Hello!") Log.d("TEST", "End") }
以下のようにすると、画面には「Hello!」と「A」が表示される 「status = true」の処理が無ければ「Hello!」と「B」が表示される
@Composable fun MainScreen() { var status by remember { mutableStateOf(false) } LaunchedEffect(Unit) { status = true } Column { Text("Hello!") if (status) { Text("A") } else { Text("B") } } }
■アプリの作成(Jetpack Compose / HTMLを取得する)
【Android Kotlin】OkHttp3でコピペ可能なAPI通信処理実装 - 都市語〜トシカタ〜 福岡発、都市を語る場所 https://japanesecitylikers.com/?p=1426 [Kotlin] OkHttp3の使い方 | GETとPOSTリクエスト | とろなび | プログラミング系備忘録 https://toronavi.com/connection-okhttp Overview - OkHttp https://square.github.io/okhttp/ マニフェストファイルを編集し、インターネットに接続できるようにする 追加場所は、ルートであるmanifestの直下でいい manifests/AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
build.gradle の dependencies 内に以下を追加する
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
以下のようにプログラムを作成する
@Composable fun MainScreen() { var title by remember { mutableStateOf("") } LaunchedEffect(Unit) { withContext(Dispatchers.IO) { try { val client = OkHttpClient() val request = Request.Builder() .url("https://www.example.com/") .build() val response = client.newCall(request).execute() val text = response.body?.string() ?: "" val titleRegex = "<title>(.*?)</title>".toRegex() val matchResult = titleRegex.find(text) title = matchResult?.groups?.get(1)?.value ?: "Title not found" Log.d("OK", title) } catch (e: Exception) { Log.d("ERROR", "OkHttpClient") } } } Column { Text(title) } }
■アプリの作成(Jetpack Compose / リストの登録編集削除)
#63 Jetpack ComposeでToDoアプリを作る - 概要 | Mokelab Blog https://blog.mokelab.com/63/compose_todo1.html リストの並び替えは、現状対応していないみたい 【Android】 Jetpack Composeでドラッグ&ドロップの並び替えを実現する 〜ライブラリに頼って〜 - Qiita https://qiita.com/tsumuchan/items/d0fc2a4bd4af6802f9fc Sorting List Items in LazyColumn - Android Jetpack Compose - Stack Overflow https://stackoverflow.com/questions/73915584/sorting-list-items-in-lazycolumn-android-jetpack-compos... 以下はデータ追加用に「+」ボタンを表示する例
Scaffold( topBar = { TopAppBar( title = { Text("リストのサンプル") } ) }, floatingActionButton = { FloatingActionButton(onClick = { Log.d("FloatingActionButton", "Clicked!") }) { Icon( imageVector = Icons.Filled.Add, contentDescription = "追加" ) } } ) { innerPadding ->
上記を踏まえて、リストの登録編集削除を作成する ■リストの登録編集削除(調整中) build.gradle
plugins { id 'com.android.application' version '8.0.2' apply false id 'com.android.library' version '8.0.2' apply false id 'org.jetbrains.kotlin.android' version '1.7.20' apply false id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.20' apply false }
app/build.gradle
plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.plugin.serialization' } 〜中略〜 dependencies { implementation 'androidx.core:core-ktx:1.8.0' implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' implementation 'androidx.activity:activity-compose:1.5.1' implementation platform('androidx.compose:compose-bom:2022.10.00') implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.material3:material3' implementation "androidx.navigation:navigation-compose:2.5.3" implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2" implementation "androidx.datastore:datastore-preferences:1.0.0" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') androidTestImplementation 'androidx.compose.ui:ui-test-junit4' debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' }
java/jp/terraport/list/MainActivity.kt
package jp.terraport.list import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import jp.terraport.list.ui.MainApp import jp.terraport.list.ui.theme.ListTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ListTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { MainApp() } } } } } @Preview(showSystemUi = true) @Composable fun GreetingPreview() { ListTheme { MainApp() } }
java/jp/terraport/list/ui/MainApp.kt
package jp.terraport.list.ui import android.net.Uri import androidx.compose.runtime.Composable import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import jp.terraport.list.ui.add.AddScreen import jp.terraport.list.ui.edit.EditScreen import jp.terraport.list.ui.list.ListScreen @Composable fun MainApp() { val navController = rememberNavController() NavHost(navController = navController, startDestination = "ListScreen") { composable("ListScreen") { ListScreen( onNavigateToAddScreen = { navController.navigate("AddScreen") }, onNavigateToEditScreen = { id, title, body -> navController.navigate("EditScreen?id=" + Uri.encode(id) + "&title=" + Uri.encode(title) + "&body=" + Uri.encode(body)) } ) } composable("AddScreen") { AddScreen( onNavigateToFirstScreen = { navController.navigateUp() } ) } composable( "EditScreen?id={id}&title={title}&body={body}", arguments = listOf( navArgument("id") { type = NavType.StringType }, navArgument("title") { type = NavType.StringType }, navArgument("body") { type = NavType.StringType } ) ) { backStackEntry -> EditScreen( backStackEntry.arguments?.getString("id") ?: "", backStackEntry.arguments?.getString("title") ?: "", backStackEntry.arguments?.getString("body") ?: "", onNavigateToFirstScreen = { navController.navigateUp() } ) } } }
java/jp/terraport/list/ui/list/ListScreen.kt
package jp.terraport.list.ui.list import android.util.Log import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.constraintlayout.compose.ConstraintLayout import jp.terraport.list.utils.Memo import jp.terraport.list.utils.getMemo @OptIn(ExperimentalMaterial3Api::class) @Composable fun ListScreen( onNavigateToAddScreen: () -> Unit, onNavigateToEditScreen: (String, String, String) -> Unit ) { val context = LocalContext.current val memos = remember { mutableStateListOf<Memo>() } LaunchedEffect(Unit) { memos.addAll(getMemo(context)) } Scaffold( topBar = { TopAppBar( title = { Text("リストのサンプル") } ) }, floatingActionButton = { FloatingActionButton(onClick = { onNavigateToAddScreen() }) { Icon( imageVector = Icons.Filled.Add, contentDescription = "追加" ) } } ) { innerPadding -> ConstraintLayout( modifier = Modifier.padding(innerPadding) ) { LazyColumn { items(memos.size) { index -> val content = memos[index] ListTitle(title = content.title, body = content.detail) { onNavigateToEditScreen(content.id, content.title, content.detail) } } } } } }
java/jp/terraport/list/ui/list/ListTitle.kt
package jp.terraport.list.ui.list import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable fun ListTitle( title: String, body: String, onClick: () -> Unit ) { Surface( modifier = Modifier.clickable { onClick() }, shape = MaterialTheme.shapes.medium ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(all = 8.dp) ) { Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp, vertical = 16.dp), ) { Text(title) Spacer(modifier = Modifier.height(4.dp)) Text(body) } } } }
java/jp/terraport/list/ui/add/AddScreen.kt
package jp.terraport.list.ui.add import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import jp.terraport.list.utils.Memo import jp.terraport.list.utils.generateRandomString import jp.terraport.list.utils.getMemo import jp.terraport.list.utils.putMemo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @OptIn(ExperimentalMaterial3Api::class) @Composable fun AddScreen( onNavigateToFirstScreen: () -> Unit ) { val context = LocalContext.current var titleText by remember { mutableStateOf(TextFieldValue("")) } var detailText by remember { mutableStateOf(TextFieldValue("")) } Scaffold( topBar = { TopAppBar( title = { Text("登録") }, navigationIcon = { IconButton(onClick = onNavigateToFirstScreen) { Icon( imageVector = Icons.Default.ArrowBack, contentDescription = "戻る" ) } } ) } ) { innerPadding -> ConstraintLayout( modifier = Modifier.padding(innerPadding), ) { Column { OutlinedTextField( value = titleText, onValueChange = { titleText = it }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(16.dp)) OutlinedTextField( value = detailText, onValueChange = { detailText = it }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { runBlocking(Dispatchers.IO) { val memos: MutableList<Memo> = mutableListOf() memos.add(Memo(generateRandomString(16), titleText.text, detailText.text)) memos.addAll(getMemo(context)) putMemo(context, memos) } onNavigateToFirstScreen() }) { Text("保存") } } } } }
java/jp/terraport/list/ui/edit/EditScreen.kt
package jp.terraport.list.ui.edit import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import jp.terraport.list.utils.Memo import jp.terraport.list.utils.getMemo import jp.terraport.list.utils.putMemo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @OptIn(ExperimentalMaterial3Api::class) @Composable fun EditScreen( id: String, title: String, body: String, onNavigateToFirstScreen: () -> Unit ) { val context = LocalContext.current var titleText by remember { mutableStateOf(TextFieldValue(title)) } var detailText by remember { mutableStateOf(TextFieldValue(body)) } Scaffold( topBar = { TopAppBar( title = { Text("詳細") }, navigationIcon = { IconButton(onClick = onNavigateToFirstScreen) { Icon( imageVector = Icons.Default.ArrowBack, contentDescription = "戻る" ) } } ) } ) { innerPadding -> ConstraintLayout( modifier = Modifier.padding(innerPadding), ) { Column { OutlinedTextField( value = titleText, onValueChange = { titleText = it }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(16.dp)) OutlinedTextField( value = detailText, onValueChange = { detailText = it }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { runBlocking(Dispatchers.IO) { val memos: MutableList<Memo> = mutableListOf() getMemo(context).forEach { memo -> if (id == memo.id) { memos.add(Memo(id, titleText.text, detailText.text)) } else { memos.add(Memo(memo.id, memo.title, memo.detail)) } } putMemo(context, memos) } onNavigateToFirstScreen() }) { Text("保存") } Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { runBlocking(Dispatchers.IO) { val memos: MutableList<Memo> = mutableListOf() getMemo(context).forEach { memo -> if (id != memo.id) { memos.add(Memo(memo.id, memo.title, memo.detail)) } } putMemo(context, memos) } onNavigateToFirstScreen() }) { Text("削除") } } } } }
java/jp/terraport/list/utils/Memo.kt
package jp.terraport.list.utils import android.content.Context import android.util.Log import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json import java.io.IOException @Serializable data class Memo( val id: String, val title: String, val detail: String ) suspend fun getMemo(context: Context): List<Memo> { return try { Json.decodeFromString(ListSerializer(Memo.serializer()), getData(context, "memo")) } catch (e: Exception) { Log.e("getMemo", "Decode failed.", e) emptyList() } } suspend fun putMemo(context: Context, memos: List<Memo>) { try { putData(context, "memo", Json.encodeToString(ListSerializer(Memo.serializer()), memos)) } catch (e: IOException) { Log.e("putMemo", "Encode Failed.", e) } }
java/jp/terraport/list/utils/Common.kt
package jp.terraport.list.utils import android.content.Context import android.util.Log import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.first import java.io.IOException val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "list") suspend fun getData(context: Context, name: String): String { var data: String? = null try { data = context.dataStore.data.first()[stringPreferencesKey(name)] } catch (e: IOException) { Log.e("getData", "Get failed.", e) } return data ?: "[]" } suspend fun putData(context: Context, name: String, data: String) { try { context.dataStore.edit { entries -> entries[stringPreferencesKey(name)] = data } } catch (e: IOException) { Log.e("putData", "Put failed.", e) } } fun generateRandomString(length: Int): String { val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') return (1..length) .map { allowedChars.random() } .joinToString("") }
■アプリの作成(Jetpack Compose / ビューモデルを扱う)
※未検証 ViewModel の概要 | Android デベロッパー | Android Developers https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ja #69 Jetpack ComposeでToDoアプリを作る - HiltとViewModel | Mokelab Blog https://blog.mokelab.com/69/compose_todo7.html とにかく簡単にViewModelまとめ - Qiita https://qiita.com/KIRIN3qiita/items/7d833e2c010c0b2c02d9 ViewModelの採用が必須というわけでも無いらしい Jetpack ComposeでViewModelを使わずに、Composable関数を使って状態とロジックを切り出す! - Qiita https://qiita.com/karamage/items/9b2b5a79c364b72836d4 「SwiftUIでMVVMを採用するのは止めよう」と思い至った理由 - Qiita https://qiita.com/karamage/items/8a9c76caff187d3eb838 ViewModel を捨てて マルチプラットフォーム に備える https://android.benigumo.com/20220928/without-viewmodel/ 以下、ViewModelを使わずにComposable関数で対応する場合のコード ■カウントアップするプログラム
@Composable fun MainScreen() { Column { var count by remember { mutableStateOf(0) } Text("ボタンのタップ回数: $count") Button( onClick = { count++ } ) { Text("カウントアップ") } } }
※上記コードでは、画面が回転したときに値が失われる 「by remember」の代わりに「by rememberSaveable」にすると、画面が回転しても値を保持できる ただし rememberSaveable は Parcelable しか保存できないので、オブジェクトを保存したい場合は Parcelable にシリアライズする必要があるらしい(未検証) ■状態とロジックを切り離す
@Composable fun MainScreen() { Column { val counter = rememberCounter() Text("ボタンのタップ回数: ${counter.count}") Button( onClick = { counter.increment() } ) { Text("カウントアップ") } } }
rememberCounter は model/Counter.kt など別ファイルに記載する
data class Counter( val count: Int, val increment: () -> Unit ) @Composable fun rememberCounter(): Counter { var count by rememberSaveable { mutableStateOf(0) } return remember(count) { Counter( count = count, // 状態 increment = { count++ } // ロジック ) } }
■アプリの作成(Jetpack Compose / カメラを扱う)
※未検証 CameraX | Android デベロッパー | Android Developers https://developer.android.com/jetpack/androidx/releases/camera?hl=ja Jetpack ComposeでCameraXを実装する | Androg https://kwmt27.net/2021/06/12/jetpack-compose-camerax/ Jetpack Composeで写真を撮影するか画像を選択する - 山本隆の開発日誌 https://www.gesource.jp/weblog/?p=8773 [WIP]CameraXで作るQRコードリーダ|Masato Ishikawa https://note.com/masato1230/n/na09514fe5698
■アプリの作成(Jetpack Compose / Firebaseを扱う)
※未検証 [Jetpack Compose]Analytics / Crashlytics / FCMを導入 - Kumanote Tech Blog https://blog.kumano-te.com/activities/introduce-firebase-to-android-app-with-compose
■アプリの作成(XMLレイアウト)
■ハローワールド src/main/java/org/refirio/helloworld/MainActivity.kt
package org.refirio.helloworld import android.os.Bundle import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } }
src/main/res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="ハローワールド!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
■ボタンとトースト src/main/java/org/refirio/helloworld/MainActivity.kt
package org.refirio.helloworld import android.os.Bundle import android.widget.Button import android.widget.Toast import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val testButton = findViewById<Button>(R.id.test_button) testButton.setOnClickListener { Toast.makeText(applicationContext, "これはトーストです", Toast.LENGTH_SHORT).show(); } } }
src/main/res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/test_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="ボタン" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
■ビューバインディング AndroidStudio4.1から、ビューバインディングという機能が用意されている これはfindViewByIdの後継となる機能で、findViewByIdよりも高速に動作するらしい build.gradle(Androidビューでは「Gradle Scripts」直下の「build.gradle (Module: helloworld.app)」が該当する)
android { 〜略〜 buildFeatures { viewBinding = true } }
コードを変更したら「Sync Now」をクリック さらに、アクティビティを以下のように変更 src/main/java/org/refirio/helloworld/MainActivity.kt
package org.refirio.helloworld import android.os.Bundle import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import org.refirio.helloworld.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) val view = binding.root setContentView(view) binding.testButton.setOnClickListener { Toast.makeText(applicationContext, "これはトーストです", Toast.LENGTH_SHORT).show(); } } }
これで先と同じ結果が得られる ビューバインディングでは、レイアウトXMLファイルに対応するクラスが自動で生成される 今回の場合レイアウトファイルは activity_main.xml なので、これに接尾語「Binding」を加えた「ActivityMainBinding」が自動的に生成されている このクラスを使うように指定する。そしてonCreateメソッド内で ・生成されたバインディングクラスに含まれるinflateメソッドを呼び出す ・rootプロパティからルートビューへの参照を取得する ・取得したルートビューをsetContentViewにわたす という処理を行う(これはビューバインディングを使う場合のお決まりの処理となる) 「testButton」は、ボタンのID「test_button」をキャメルケースにしたものを指定するみたい ■画像の表示 任意の画像をコピーする プロジェクトウインドウのツリーで「res」内の「drawable」を選択する その状態でペーストを行う コピー先の選択として「drawable」と「drawable-24」のように表示されるが、「drawable」にペーストする (「drawable-24」はAPI24(Android7.0)以降のバージョンでのみ使用したいリソースを入れる場所) コピーダイアログが開くので、そのまま「Refactor」をクリックする これで「drawable」フォルダ内に画像が表示される レイアウトのXMLを開き、「common」内の「ImageView」をドラッグ&ドロップで配置する 画像の選択ダイアログが表示されるので、配置したい画像を選択して「OK」をクリック 画像が配置されるので、あとはテキストやボタンと同様に表示位置の調整を行う ■アクティビティの追加とインテント プロジェクトウインドウのツリーで「app」を右クリックし、 「New → Activity → Empty Activity」を選択する 表示されたダイアログで「Activity Name」で「ResultActivity」と入力して、あとはデフォルトのまま「Finish」ボタンを押す プロジェクトウインドウのツリーに「ResultActivity」と「activity_result.xml」が追加される 以下のようにすると、追加したアクティビティに遷移するためのボタンとして機能する src/main/java/org/refirio/helloworld/MainActivity.kt
binding.testButton.setOnClickListener { val intent = Intent(this, ResultActivity::class.java) startActivity(intent) }
■staticメソッドを定義する companion object を使ってstaticメソッドを定義する Kotlinで静的変数・メソッドを定義する方法 - goroyaのSE日記 http://gogoroya.hatenadiary.jp/entry/2017/06/05/234348 kotlinはJavaからどう見えるか? - Qiita https://qiita.com/boohbah/items/167233c7eafe17f3150b ■Kotlin RPEL ※XcodeでいうPlaygroundのようなもの プロジェクトを開いた状態で、メニューの「ツール → Kotlin → Kotlin RPEL」から実行できる ■アプリのアイコンを変更する ※未検証 Android Studio 開発【アプリアイコンの変更】 - ハコニワ デザイン http://hakoniwadesign.com/?p=4908 Androidアプリのアイコン画像を変更する方法【初心者向け】 | TechAcademyマガジン https://techacademy.jp/magazine/2710 ■スプラッシュ画面を表示する ※未検証 Splash画面でpostDelayedして一定時間画面を表示する - Qiita https://qiita.com/shanonim/items/35296c02494ffdbd7273 Androidでスプラッシュ画面を作る方法 - Qiita https://qiita.com/glayash/items/646e5c0d5de82cfc17bc ■カメラ 【Android】カメラ機能に触れてみる(Android5.0〜) - vaguely https://mslgt.hatenablog.com/entry/2015/05/12/013013 AndroidのCamera2 APIのサンプル : 時々、失業SEの開発日誌 http://blog.kotemaru.org/2015/05/23/android-camera2-sample.html 【kotlin】CameraXでAndroidカメラを実装してみた | RE:ENGINES https://re-engines.com/2019/10/31/%E3%80%90kotlin%E3%80%91camerax%E3%81%A7android%E3%82%AB%E3%83%A1%... Getting Started with CameraX https://codelabs.developers.google.com/codelabs/camerax-getting-started/#2 【Android】CameraX試してみた - Qiita https://qiita.com/emusute1212/items/6195cf18bfcbea2ef1d1 Android CameraXでプレビューを表示する | Developers.IO https://dev.classmethod.jp/smartphone/android/android-camerax-preview/ Android4と5・6でカメラは仕様が大きく変わっているみたい 以下、2021年6月時点で改めて調べたときの記事(要検証) CameraX の概要 | Android デベロッパー | Android Developers https://developer.android.com/training/camerax?hl=ja プレビューを実装する | Android デベロッパー | Android Developers https://developer.android.google.cn/training/camerax/preview?hl=ja 【初心者向け】CameraXでAndroidのカメラアプリを作る - Qiita https://qiita.com/senju797/items/7b846fce6a828004279c AndroidのカメラサポートライブラリのCameraXを使ってみました | Kotlin | アプリ関連ニュース | ギガスジャパン http://www.gigas-jp.com/appnews/archives/9555 AndroidでCameraXを使ってみる(プレビューの表示) - くらげになりたい。 https://www.memory-lovers.blog/entry/2021/01/25/230000 ■プッシュ Androidのプッシュ機能は、以下のように仕組みが変わっている C2DM(Cloud to Device Messaging) ↓ GCM(Google Cloud Messaging) ↓ FCM(Firebase Cloud Messaging) 【Androidプッシュ通知】GCMの廃止、FCMへの移行とは? | 株式会社アイリッジ https://iridge.jp/blog/201810/23018/ 「Googleクラウドメッセージング(GCM)」が1年後に廃止、「Firebase Cloud Messaging(FCM)」への移行が必要に:Googleのアプリメッセージング基盤が完全に交代 - @IT http://www.atmarkit.co.jp/ait/articles/1804/13/news051.html 【Androidプッシュ通知】GCMの廃止、FCMへの移行とは? | 株式会社アイリッジ https://iridge.jp/blog/201810/23018/
■アプリの作成(XMLレイアウト / フラグメント)
■フラグメント Fragmentの基本 - Qiita https://qiita.com/naoi/items/3e1125d1e1418d09f77a Android はじめてのFragment - Qiita https://qiita.com/Reyurnible/items/dffd70144da213e1208b ■フラグメントでページの切り替え 以下で新規にプロジェクトを作成 プロジェクトの選択: Empty Activity プロジェクトの名前: fragment ビューバインディングを使えるようにする build.gradle を変更したら「Sync Now」をクリック アクティビティを追加で作成する プロジェクトウインドウのツリーで「app」を右クリックし、 「New → Activity → Empty Activity」を選択する 表示されたダイアログで「Activity Name」で「SubActivity」と入力して、あとはデフォルトのまま「Finish」ボタンを押す プロジェクトウインドウのツリーに「SubActivity」と「activity_sub.xml」が追加される タイトル表示用のフラグメントを作成する プロジェクトウインドウのツリーで「app」を右クリックし、 「New → Fragment → Fragment(Blank)」を選択する 表示されたダイアログで「Fragment Name」で「TitleFragment」と入力して、あとはデフォルトのまま「Finish」ボタンを押す プロジェクトウインドウのツリーに「TitleFragment.kt」と「fragment_title.xml」が追加される fragment_title.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".TitleFragment"> <!-- TODO: Update blank fragment layout --> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:text="@string/hello_blank_fragment" /> </FrameLayout>
このファイルを、以下のように変更する
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/constraintLayout" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".TitleFragment" > <TextView android:id="@+id/titleText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="タイトル" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
TitleFragment
package org.refirio.fragment import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment // TODO: Rename parameter arguments, choose names that match // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER private const val ARG_PARAM1 = "param1" private const val ARG_PARAM2 = "param2" /** * A simple [Fragment] subclass. * Use the [TitleFragment.newInstance] factory method to * create an instance of this fragment. */ class TitleFragment : Fragment() { // TODO: Rename and change types of parameters private var param1: String? = null private var param2: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { param1 = it.getString(ARG_PARAM1) param2 = it.getString(ARG_PARAM2) } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_title, container, false) } companion object { /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @param param1 Parameter 1. * @param param2 Parameter 2. * @return A new instance of fragment TitleFragment. */ // TODO: Rename and change types and number of parameters @JvmStatic fun newInstance(param1: String, param2: String) = TitleFragment().apply { arguments = Bundle().apply { putString(ARG_PARAM1, param1) putString(ARG_PARAM2, param2) } } } }
このファイルを、以下のように変更する
package org.refirio.fragment import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import org.refirio.fragment.databinding.FragmentTitleBinding class TitleFragment : Fragment() { private var _binding: FragmentTitleBinding? = null private val binding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding = FragmentTitleBinding.inflate(inflater, container, false) return binding.root } override fun onDestroyView() { super.onDestroyView() _binding = null } fun setTitle(title: String) { binding.titleText.text = title } }
タイトル表示用のフラグメントをアクティビティへ配置する activity_main.xml を開き、「Hello World」のテキストビューを削除してから、 「common」内の「<fragment>」をドラッグ&ドロップで中央に配置する 「Fragments」ダイアログに、存在するフラグメントが一覧表示されるので、先程作成した「TitleFragment」を作成して「OK」をクリックする 成約は上端と左右を画面の端に接続し、「Attribute」でマージンを0に設定する(もともと0になっているはず) また、以下の設定を行う id: titleFragment layout_width: 0dp layout_height: wrap_content フラグメントのプレビューを有効にする プログラムの動作には影響しないが、レイアウトがイメージしやすくなるので設定しておくといい 配置したフラグメントを選択し、「All Attrivute」内の「layout」の「[]」をクリックする 「Pick a Resource」ダイアログが開くので、先ほど作成した「fragment_title」を選択して「OK」をクリックする ここまで作業できたらいったん実行してみる 画面上部にフラグメント内の文字列が表示される サブ画面を作成する 「activity_sub.xml」を開き、初期配置されているテキストビューがあれば削除する 「layout」から「FrameLayout」を画面中央にドラッグ&ドロップで配置 成約は上端と左右を画面の端に接続し、「Attribute」でマージンを0に設定する また、以下の設定を行う id: titleFrame layout_width: 0dp layout_height: wrap_content 「activity_sub.xml」に「Common」内の「Button」を3つ配置する 位置はtitleFrameの下なら適当でいい また、それぞれに以下の設定を行う id: firstButton text: ボタン1 id: secondButton text: ボタン2 id: thirdButton text: ボタン3 ボタンに制約を追加しておく 一例だが、3つのボタンを選択して「Align」ボタンから「Top Edges」を選択する これで各ボタンが水平に並ぶ さらに3つのボタンを選択して、いずれかのボタンを右クリックして「Chains → Create Horizontal Chain」を選択する これで画面サイズやボタンサイズが変更されても水平方向に均等に配置される フラグメント表示用のビューを配置する 「Layout」内の「FrameLayout」をドラッグ&ドロップでボタンの下に配置する 以下の設定を行う id: container layout_width: 0dp layout_height: 0dp 制約は、上端を真ん中のボタンの下に、下端と左右は画面の端に接続する マージンはそれぞれ8を設定しておく 各画面のフラグメントを用意する プロジェクトウインドウのツリーで「app」を右クリックし、 「New → Fragment → Fragment(Blank)」を選択する 表示されたダイアログで以下を入力し、あとはデフォルトのまま「Finish」ボタンを押す FirstFragment SecondFragment ThirdFragment プロジェクトウインドウのツリーに以下が追加される FirstFragment.kt SecondFragment.kt ThirdFragment.kt fragment_pfirst.xml fragment_second.xml fragment_third.xml 画面の切り替えがわかるように、それぞれに配置されているテキストを変更しておく SubAcrivity.kt
package org.refirio.fragment import android.os.Bundle import androidx.appcompat.app.AppCompatActivity class SubActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_sub) } }
以下のように変更する
package org.refirio.fragment import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import org.refirio.fragment.databinding.ActivitySubBinding class SubActivity : AppCompatActivity() { private lateinit var binding: ActivitySubBinding private lateinit var title: TitleFragment override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivitySubBinding.inflate(layoutInflater) setContentView(binding.root) binding.firstButton.setOnClickListener { supportFragmentManager.beginTransaction().apply { replace(R.id.container, FirstFragment()) addToBackStack(null) commit() } } binding.secondButton.setOnClickListener { supportFragmentManager.beginTransaction().apply { replace(R.id.container, SecondFragment()) addToBackStack(null) commit() } } binding.thirdButton.setOnClickListener { supportFragmentManager.beginTransaction().apply { replace(R.id.container, ThirdFragment()) addToBackStack(null) commit() } } title = TitleFragment() supportFragmentManager.beginTransaction().apply { replace(R.id.titleFrame, title) //addToBackStack(null) commit() } } override fun onResume() { super.onResume() title.setTitle("サブ画面") } }
タイトル画面を完成させる activity_main.xml にボタンを配置し、上下左右に制約を追加し、以下の設定を行う id: startButton layout_width: wrap_content layout_height: wrap_content MainActivity.kt
package org.refirio.fragment import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import org.refirio.fragment.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.startButton.setOnClickListener { val intent = Intent(this, SubActivity::class.java) startActivity(intent) } } }
何故か「android Unresolved reference」のエラーになったが、 メニューから「Build → Clean Project」を実行するとビルドできるようになった ■フラグメントと値のやりとり 以下で新規にプロジェクトを作成 プロジェクトの選択: Empty Activity プロジェクトの名前: fragment ビューバインディングを使えるようにする build.gradle を変更したら「Sync Now」をクリック ボタン表示用のフラグメントを作成する プロジェクトウインドウのツリーで「app」を右クリックし、 「New → Fragment → Fragment(Blank)」を選択する 表示されたダイアログで「Fragment Name」で「ButtonFragment」と入力して、あとはデフォルトのまま「Finish」ボタンを押す プロジェクトウインドウのツリーに「ButtonFragment.kt」と「fragment_button.xml」が追加される fragment_button.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ButtonFragment"> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
ラベル表示用のフラグメントを作成する プロジェクトウインドウのツリーで「app」を右クリックし、 「New → Fragment → Fragment(Blank)」を選択する 表示されたダイアログで「Fragment Name」で「LabelFragment」と入力して、あとはデフォルトのまま「Finish」ボタンを押す プロジェクトウインドウのツリーに「LabelFragment.kt」と「fragment_label.xml」が追加される fragment_label.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".LabelFragment" > <TextView android:id="@+id/counterView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="0" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
フラグメントのプログラムを実装する ButtonFragment.kt
package org.refirio.fragment import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import org.refirio.fragment.databinding.FragmentButtonBinding class ButtonFragment : Fragment() { private var _binding: FragmentButtonBinding? = null private val binding get() = _binding!! /* * アクティビティがボタンクリック時のコールバックインターフェイスを実装していることを確認 */ override fun onAttach(context: Context) { super.onAttach(context) if (context !is OnButtonClickListener) { throw RuntimeException("リスナーを実装してください") } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding = FragmentButtonBinding.inflate(inflater, container, false) binding.button.setOnClickListener { /* * ボタンクリック時のリスナーをセット */ val listener = context as? OnButtonClickListener listener?.onButtonClicked() } return binding.root } override fun onDestroyView() { super.onDestroyView() _binding = null } /* * ボタンクリック時のコールバックインターフェイスを定義 */ interface OnButtonClickListener { fun onButtonClicked() } }
LabelFragment.kt
package org.refirio.fragment import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import org.refirio.fragment.databinding.FragmentLabelBinding class LabelFragment : Fragment() { private var _binding: FragmentLabelBinding? = null private val binding get() = _binding!! private var counter = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) counter = // フラグメントが再生成されたら値を取り出す savedInstanceState?.getInt("counter") // フラグメントのargumentsプロパティから値を取り出す ?: arguments?.getInt("counter") // 値がなければ0をセット ?: 0 } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding = FragmentLabelBinding.inflate(inflater, container, false) binding.counterView.text = counter.toString() return binding.root } /* * フラグメントの再生性に対応するため、カウントの値を保存する */ override fun onSaveInstanceState(outState: Bundle) { //super.onSaveInstanceState(outState) outState.putInt("counter", counter) } override fun onDestroyView() { super.onDestroyView() _binding = null } /* * カウントアップして表示を更新する */ fun update() { counter++ binding.counterView.text = counter.toString() } } /* * フラグメントのコンストラクタに引数を渡せるようにする */ fun newLabelFragment(value : Int) : LabelFragment { val fragment = LabelFragment() val args = Bundle() args.putInt("counter", value) // フラグメントのargumentsプロパティは、フラグメントが再生性されても引き継がれる fragment.arguments = args return fragment }
メインアクティビティとそのレイアウトを実装する activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.5" /> <fragment android:id="@+id/buttonFragment" android:name="org.refirio.fragment.ButtonFragment" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toTopOf="@+id/guideline" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:layout="@layout/fragment_button" /> <FrameLayout android:id="@+id/container" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/guideline"> </FrameLayout> </androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
package org.refirio.fragment import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import org.refirio.fragment.databinding.ActivityMainBinding class MainActivity : AppCompatActivity(), ButtonFragment.OnButtonClickListener { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) val view = binding.root setContentView(view) /* * フラグメントを動的に追加する */ if (supportFragmentManager.findFragmentByTag("LabelFragment") == null) { supportFragmentManager.beginTransaction().apply { add(R.id.container, newLabelFragment(0), "LabelFragment") commit() } } } /* * ButtonFragmentのコールバックインターフェイスを実装する */ override fun onButtonClicked() { val fragment = supportFragmentManager.findFragmentByTag("LabelFragment") as LabelFragment fragment.update() } }
■スライドショー 以下で新規にプロジェクトを作成 プロジェクトの選択: Empty Activity プロジェクトの名前: slideshow ビューバインディングを使えるようにする build.gradle を変更したら「Sync Now」をクリック 使用する画像を用意する 画像を選択してコピーし、app/res/drawable へコピーする 画像表示用のフラグメントを作成する プロジェクトウインドウのツリーで「app」を右クリックし、 「New → Fragment → Fragment(Blank)」を選択する 表示されたダイアログで「Fragment Name」で「ImageFragment」と入力して、あとはデフォルトのまま「Finish」ボタンを押す プロジェクトウインドウのツリーに「ImageFragment.kt」と「fragment_image.xml」が追加される フラグメントに画像を配置する fragment_image.xml を開き、最初から表示されているTextViewを削除し、 「common」内の「ImageView」をドラッグ&ドロップで中央に配置する 画像選択のダイアログが表示されるので、「backgrounds/scenic」を選択して「OK」をクリックする これ背景用の風景写真が表示されるサンプルデータ。プレビュー用なので、実際にプログラム実行時には表示されない (レイアウトエディタで画像は未設定にしておき、プログラムから表示する画像を指定するような場合に使用することができる) また、以下の設定を行う id: imageView layout_width: match_parent layout_height: match_parent scaleType: centerCrop ImageFragment.kt を以下のように変更する
package org.refirio.slideshow import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import org.refirio.slideshow.databinding.FragmentImageBinding private const val IMG_RES_ID = "IMG_RES_ID" class ImageFragment : Fragment() { private var imageResId: Int? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { imageResId = it.getInt(IMG_RES_ID) } } private var _binding: FragmentImageBinding? = null private val binding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding = FragmentImageBinding.inflate(inflater, container, false) return binding.root } override fun onDestroyView() { super.onDestroyView() _binding = null } companion object { @JvmStatic fun newInstance(imageResId: Int) = ImageFragment().apply { arguments = Bundle().apply { putInt(IMG_RES_ID, imageResId) } } } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) imageResId?.let { binding.imageView.setImageResource(it) } } }
ViewPager2を使う activity_main.xml を開き、最初から表示されているTextViewを削除し、 「Containers」内の「ViewPager2」をドラッグ&ドロップで中央に配置する 上下左右を画面の端に接続し、マージンは0にする また、以下の設定を行う id: pager layout_width: 0dp layout_height: 0dp MainActivity を以下のように変更する
package org.refirio.slideshow import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter import org.refirio.slideshow.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { class MyAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { private val resources = listOf( R.drawable.brownie, R.drawable.caramel, R.drawable.donut, R.drawable.lemon ) override fun getItemCount(): Int = resources.size override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(resources[position]) } private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.pager.adapter = MyAdapter(this) } }
実行すると、画像がスワイプで順に表示される なお、上記コードの onCreate を以下のように変更すると、5秒ごとに自動で表示が切り替わる (「val handler」以降のみを追加)
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.pager.adapter = MyAdapter(this) val handler = Handler(Looper.getMainLooper()) timer(period = 5000) { handler.post { binding.apply { pager.currentItem = (pager.currentItem + 1) % 4 } } } }
また、activity_main.xml の「Attrivutes」で「All Atrrivutes → orientation」プロパティを「vertical」に設定すると、 左右ではなく上下で画面が切り替わるようになる
■アプリの作成(XMLレイアウト / データの保存)
■SharedPreferencesでデータを保存 以下で新規にプロジェクトを作成 プロジェクトの選択: Empty Activity プロジェクトの名前: sharedpreferences ビューバインディングを使えるようにする build.gradle を変更したら「Sync Now」をクリック ビューにWebViewを配置する activity_main.xml を開き、最初から表示されているTextViewを削除し、 「Text」内の「PlainText」をドラッグ&ドロップで中央に配置する 上下左右を画面の端に接続し、マージンは0にする 垂直方向のバイアスは30にしておく また、以下の設定を行う id: editText layout_width: wrap_content layout_height: wrap_content text: Text 「Buttons」内の「Button」をドラッグ&ドロップで中央に配置する 上をtextViewの下に接続し、下左右を画面の端に接続し、マージンは0にする 垂直方向のバイアスは20にしておく また、以下の設定を行う id: saveButton layout_width: wrap_content layout_height: wrap_content text: 保存 MainActivity を以下のように変更する
package org.refirio.sharedpreferences import android.content.Context import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import org.refirio.sharedpreferences.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) val view = binding.root setContentView(view) // SharedPreferencesオブジェクトを取得 val testPref = getSharedPreferences("test", Context.MODE_PRIVATE) // プリファレンスから、保存されている文字列を取得 val storedText = testPref.getString("edit", "テスト") binding.editText.setText(storedText) // ボタンがタップされたときの処理 binding.saveButton.setOnClickListener { val inputText = binding.editText.text.toString() testPref.edit().putString("edit", inputText).apply() } } }
■アプリの作成(XMLレイアウト / WebView)
■WebViewでWebページを表示 以下で新規にプロジェクトを作成 プロジェクトの選択: Empty Activity プロジェクトの名前: webview ビューバインディングを使えるようにする build.gradle を変更したら「Sync Now」をクリック マニフェストファイルを編集し、インターネットに接続できるようにする 追加場所は、ルートであるmanifestの直下でいい manifests/AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
ビューにWebViewを配置する activity_main.xml を開き、最初から表示されているTextViewを削除し、 「Wedgets」内の「WebView」をドラッグ&ドロップで中央に配置する 上下左右を画面の端に接続し、マージンは0にする また、以下の設定を行う id: webView layout_width: match_parent layout_height: match_parent さらに、アクティビティを以下のように変更 MainActivity.kt
package org.refirio.webview import android.os.Bundle import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient import androidx.appcompat.app.AppCompatActivity import org.refirio.webview.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) val view = binding.root setContentView(view) binding.webView.loadUrl("https://refirio.net/") binding.webView.setWebViewClient(object : WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { return false } }) } }
GitHub - tyfkda/GawaNativeAndroid: 全画面に配置したWebViewでAndroidアプリを作るテスト https://github.com/tyfkda/GawaNativeAndroid [Android] アプリのタイトルバーを非表示、全画面表示にする、Theme.NoTitleBar https://akira-watson.com/android/theme-notitlebar.html WebViewでlinkタップ時にブラウザに飛ばないようにする - 布団の中にいたい https://asahima.hatenablog.jp/entry/2017/01/08/000000
■アプリの作成(XMLレイアウト / リスト表示)
■ListViewで一覧を表示 以下で新規にプロジェクトを作成 プロジェクトの選択: Empty Activity プロジェクトの名前: listview ビューバインディングを使えるようにする build.gradle を変更したら「Sync Now」をクリック ビューにListViewを配置する activity_main.xml を開き、最初から表示されているTextViewを削除し、 「Legacy」内の「ListView」をドラッグ&ドロップで中央に配置する 上下左右を画面の端に接続し、マージンは0にする また、以下の設定を行う id: timezonesView layout_width: match_parent layout_height: match_parent リスト用のレイアウトを作成する プロジェクトウインドウのツリーで「app → res → layout」を右クリックし、 「New → Layout resource file」を選択する 「New Resource File」ダイアログが開くので、「File name」に「list_timezone_cell」と入力して「OK」をクリックする 作成された list_timezone_cell.xml を開き、 「Text」内の「TextView」をドラッグ&ドロップで中央に配置する 上下左右を画面の端に接続し、マージンは8にする また、以下の設定を行う id: timezoneView layout_width: wrap_content layout_height: wrap_content さらに、アクティビティを以下のように変更 MainActivity.kt
package org.refirio.listview import android.os.Bundle import android.widget.ArrayAdapter import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import org.refirio.listview.databinding.ActivityMainBinding import java.util.* class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) val view = binding.root setContentView(view) // リストに表示するタイムゾーン val timeZones = TimeZone.getAvailableIDs() //val timeZones = listOf("タイムゾーン1", "タイムゾーン2", "タイムゾーン3") // アダプタを作成 val adapter = ArrayAdapter<String>( this, R.layout.list_timezone_cell, R.id.timezonesView, timeZones ) // リストにアダプタをセット binding.timezonesView.adapter = adapter // リストのアイテムタップ時の動作 binding.timezonesView.setOnItemClickListener { parent, view, position, id -> // アダプタから、タップされた位置のタイムゾーンを得る val timeZone = adapter.getItem(position) // Toastで表示 Toast.makeText(applicationContext, timeZone, Toast.LENGTH_SHORT).show(); } } }
■RecyclerViewで一覧を表示 以下で新規にプロジェクトを作成 プロジェクトの選択: Empty Activity プロジェクトの名前: recyclerview ビューバインディングを使えるようにする build.gradle を変更したら「Sync Now」をクリック ビューにRecyclerViewを配置する activity_main.xml を開き、最初から表示されているTextViewを削除し、 「Containers」内の「RecyclerView」をドラッグ&ドロップで中央に配置する 上下左右を画面の端に接続し、マージンは0にする また、以下の設定を行う id: timezonesView layout_width: match_parent layout_height: match_parent リスト用のレイアウトを作成する プロジェクトウインドウのツリーで「app → res → layout」を右クリックし、 「New → Layout resource file」を選択する 「New Resource File」ダイアログが開くので、「File name」に「list_timezone_cell」と入力して「OK」をクリックする 作成された list_timezone_cell.xml を開き、 「Text」内の「TextView」をドラッグ&ドロップで中央に配置する 上下左右を画面の端に接続し、マージンは8にする また、以下の設定を行う id: timezoneView layout_width: match_constraint layout_height: wrap_content 親のConstraintLayoutに以下の設定を行う layout_width: match_parent layout_height: wrap_content リスト用のアダプタを作成する プロジェクトウインドウのツリーで「app → java → org.refirio.recyclerview」を右クリックし、 「New → Kotlin Class/File」を選択する 「New Kotlin Class/File」ダイアログが開くので、「File name」に「TimezoneAcapter」と入力してEnterを入力する 作成された TimezoneAcapter を開き、以下を入力する
package org.refirio.recyclerview import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView class TimezoneAdapter( context: Context, private val timeZones: Array<String>, private val onItemClicked: (String) -> Unit ) : RecyclerView.Adapter<TimezoneAdapter.TimezoneViewHolder>() { // レイアウトからViewを生成するInflater private val inflater = LayoutInflater.from(context) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TimezoneViewHolder { // Viewを生成する val view = inflater.inflate(R.layout.list_timezone_cell, parent, false) // ViewHolderを作る val viewHolder = TimezoneViewHolder(view) // Viewをタップしたときの処理 view.setOnClickListener { // アダプター上の家を得る val position = viewHolder.adapterPosition // 位置に応じたデータを得る val timeZone = timeZones[position] // コールバックを呼び出す onItemClicked(timeZone) } return viewHolder } override fun getItemCount() = timeZones.size override fun onBindViewHolder(holder: TimezoneViewHolder, position: Int) { // 位置に応じたデータを得る val timeZone = timeZones[position] // 表示内容を更新する holder.timezone.text = timeZone } // Viewへの参照を持っておくViewHolder class TimezoneViewHolder(view: View) : RecyclerView.ViewHolder(view) { val timezone = view.findViewById<TextView>(R.id.timezoneView) } }
さらに、MainActivity を以下のように変更
package org.refirio.recyclerview import android.os.Bundle import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import org.refirio.recyclerview.databinding.ActivityMainBinding import java.util.* class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) val view = binding.root setContentView(view) // リストに表示するタイムゾーン val timeZones = TimeZone.getAvailableIDs() //val timeZones = arrayOf("タイムゾーン1", "タイムゾーン2", "タイムゾーン3") // アダプタを作成 val adapter = TimezoneAdapter(this, timeZones) { timeZone -> // Toastで表示 Toast.makeText(this, timeZone, Toast.LENGTH_SHORT).show(); } // リストにアダプタをセット binding.timezonesView.adapter = adapter // 縦に直線的に表示するレイアウトマネージャをセット binding.timezonesView.layoutManager = LinearLayoutManager( this, LinearLayoutManager.VERTICAL, false ) } }
■RecyclerViewを並び替え 【Android】簡潔に RecyclerView を使う。 - 山崎屋の技術メモ https://www.shookuro.com/entry/android-recycler-view 【Android】イメージを含んだリッチな行を持つ RecyclerView - 山崎屋の技術メモ https://www.shookuro.com/entry/2021/02/11/145746 【Android】RecyclerView、行をドラッグして並び替え - 山崎屋の技術メモ https://www.shookuro.com/entry/recycler-view3 【Android】RecyclerView つまみ(ハンドル)をドラッグして並び替え - 山崎屋の技術メモ https://www.shookuro.com/entry/recycler-view4 Kotlin beginner: kotlin リストをドラッグして並べ替え(ItemTouchHelper) https://cony-kotlin.blogspot.com/2020/10/kotlin-recyclerviewitemtouchhelper.html RecyclerView の使い方。ドラッグ&ドロップで並び替え、スワイプで削除する。【Android】 https://negichou.com/recyclerview-and-itemtouchhelper-sample/
■アプリの作成(XMLレイアウト / パーミッション)
■パーミッション Marshmallow端末で、Permission利用確認をする。 http://qiita.com/mattak/items/82ba07259cfe3a2ce4b1 Android6では AndroidManifest.xml でのパーミッション指定を行った上で、 protectionLevel が dangerous 以上のパーミッションについてはさらに権限の確認を行う必要がある ■ファイル一覧を表示 以下で新規にプロジェクトを作成 プロジェクトの選択: Empty Activity プロジェクトの名前: filelist ビューバインディングを使えるようにする build.gradle を変更したら「Sync Now」をクリック マニフェストファイルを編集し、インターネットに接続できるようにする 追加場所は、ルートであるmanifestの直下でいい manifests/AndroidManifest.xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
ビューにRecyclerViewを配置する activity_main.xml を開き、最初から表示されているTextViewを削除し、 「Containers」内の「RecyclerView」をドラッグ&ドロップで中央に配置する 上下左右を画面の端に接続し、マージンは0にする また、以下の設定を行う id: filesView layout_width: match_parent layout_height: match_parent リスト用のレイアウトを作成する プロジェクトウインドウのツリーで「app → res → layout」を右クリックし、 「New → Layout resource file」を選択する 「New Resource File」ダイアログが開くので、「File name」に「list_file_cell」と入力して「OK」をクリックする 作成された list_file_cell.xml を開き、 「Text」内の「TextView」をドラッグ&ドロップで中央に配置する 上下左右を画面の端に接続し、マージンは8にする また、以下の設定を行う id: fileView layout_width: wrap_content layout_height: wrap_content 親のConstraintLayoutに以下の設定を行う layout_width: match_parent layout_height: wrap_content リスト用のアダプタを作成する プロジェクトウインドウのツリーで「app → java → org.refirio.filelist」を右クリックし、 「New → Kotlin Class/File」を選択する 「New Kotlin Class/File」ダイアログが開くので、「File name」に「FileAcapter」と入力してEnterを入力する 作成された FileAcapter を開き、以下を入力する
package org.refirio.filelist import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import java.io.File class FileAdapter( context: Context, private val files: List<File>, private val onItemClicked: (File) -> Unit ) : RecyclerView.Adapter<FileAdapter.FileViewHolder>() { // レイアウトからViewを生成するInflater private val inflater = LayoutInflater.from(context) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder { // Viewを生成する val view = inflater.inflate(R.layout.list_file_cell, parent, false) // ViewHolderを作る val viewHolder = FileViewHolder(view) // Viewをタップしたときの処理 view.setOnClickListener { /* // アダプター上の家を得る val position = viewHolder.adapterPosition // 位置に応じたデータを得る val timeZone = timeZones[position] */ // コールバックを呼び出す onItemClicked(files[viewHolder.adapterPosition]) } return viewHolder } override fun getItemCount() = files.size override fun onBindViewHolder(holder: FileViewHolder, position: Int) { // 位置に応じたデータを得る val file = files[position].name // 表示内容を更新する holder.file.text = file } // Viewへの参照を持っておくViewHolder class FileViewHolder(view: View) : RecyclerView.ViewHolder(view) { val file = view.findViewById<TextView>(R.id.fileView) } }
さらに、MainActivity を以下のように変更
package org.refirio.filelist import android.Manifest import android.content.pm.PackageManager import android.os.Bundle import android.os.Environment import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import org.refirio.filelist.databinding.ActivityMainBinding import java.io.File class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private var currentDir : File = Environment.getExternalStorageDirectory() private lateinit var recyclerView: RecyclerView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) val view = binding.root setContentView(view) binding.filesView.layoutManager = LinearLayoutManager( this, LinearLayoutManager.VERTICAL, false ) // パーミッションを確認 if (hasPermission()) { // ファイル一覧を表示 showFiles() } } private fun showFiles() { val adapter = FileAdapter( this, currentDir.listFiles().toList() ) { file -> if (file.isDirectory) { currentDir = file showFiles() } else { // Toastで表示 Toast.makeText(this, file.absolutePath, Toast.LENGTH_SHORT).show(); } } // リストにアダプタをセット binding.filesView.adapter = adapter // アプリバーに表示中のディレクトリのパスを設定する title = currentDir.path } private fun hasPermission() : Boolean { // パーミッションを持っているか確認 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // 持っていないならパーミッションを要求 ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 1) return false } return true } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { //super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (!grantResults.isEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { showFiles() } else { finish() } } override fun onBackPressed() { if (currentDir != Environment.getExternalStorageDirectory()) { currentDir = currentDir.parentFile showFiles() } else { super.onBackPressed() } } }
■アプリの作成(XMLレイアウト / ドロワーメニュー)
以下で新規にプロジェクトを作成 プロジェクトの選択: Empty Activity プロジェクトの名前: drawer ビューバインディングを使えるようにする build.gradle を変更したら「Sync Now」をクリック メニュー用のフラグメントを作成する プロジェクトウインドウのツリーで「app」を右クリックし、 「New → Fragment → Fragment(Blank)」を選択する 表示されたダイアログで「Fragment Name」で「MenuFragment」と入力して、あとはデフォルトのまま「Finish」ボタンを押す プロジェクトウインドウのツリーに「MenuFragment.kt」と「fragment_menu.xml」が追加される fragment_menu.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="0dp" android:layout_height="0dp" android:gravity="center_horizontal" android:orientation="vertical" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <Button android:id="@+id/firstButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="8dp" android:text="First" /> <Button android:id="@+id/secondButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="8dp" android:text="Second" /> <Button android:id="@+id/thirdButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="8dp" android:text="Third" /> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
MenuFragment
package org.refirio.drawer import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import androidx.fragment.app.Fragment class MenuFragment() : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_menu, container, false) val firstButton = view.findViewById<Button>(R.id.firstButton) firstButton.setOnClickListener { val listener = context as? OnButtonClickListener listener?.onFirstButtonClicked() } val secondButton = view.findViewById<Button>(R.id.secondButton) secondButton.setOnClickListener { val listener = context as? OnButtonClickListener listener?.onSecondButtonClicked() } val thirdButton = view.findViewById<Button>(R.id.thirdButton) thirdButton.setOnClickListener { val listener = context as? OnButtonClickListener listener?.onThirdButtonClicked() } return view } interface OnButtonClickListener { fun onFirstButtonClicked() fun onSecondButtonClicked() fun onThirdButtonClicked() } }
コンテンツ用のフラグメントを作成する プロジェクトウインドウのツリーで「app」を右クリックし、 「New → Fragment → Fragment(Blank)」を選択する 表示されたダイアログで「Fragment Name」で「FirstFragment」と入力して、あとはデフォルトのまま「Finish」ボタンを押す プロジェクトウインドウのツリーに「FirstFragment.kt」と「fragment_first.xml」が追加される fragment_first.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="First" android:textAppearance="@style/TextAppearance.AppCompat.Large" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
FirstFragment.kt
package org.refirio.drawer import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment class FirstFragment() : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.fragment_first, container, false) return view } }
同様に SecondFragment と ThirdFragment を作成する (SecondFragment.kt と fragment_second.xml と ThirdFragment.kt と fragment_third.xml が追加される。) メインアクティビティとそのレイアウトを実装する activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/drawerLayout" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <!-- メインコンテンツ --> <FrameLayout android:id="@+id/container" android:layout_width="wrap_content" android:layout_height="wrap_content"> </FrameLayout> <!-- ドロワーコンテンツ --> <FrameLayout android:id="@+id/drawer" android:layout_width="240dp" android:layout_height="match_parent" android:layout_gravity="start" android:background="?android:attr/colorBackground"> <fragment android:id="@+id/fragment" android:name="org.refirio.drawer.MenuFragment" android:layout_width="match_parent" android:layout_height="match_parent" tools:layout="@layout/fragment_menu" /> </FrameLayout> </androidx.drawerlayout.widget.DrawerLayout>
MainActivity.kt
package org.refirio.drawer import android.content.res.Configuration import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.view.MenuItem import androidx.appcompat.app.ActionBarDrawerToggle import androidx.drawerlayout.widget.DrawerLayout class MainActivity : AppCompatActivity(), MenuFragment.OnButtonClickListener { // ドロワーの状態操作用オブジェクト private var drawerToggle : ActionBarDrawerToggle? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) if (supportFragmentManager.findFragmentByTag("ContentFragment") == null) { supportFragmentManager.beginTransaction() .add(R.id.container, FirstFragment(), "ContentFragment") .commit() } // レイアウトからドロワーを探す val drawerLayout = findViewById<DrawerLayout>(R.id.drawerLayout) // ドロワーを作成する val toggle = ActionBarDrawerToggle(this, drawerLayout, R.string.app_name, R.string.app_name) toggle.isDrawerIndicatorEnabled = true drawerLayout.addDrawerListener(toggle) // ドロワーの状態を保持 drawerToggle = toggle // ドロワーの設定を行う supportActionBar?.apply { setDisplayHomeAsUpEnabled(true) setHomeButtonEnabled(true) } } override fun onFirstButtonClicked() { supportFragmentManager.beginTransaction() .replace(R.id.container, FirstFragment()) .addToBackStack(null) .commit() } override fun onSecondButtonClicked() { supportFragmentManager.beginTransaction() .replace(R.id.container, SecondFragment()) .addToBackStack(null) .commit() } override fun onThirdButtonClicked() { supportFragmentManager.beginTransaction() .replace(R.id.container, ThirdFragment()) .addToBackStack(null) .commit() } // アクティビティの生成が終わった後に呼ばれる override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState) // ドロワーのトグルの状態を回復する drawerToggle?.syncState() } // 画面構成が変わったときに呼ばれる override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) // 状態の変化をドロワーに伝える drawerToggle?.onConfigurationChanged(newConfig) } // オプションメニューがタップされたときに呼ばれる override fun onOptionsItemSelected(item: MenuItem): Boolean { // ドロワーに伝える if (drawerToggle?.onOptionsItemSelected(item) == true) { return true } else { return super.onOptionsItemSelected(item) } } }
ここまででいったん完成 引き続き、 ・縦表示ならドロワーメニュー ・横表示ならサイドメニュー としてみる 横表示用にレイアウトを追加する activity_main.xml を開き、「Design」タブで表示する タブ内のツールバーから「Orientation in Editor → Create Landscape Variation」を選択する 以下のファイルが作成される。内容はもとの activity_main.xml と同じものになる app/src/main/res/layout-land/activity_main.xml このファイルを以下のように修正する (rootのIDをdrawerLayoutから別のものに変えると、「Configurations for activity_main.xml must agree on the root element's ID.」のエラーになった このエラーを回避するために、rootのDrawerLayoutは触らずにLinearLayoutを追加している 正しい方法かどうかは不明)
<?xml version="1.0" encoding="utf-8"?> <androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/drawerLayout" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <LinearLayout android:id="@+id/linearLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <!-- ドロワーコンテンツ --> <FrameLayout android:id="@+id/drawer" android:layout_width="240dp" android:layout_height="match_parent" android:layout_gravity="start" android:background="?android:attr/colorBackground"> <fragment android:id="@+id/fragment" android:name="org.refirio.drawer.MenuFragment" android:layout_width="match_parent" android:layout_height="match_parent" tools:layout="@layout/fragment_menu" /> </FrameLayout> <!-- メインコンテンツ --> <FrameLayout android:id="@+id/container" android:layout_width="wrap_content" android:layout_height="wrap_content"> </FrameLayout> </LinearLayout> </androidx.drawerlayout.widget.DrawerLayout>
MainActivity.kt を以下のように変更する
package org.refirio.drawer import android.content.res.Configuration import android.os.Bundle import android.view.MenuItem import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity import androidx.drawerlayout.widget.DrawerLayout class MainActivity : AppCompatActivity(), MenuFragment.OnButtonClickListener { // ドロワーの状態操作用オブジェクト private var drawerToggle : ActionBarDrawerToggle? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) if (supportFragmentManager.findFragmentByTag("ContentFragment") == null) { supportFragmentManager.beginTransaction() .add(R.id.container, FirstFragment(), "ContentFragment") .commit() } val orientation = resources.configuration.orientation // 縦向きの場合 if (orientation == Configuration.ORIENTATION_PORTRAIT) { val drawerLayout = findViewById<DrawerLayout>(R.id.drawerLayout) setupDrawer(drawerLayout) } } override fun onFirstButtonClicked() { supportFragmentManager.beginTransaction() .replace(R.id.container, FirstFragment()) .addToBackStack(null) .commit() } override fun onSecondButtonClicked() { supportFragmentManager.beginTransaction() .replace(R.id.container, SecondFragment()) .addToBackStack(null) .commit() } override fun onThirdButtonClicked() { supportFragmentManager.beginTransaction() .replace(R.id.container, ThirdFragment()) .addToBackStack(null) .commit() } // アクティビティの生成が終わった後に呼ばれる override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState) // ドロワーのトグルの状態を回復する drawerToggle?.syncState() } // 画面構成が変わったときに呼ばれる override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) // 状態の変化をドロワーに伝える drawerToggle?.onConfigurationChanged(newConfig) } // オプションメニューがタップされたときに呼ばれる override fun onOptionsItemSelected(item: MenuItem): Boolean { // ドロワーに伝える if (drawerToggle?.onOptionsItemSelected(item) == true) { return true } else { return super.onOptionsItemSelected(item) } } private fun setupDrawer(drawerLayout: DrawerLayout) { // ドロワーを作成する val toggle = ActionBarDrawerToggle(this, drawerLayout, R.string.app_name, R.string.app_name) toggle.isDrawerIndicatorEnabled = true drawerLayout.addDrawerListener(toggle) // ドロワーの状態を保持 drawerToggle = toggle // ドロワーの設定を行う supportActionBar?.apply { setDisplayHomeAsUpEnabled(true) setHomeButtonEnabled(true) } } }
■アプリの作成(XMLレイアウト / カメラ)
以下で新規にプロジェクトを作成 プロジェクトの選択: Empty Activity プロジェクトの名前: camerax ビューバインディングを使えるようにする さらに、カメラを使うために以下も追加する
dependencies { 〜略〜 def camerax_version = "1.0.0-alpha01" implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}" }
build.gradle を変更したら「Sync Now」をクリック マニフェストファイルを編集し、撮影と保存ができるようにする 追加場所は、ルートであるmanifestの直下でいい manifests/AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
引き続きまとめ中
■アプリの作成(XMLレイアウト / テンプレートから作成)
Androidでログイン機能を作る場合、AndroidStudio標準のテンプレートにあるので利用できるかも その他、標準のテンプレートが以下で解説されている テンプレートからコードを追加する | Android Developers https://developer.android.com/studio/projects/templates?hl=ja
■作例(XMLレイアウト / RSSリーダー)
以下で新規にプロジェクトを作成 プロジェクトの選択: Empty Activity プロジェクトの名前: reader ビューバインディングを使えるようにする build.gradle を変更したら「Sync Now」をクリック 画像表示のために、Picassoを使えるようにする 同ファイルの dependencies 内の implementation の一覧の最後に以下を追加
implementation 'com.squareup.picasso:picasso:2.71828'
build.gradle を変更したら「Sync Now」をクリック なお、バージョンとして「2.5.2」を指定すると「Sync Now」は表示されなかった https://github.com/square/picasso を参考に、「2.71828」を指定すると表示された Webページ表示のために、Chrome Custom Tabsを使えるようにする 同ファイルの dependencies 内の implementation の一覧の最後に以下を追加
implementation 'androidx.browser:browser:1.0.0'
build.gradle を変更したら「Sync Now」をクリック 今も「androidx.browser:browser:1.0.0」の指定で正しいかどうかは確認しておきたい なお「implementation 'com.android.support:customtabs:27.1.1'」という指定は古い方法だと思われる APK creation failure due to Duplicate class android.support - Stack Overflow https://stackoverflow.com/questions/59239626/apk-creation-failure-due-to-duplicate-class-android-sup... AWS CognitoにAndroidネイティブアプリでサインインする - Qiita https://qiita.com/poruruba/items/12985ec474364594be55 マニフェストファイルを編集し、インターネットに接続できるようにする 追加場所は、ルートであるmanifestの直下でいい manifests/AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
リスト用のレイアウトを作成する res/layout/list_article_cell.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/titleView" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="4dp" android:layout_marginEnd="8dp" android:layout_marginBottom="4dp" android:textAppearance="@style/TextAppearance.AppCompat.Medium" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/imageView" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.08" tools:text="ニュースタイトル" /> <TextView android:id="@+id/nameView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="4dp" android:layout_marginEnd="8dp" android:layout_marginBottom="4dp" android:textAppearance="@style/TextAppearance.AppCompat.Medium" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/titleView" tools:text="名前" /> <ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="4dp" android:layout_marginEnd="8dp" android:layout_marginBottom="4dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.98" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:srcCompat="@android:drawable/ic_popup_sync" /> </androidx.constraintlayout.widget.ConstraintLayout>
メイン画面のレイアウトを調整する res/layout/lactivity_main.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/articlesView" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:listitem="@layout/list_article_cell" /> </androidx.constraintlayout.widget.ConstraintLayout>
app/java/org.refirio.reader/HttpTask.kt
package org.refirio.reader import android.os.AsyncTask import android.util.Log import java.io.* import java.net.HttpURLConnection import java.net.URL class HttpTask(callback: (String?) -> Unit) : AsyncTask<String, Unit, String>() { var callback = callback override fun doInBackground(vararg params: String): String? { val url = URL(params[1]) val httpClient = url.openConnection() as HttpURLConnection httpClient.setReadTimeout(10 * 1000) httpClient.setConnectTimeout(10 * 1000) httpClient.requestMethod = params[0] if (params[0] == "POST") { httpClient.instanceFollowRedirects = false httpClient.doOutput = true httpClient.doInput = true httpClient.useCaches = false httpClient.setRequestProperty("Content-Type", "application/json; charset=utf-8") } try { if (params[0] == "POST") { httpClient.connect() val os = httpClient.getOutputStream() val writer = BufferedWriter(OutputStreamWriter(os, "UTF-8")) writer.write(params[2]) writer.flush() writer.close() os.close() } if (httpClient.responseCode == HttpURLConnection.HTTP_OK) { val stream = BufferedInputStream(httpClient.inputStream) val data: String = readStream(inputStream = stream) return data } else { Log.d("HttpTask", "ERROR: ${httpClient.responseCode}") } } catch (e: Exception) { e.printStackTrace() } finally { httpClient.disconnect() } return null } fun readStream(inputStream: BufferedInputStream): String { val bufferedReader = BufferedReader(InputStreamReader(inputStream)) val stringBuilder = StringBuilder() bufferedReader.forEachLine { stringBuilder.append(it) } return stringBuilder.toString() } override fun onPostExecute(result: String?) { super.onPostExecute(result) callback(result) } }
app/java/org.refirio.reader/ArticleAcapter.kt
package org.refirio.reader import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.squareup.picasso.Picasso import org.json.JSONArray import org.json.JSONObject import java.text.ParseException import java.text.SimpleDateFormat class ArticleAcapter( context: Context, private val articles: JSONArray, private val onItemClicked: (JSONObject) -> Unit ) : RecyclerView.Adapter<ArticleAcapter.ViewHolder>() { // レイアウトからViewを生成するInflater private val inflater = LayoutInflater.from(context) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { // Viewを生成する val view = inflater.inflate(R.layout.list_article_cell, parent, false) // ViewHolderを作る val viewHolder = ViewHolder(view) // Viewをタップしたときの処理 view.setOnClickListener { // アダプター上の家を得る val position = viewHolder.adapterPosition // 位置に応じたデータを得る val article = articles.getJSONObject(position) // コールバックを呼び出す onItemClicked(article) } return viewHolder } override fun getItemCount() = articles.length() override fun onBindViewHolder(holder: ViewHolder, position: Int) { // 位置に応じたデータを得る val article = articles.getJSONObject(position) // 表示内容を更新する holder.title.text = article.getString("title") holder.name.text = "by " + article.getString("name") + " at " + article.getString("datetime").toDateTime("MM/dd HH:mm") Picasso.get() .load(article.getString("image")) // 画像のURL .resize(100, 100) // 表示サイズを指定 .centerCrop() // 中央から切り出し .into(holder.image) // imageViewに流し込み } // Viewへの参照を持っておくViewHolder class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val title = view.findViewById<TextView>(R.id.titleView) val name = view.findViewById<TextView>(R.id.nameView) val image = view.findViewById<ImageView>(R.id.imageView) } } fun String.toDateTime(pattern: String = "yyyy/MM/dd HH:mm:ss"): String { val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") val datetime = try { dateFormat.parse(this) } catch (e: ParseException) { null } if (datetime == null) { return "" } else { return SimpleDateFormat(pattern).format(datetime) } }
app/java/org.refirio.reader/MainActivity.kt
package org.refirio.reader import android.net.Uri import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.browser.customtabs.CustomTabsIntent import androidx.recyclerview.widget.LinearLayoutManager import org.json.JSONObject import org.refirio.reader.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) val view = binding.root setContentView(view) // JSONを読み込み HttpTask({ if (it == null) { println("Data is empty.") return@HttpTask } // JSONを解析する val json = JSONObject(it) val articles = json.getJSONArray("articles") // RecyclerViewをレイアウトから探す //val recyclerView = findViewById<RecyclerView>(R.id.articles) // RecyclerViewにアダプターをセットする binding.articlesView.adapter = ArticleAcapter(this, articles) { article -> // 記事をタップしたらChrome Custom Tabsで開く val intent = CustomTabsIntent.Builder().build() intent.launchUrl(this, Uri.parse(article.getString("url"))) } // レイアウトマネージャーをセットする binding.articlesView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) }).execute("GET", "https://refirio.org/reader/?type=json") } }
■非同期通信
※未検証 ※AsyncTaskと同等の処理をCoroutinesという機能で実装できるようになっているとのこと Kotlin Coroutine 入門1: 起動と suspend - Qiita https://qiita.com/wm3/items/48b5b5c878561ff4761a KotlinとCoroutinesでAyncTask - Qiita https://qiita.com/naoi/items/8be7d7e331668f6c67d4 Kotlin 1.3をサクッと学ぶ - CoroutineとKotlin/Nativeを触って理解しよう - エンジニアHub|Webエンジニアのキャリアを考える! https://eh-career.com/engineerhub/entry/2020/02/04/103000
■製品用、開発用などの切り分け
パッケージ名が同じアプリは上書きインストールされる つまり、GooglePlayからインストールした本番アプリがあると、その端末には開発版をインストールできない が、フレーバーを使用することによりこの問題を解消できる flavorDimensionsによるflavorの指定方法 - Qiita https://qiita.com/boohbah/items/389b159a1693247b15de Android StudioでFlavor・BuildTypeを分岐させてアプリビルドする詳細手順 | PisukeCode - Web開発まとめ https://pisuke-code.com/android-studio-build-variants/ 開発環境・検収環境・本番環境での処理の分岐も、この仕組みで対応できそう ■前提 productFlavors production ... 製品用 develop ... 開発用 buildTypes release ... 製品用 debug ... 開発用 と設定するものとする ■プロジェクトの作成 新規 Android Studio プロジェクトの開始 ↓ 新規プロジェクトの作成 アプリケーション名: Build Test1 会社ドメイン: refirio.net プロジェクトの場所: C:\Users\refirio\AndroidStudioProjects\BuildTest1 (アプリケーション名をもとに自動入力される) パッケージ名: net.refirio.buildtest1 (アプリケーション名と会社ドメインをもとに自動入力される) Kitlinサポートを含める: チェックする 「次へ」 ↓ ターゲットAndroidデバイス 「API 19: Android 4.4 (KitKat)」にして「次へ」 ↓ Mobile にアクティビティを追加する 「空のアクティビティ」が選択されているので、そのまま「次へ」 ↓ アクティビティの設定 特に変更せず「完了」 エミュレータと実機で、アプリを起動できるかテストする ■Flavorの設定 app の build.gradle の
buildTypes {
の上(下ではない)に以下のコードを追加
flavorDimensions "mode" productFlavors { production { } develop { applicationIdSuffix ".dev" } }
さらに
buildTypes {
の中に以下のコードを追加 (デバッグ版書き出し時に「.debug」を付けたくないなら、省略しても良さそう。要検証)
debug { applicationIdSuffix ".debug" }
さらに manifestPlaceholders を追加 (デバッグ版とリリース版で別のアプリ名を表示する準備)
android { compileSdkVersion XX defaultConfig { 〜 略 〜 manifestPlaceholders = [appName:"@string/app_name"] } 〜 略 〜 productFlavors { 〜 略 〜 develop { 〜 略 〜 manifestPlaceholders = [appName:"@string/app_name_dev"] } } }
app\src\main\AndroidManifest.xml のアプリ名を調整 (デバッグ版とリリース版で別のアプリ名を表示する準備)
android:label="@string/app_name" ↓ android:label="${appName}"
app\src\main\res\values\strings.xml で開発版のアプリ名を追加 (デバッグ版とリリース版で別のアプリ名を定義)
<string name="app_name">Build Test1</string> <string name="app_name_dev">Build Test1 Dev</string> … 追加
■ビルドの切り替え 画面左端の「ビルド・バリアント」をクリックすると、その中の「ビルド・バリアント」部分でビルドを切り替えられる 「developDebug」と「productionDebug」を切り替えると、名前の違うアプリをそれぞれインストールできる 「developRelease」と「productionRelease」は何故かエラーになって実行できない プロジェクト作成直後も「debug」と「release」から選択できるが、この「release」も実行できない リリース版は署名の登録が必要…などがあるみたい 要勉強 Android Studio : debugビルドとReleaseビルドの切替、releaseビルドの追加方法、署名付きapk作成方法 - 生活を良くします-怠惰なプログラミング https://www.what-a-day.net/entry/2016/12/11/001948 ■ビルド設定によるプログラムの分岐 設定が完了した上で実行すると、以下にプログラムが自動作成される app\build\generated\source\buildConfig\production\debug\net\refirio\buildtest1\BuildConfig.java この内容を利用して、以下のように分岐する
if (BuildConfig.DEBUG) { Log.d("TEST", "DEBUG") } else { Log.d("TEST", "RELEASE") } if ("develop".equals(BuildConfig.FLAVOR)) { Log.d("TEST", "develop") } else { Log.d("TEST", "production") }
■その他参考になりそうなページ アプリケーション ID の設定 | Android Developers https://developer.android.com/studio/build/application-id?hl=ja https://developer.android.com/studio/build/application-id?hl=ja#%E3%83%93%E3%83%AB%E3%83%89_%E3%83%9...
■野良アプリとして書き出す
■APKの書き出し ビルド Build Bundle(s) / APK(s) APKのビルド プロジェクトの app\build\outputs\apk\debug 内に app-debug.apk という名前で出力されている (実行しなくても、ビルドの度に自動作成されているみたい?) ■ダウンロードページの作成 /download/index.php
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>テスト</title> </head> <body> <h1>テスト</h1> <p>デバイスのブラウザより、以下のリンクからインストールをお願いします。</p> <ul> <li><a href="apk/app-debug.apk">Android用アプリをインストール</a></li> </ul> </body> </html>
■ダウンロードと起動 ※あらかじめ、APKから直接インストールできるように端末を設定しておく Android.txt の「野良アプリのインストールを許可」を参照 上記ページにChromeでアクセスし、「Android用アプリをインストール」リンクを長押しする(タップだとインストールできないことがある) ダイアログが表示されるので 「リンクをダウンロード」 を選択する。ダウンロードが完了すると 「開く」 が表示されるのでタップ 「このアプリケーションをインストールしてもよろしいですか?」 と聞いてくる 「インストール」 をタップでインストールされる もしくは、過去にダウンロード済みのファイルをタップする ダイアログが表示され 「インストール」 を選択すると 「この既存のアプリにアップデートをインストールしますか?」 と聞いてくる 「インストール」 をタップでインストールされる ■ダウンロードと起動の補足 ※文言や細かな挙動は、端末によって異なる可能性がある 「ファイルをダウンロードするには、Chromeでストレージへのアクセスを許可する必要があります」 と表示された場合、 「続行」 を選択。遷移先の画面で 「権限」の「ストレージ」 で許可して元の画面に戻る 「セキュリティ上の理由から、お使いのスマートフォンではこの提供元からの不明なアプリをインストールすることはできません」 と表示された場合、 「設定」 を選択。遷移先の画面で 「この提供元のアプリを許可」 で許可して元の画面に戻る 「この種類のファイルはお使いの端末に悪影響を与える可能性があります。」 と表示された場合、 「OK」 にするとダウンロードされる 「もう一度ダウンロードしますか?」 と聞いてきた場合、 「ダウンロード」 にするとダウンロードされる 開く際にアプリの選択を求められたら 「パッケージインストーラー」 を選択する ダウンロードしたファイルをタップして 「ファイルを開けません」 と聞いてきた場合、最初の手順でリンクをタップせずに長押ししてダウンロードする 「パッケージの解析中にエラーが発生しました」 と表示された場合、要求されるOSのバージョンを満たしていない可能性がある それでもインストールできない場合、AndroidStudioでの書き出し手順に問題が無いか確認する ■アンインストール 設定 → アプリケーション → (アプリ名) → アンインストール を選択すると 「このアプリをアンインストールしますか?」 と聞いてくるので 「OK」 でアンインストールされる その状態で再度インストールすると、普通にインストールされた なお、アプリ一覧画面の右上ボタンで「アプリの設定をリセット」を選択すると、諸々の許可設定などがリセットされる 間違った選択をしてインストールできなくなった場合などに試す 以下の問題は過去のものかもしれないが、インストールできない場合に確認する Android Lollipopでapkがインストールできない問題 - Qiita https://qiita.com/orangain/items/a70f5b774296e609fb75
■作業アカウントの追加
■Google Play デベロッパー アカウント ユーザーの追加と権限の管理 - Play Console ヘルプ https://support.google.com/googleplay/android-developer/answer/2528691?hl=ja ■Firebase プロジェクト メンバーを管理する - Firebase ヘルプ https://support.google.com/firebase/answer/7000272?hl=ja
■テスト
テスト公開については要調査 以下はテスターを追加したときのメモ ■テスターの追加 Google Play Consoleで 「設定 → テスターの管理 → リストを作成」 からグループとユーザを追加。すでにグループが存在していれば、該当のリストを編集してメールアドレスを追加
■リリース
Google Play Storeにアプリを公開する - Qiita https://qiita.com/minuro/items/536ac3f7c27c1442a1cb 【Androidアプリ開発 vol.12】完成したAndroidアプリをGooglePlayストアで公開する手順? | くねおの電脳リサーチ https://kuneoresearch.com/android-appdev12-release-to-google-play01/
■トラブル
■エミュレータがインターネットに繋がらない MacでAPI27のエミュレータがインターネットに繋がらない場合、 Mac側で「システム環境設定 → ネットワーク → 詳細 → DNS」に「8.8.8.8」と「8.8.4.4」を追加する 「OK → 適用」として確認する AndroidStudio付属のエミュレータがネットワークに繋がらない時の対処法(API27で動作確認) | takelab.note https://wandering-engineer.tech/2019/08/23/post-4626/ ■既存プロジェクトを開くとエラー 古いプロジェクトを開くと、以下のようなエラーが表示されるものがあった Calendar エラー :(52, 0) Could not find property 'debugKeystore' on SigningConfig_Decorated{name=debug, storeFile=C:\Users\refirio\.android\debug.keystore, storePassword=android, keyAlias=AndroidDebugKey, keyPassword=android, storeType=C:\Users\refirio\.android\debug.keystore}. <a href="openFile:C:\Users\refirio\AndroidStudioProjects\Calendar\app\build.gradle">Open File</a> Camera エラー :(52, 0) Could not get unknown property 'debugKeystore' for SigningConfig_Decorated{name=debug, storeFile=C:\Users\refirio\.android\debug.keystore, storePassword=android, keyAlias=AndroidDebugKey, keyPassword=android, storeType=C:\Users\refirio\.android\debug.keystore, v1SigningEnabled=true, v2SigningEnabled=true} of type com.android.build.gradle.internal.dsl.SigningConfig. <a href="openFile:C:\Users\refirio\AndroidStudioProjects\Camera\app\build.gradle">Open File</a> gradle.properties を作成する C:\Users\refirio\AndroidStudioProjects\Camera\gradle.properties
#デバッグ用のデバッグ署名 debugKeystore="../../../../../.android/debug.keystore" DEV_KEY_ALIAS=androiddebugkey DEV_STORE_PASSWORD=android DEV_KEY_PASSWORD=android #release用の署名情報 productKeystore="../../../xxxx.keystore" KEY_ALIAS=xxxx STORE_PASSWORD=xxxx KEY_PASSWORD=xxxx
設定するとエラーの内容が変わった エラー :Failed to find target with hash string 'android-25' in: C:\Users\refirio\AppData\Local\Android\Sdk <a href="install.android.platform">Install missing platform(s) and sync project</a> リンクをクリックすると、Android SDK Platform 25 のダウンロードとインストールがはじまった その後も何度かインストールを促されるので、すべてインストール ひととおりインストールしたら実行ボタン(実機書き出し)を押せるようになったが、実行すると C:\Users\refirio\AndroidStudioProjects\Camera\app\src\main\java\xxx\app\util\GoogleAnalyticsUtil.java エラー :(65, 70) エラー: シンボルを見つけられません シンボル: 変数 analytics 場所: クラス xml と言われ、ソースコードの tracker = GoogleAnalytics.getInstance(mContext).newTracker(R.xml.analytics); にフォーカスが当たった 1行目の package xxx.app.util; に赤線が引かれて The SDK platform-tools version (24.0.1) is too old to check APIs compiled with API 25 と言われる。APIレベル25までインストールが必要 ツール → Android → SDK Manager でAndroid7.1.1(APIレベル25)までインストール AndroidStudioを再起動 アップデートがあると言われたのでインストール プロジェクトを開くと、Gradleの同期とビルドが始まった 実行してみるが、それでも C:\Users\refirio\AndroidStudioProjects\Camera\app\src\main\java\xxx\app\util\GoogleAnalyticsUtil.java エラー :(65, 70) エラー: シンボルを見つけられません シンボル: 変数 analytics 場所: クラス xml と言われた。リポジトリ作成者に確認すると、res/xml/ 内に必要なファイルが無かった プッシュされていなかっただけだったので、プッシュ&プルで取り込んで解決した ■ビルド時に Kotlin Gradle plugin version のエラーになる このファイルの「AndroidStudioをバージョンアップしたときのメモ」を参照 ■Android Studio が起動しない studio.exe と同じ場所に studio.bat があるので、これをコマンドプロンプトから実行する 起動時にエラーがあれば、以下のようにエラーメッセージが表示される >studio.bat Error opening zip file or JAR manifest missing : C:\Users\Refirio\.AndroidStudio3.2\config\jp.sourceforge.mergedoc.pleiades\pleiades.jar Error occurred during initialization of VM agent library failed to init: instrument
■その他メモ
Androidアプリエンジニアの基礎知識 - Speaker Deck https://speakerdeck.com/nein37/androidapurienziniafalseji-chu-zhi-shi もしあなたが急にAndroidアプリを業務で作るはめになった場合の選択肢(2021年初頭版) - Qiita https://qiita.com/Gazyu/items/dafdb74c4aadf722da92 [kotlin]アンドロイドでリアルタイム画像認識アプリをつくる - Qiita https://qiita.com/YS-BETA/items/cd412524932dda9ac44c 【Kotlin】OpenCVをインストールして使う方法 | 西住工房 https://algorithm.joho.info/programming/kotlin/opencv-install-kt/

Advertisement