- アカウント
- AndroidStudio環境の作成
- AndroidStudioのバージョンアップ
- Gitの連携
- プロジェクトの作成
- エミュレータでの実行
- 実機での実行
- Kotlin
- アプリの作成(Jetpack Compose)
- アプリの作成(Jetpack Compose / 一覧)
- アプリの作成(Jetpack Compose / 一覧と詳細)
- アプリの作成(Jetpack Compose / 入力内容の表示)
- アプリの作成(Jetpack Compose / データの保存)
- アプリの作成(Jetpack Compose / JSONを扱う)
- アプリの作成(Jetpack Compose / Webサイトを開く)
- アプリの作成(Jetpack Compose / 起動時に処理を行う)
- アプリの作成(Jetpack Compose / HTMLを取得する)
- アプリの作成(Jetpack Compose / リストの登録編集削除)
- アプリの作成(Jetpack Compose / ビューモデルを扱う)
- アプリの作成(Jetpack Compose / カメラを扱う)
- アプリの作成(Jetpack Compose / Firebaseを扱う)
- アプリの作成(XMLレイアウト)
- アプリの作成(XMLレイアウト / フラグメント)
- アプリの作成(XMLレイアウト / データの保存)
- アプリの作成(XMLレイアウト / WebView)
- アプリの作成(XMLレイアウト / リスト表示)
- アプリの作成(XMLレイアウト / パーミッション)
- アプリの作成(XMLレイアウト / ドロワーメニュー)
- アプリの作成(XMLレイアウト / カメラ)
- アプリの作成(XMLレイアウト / テンプレートから作成)
- 作例(XMLレイアウト / RSSリーダー)
- 非同期通信
- 製品用、開発用などの切り分け
- 野良アプリとして書き出す
- 作業アカウントの追加
- テスト
- リリース
- トラブル
- その他メモ
アカウント
■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.html
の
http://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のバージョンアップ
Android Studioを最新版にバージョンアップ(更新)する方法 - 福岡・東京のシステム開発会社 (株)ユーフィット
https://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 のとき、この手順でバージョンアップできた。が、メニューが英語表記に戻った
「Android Studio Koala Feature Drop」へバージョンアップしたとき、Android Studio内で完結しなかった(いきなり公式サイトへ飛ばされた)
それ以外にも戸惑ったところがあったので、以下に作業時のメモを記載する
■Android Studio Koala Feature Dropへのバージョンアップ
画面右上にアップデートアイコンが表示されていたので、アップデートを実行
ブラウザで以下のページが表示された。そのまま進めて実行ファイル android-studio-2024.1.2.12-windows.exe をダウンロード
Android Studio とアプリツールのダウンロード - Android デベロッパー | Android Developers
https://developer.android.com/studio?utm_source=android-studio&hl=ja
いったんAndroidStudioを終了し、ダウンロードしたファイルを実行
最初に「Uninstall old version」の画面が表示されたので、内容に従ってアンインストールする
その後「Android Studio Setup」の画面が表示されたので、内容に従ってインストールする
インストールが完了すると、AndroidStudioが起動した
画面右下に「Unsupported Git Version 2.9.0 / At least 2.19.2 is required」と表示されているので、「Configure...」をクリック
「Setting → Version Control → Git」が表示されたが、ここからアップデートするわけでは無いみたい
Soucetree経由で起動したコンソールから確認すると、以下のとおりバージョンが表示された
$ git --version
git version 2.9.0.windows.1
以下の公式サイトから最新版を入手する
Git - Downloading Package
https://git-scm.com/downloads/win
「Click here to download」からダウンロードして実行
色々聞かれたが、デフォルトのまま進めた。途中、2.9.0がアンインストールされたみたい
Soucetree経由で起動したコンソールから確認すると、以下のとおりバージョンが表示された
また、GitHubのリポジトリとBitbucketのリポジトリの両方で、特に問題無くPULL/PUSHができた
$ git --version
git version 2.30.2.windows.1
改めてAndroidStudioを起動すると、Gitに関する警告は表示されなくなっていた
(画面右下に、Microsoft Defenderが何か制限しているような通知が表示された。「Automatically」をクリックすると必要な設定がされたようだが、特に関係ない?)
実機の接続確認をしようとするが、いつまで経っても認識されず
Device Managerを開くが、対象の端末は表示されず
一瞬表示されては消えるので、認識しようとして失敗しているような挙動に見える
ステータスバーに「android studio adb server start failed」のようなエラーメッセージが表示されている
windows10でadbが起動しなかったのはmicrosoft万歳だった件 #AndroidStudio - Qiita
https://qiita.com/Amb98/items/7f204150ed2d61693c89
Failed to start adb server in Android Studio - Stack Overflow
https://stackoverflow.com/questions/71705735/failed-to-start-adb-server-in-android-studio
とりあえずWindowsを再起動したが改善せず
android - adb のエラー現象について - スタック・オーバーフロー
https://ja.stackoverflow.com/questions/88903/adb-%E3%81%AE%E3%82%A8%E3%83%A9%E3%83%BC%E7%8F%BE%E8%B1...
「File → Setting → Build, Execution, Deployment → Debugger」の画面へ遷移
「untick "Enable adb mDNS for wireless debugging"」とあるが、そのような項目は無い
「ADB server mDNS backend」という項目はあり、「openscreen」が選択されている
ひとまず「default」として「OK」をクリック
念のためAndroidStudioを再起動
これで実機が認識された
アプリを実機にインストールできることも確認できた
■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%...
プロジェクトの作成
■プロジェクトの新規作成(Koala Feature Drop時点)
Flamingo時点と同じ手順で作成できた
「Minimum SDK」は「API 26 ("Oreo" Android 8.0)」を選択した
※プロジェクトはKotlin+Jetpackで作成された
言語などの選択欄は表示されなかった
■プロジェクトの新規作成(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/
■ハローワールド(Koala Feature Drop時点)
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.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
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)
enableEdgeToEdge()
setContent {
HelloWorldTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
HelloWorldTheme {
Greeting("Android")
}
}
以前に比べて enableEdgeToEdge() というコードが追加されている
edge-to-edgeについては、以下などのページを参照
APIレベルを上げると強制的に適用されるようなので、原則有効であるものと考える方が良さそう
アプリの対象 API レベル 35 で初めて edge-to-edge に対処する[Android View編] #Android - Qiita
https://qiita.com/seabat-dev/items/b1ee9c71674e80abccc7
Androidのedge-to-edge表示対応の作業メモ(Compose向け)
https://zenn.dev/tomoya0x00/articles/f854a6825a1182
■練習用にテーマを外した場合(Koala Feature Drop時点)
- - - - - - - - - - - - - - - - - - - -
package net.refirio.helloworld
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
MainScreen(modifier = Modifier.padding(innerPadding))
}
}
}
}
@Composable
fun MainScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text("Hello Android!!")
}
}
@Preview(showBackground = true)
@Composable
fun MainScreenPreview() {
MainScreen()
}
- - - - - - - - - - - - - - - - - - - -
■ハローワールド(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>
■練習用にテーマを外した場合(Flamingo時点)
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/net/refirio/list/MainActivity.kt
package net.refirio.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 net.refirio.list.ui.MainApp
import net.refirio.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/net/refirio/list/ui/MainApp.kt
package net.refirio.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 net.refirio.list.ui.add.AddScreen
import net.refirio.list.ui.edit.EditScreen
import net.refirio.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/net/refirio/list/ui/list/ListScreen.kt
package net.refirio.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 net.refirio.list.utils.Memo
import net.refirio.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/net/refirio/list/ui/list/ListTitle.kt
package net.refirio.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/net/refirio/list/ui/add/AddScreen.kt
package net.refirio.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 net.refirio.list.utils.Memo
import net.refirio.list.utils.generateRandomString
import net.refirio.list.utils.getMemo
import net.refirio.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/net/refirio/list/ui/edit/EditScreen.kt
package net.refirio.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 net.refirio.list.utils.Memo
import net.refirio.list.utils.getMemo
import net.refirio.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/net/refirio/list/utils/Memo.kt
package net.refirio.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/net/refirio/list/utils/Common.kt
package net.refirio.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/