■目次
アカウントAndroidStudio環境の作成AndroidStudioのバージョンアップGitの連携プロジェクトの作成アプリの作成アプリの作成(フラグメント)アプリの作成(データの保存)アプリの作成(WebView)アプリの作成(リスト表示)アプリの作成(パーミッション)アプリの作成(ドロワーメニュー)アプリの作成(テンプレートから作成)作例(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/ WindowsもMacも、インストールで詰まることは無かった インストール完了後も、何かと追加で色々とダウンロード&インストールさせられる エミュレータも、初回起動時はイメージのダウンロードが必要 Ver 3.1.4 時点では、主に以下の書籍を参考にした 基本からしっかり身につくAndroidアプリ開発入門 https://www.amazon.co.jp/dp/479739580X ■インストール参考ページ(Ver 3.1.2 時点) Android Studioインストール https://akira-watson.com/android/adt-windows.html JDKインストール http://techfun.cc/java/windows-jdk-install.html http://techfun.cc/java/windows-jdk-pathset.html ■インストールメモ(Ver 2.3.3 時点) http://www.oracle.com/technetwork/java/javase/downloads/index.htmlhttp://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html から jdk-8u91-windows-x64.exe をダウンロード&インストール https://developer.android.com/studio/ から android-studio-bundle-143.2821654-windows.exe をダウンロード&インストール Android Studio の起動時にエラーが表示されたことがあったが、JavaとJDKの再インストールで直った。 SDK Manager で、Android 4.3 〜 7.1.1 までインストール。 ■日本語化(Ver 3.3.2 時点) ※日本語化は Android Studio 終了中に実行しないと反映されないことがあるかも ※毎回バージョンアップが手間なので、Ver 4 からは英語のまま使っている 以下の方法で日本語化できた studio64.exe.vmoptions は初めから config 内にあった Android Studio3.1を日本語化する | HIROMARTBLOG http://hiromart.com/blog/androidstudiojapanese/ Macの場合、以下の手順で日本語化できた MacでAndroid Studioを日本語化する - ソフラボの技術ブログ http://shinsuke789.hatenablog.jp/entry/2018/08/27/130714 ■日本語化(Ver 2.3.3 時点) 以下の方法で日本語化できた。一部は英語のままだけど特に問題はなし Android Studio 2.0 を日本語化してみた | 寿司すき焼き相撲 http://s3wordpress.wpblog.jp/2016/05/18/android-studio-2-0-%E3%82%92%E6%97%A5%E6%9C%AC%E8%AA%9E%E5%8... ■ショートカットの変更 Ctrl+Yは、Redoではなく行削除が割り当てられている 一般的なショートカットと異なるので余計なトラブルのもとだが、設定で変更はできる http://qiita.com/decchi/items/f8603ccccec03a71a4d9 ■エディタの設定 ファイル → 設定 → Editor → 外観 行番号を表示する 空白を表示する ■デバッグ 画面下部の「ログキャット」をクリックすると、デバッグログを確認できる 端末、アプリ、デバッグレベルなどを絞り込んで確認する ■メモリの割り当て ※現状未設定 http://tools.android.com/tech-docs/configuration
■AndroidStudioのバージョンアップ
http://www.youfit.co.jp/archives/885 Ver 2.1.2 → Ver 2.2.3 のとき、この手順でバージョンアップできた Ver 2.2.3 → Ver 3.1.2 のとき、この手順でバージョンアップできた。が、メニューが英語表記に戻った Ver 3.1.2 → Ver 3.2.0 のとき、この手順でバージョンアップできた。が、メニューが英語表記に戻った ■AndroidStudioが起動しなくなった場合 Macで発生 アップデートして使えているかと思ったが、Mac再起動後はAndroidStudioを起動できなくなった エラーなどは何も確認できない 最新の日本語化パッチを当てると起動できるようになった AndroidStudioがアップデート後に起動しない時 - Qiita https://qiita.com/filu_/items/6470ab95b45a4382e34e ■メニューが英語表記に戻った場合 「AndroidStudio環境を作った時のメモ」の「日本語化」の手順を再度行って日本語化する Ver 2.3.3 時点のメモだが、同じ手順で日本語化できた ■Kotlin Gradle plugin version のエラーになった場合 The Android Gradle plugin supports only Kotlin Gradle plugin version 1.2.51 and higher. Project 'HelloKotlin' is using version 1.2.30. と表示された場合、指示に従ってGradleファイルを書き換える C:\Users\Refirio\AndroidStudioProjects\HelloKotlin\build.gradle ext.kotlin_version = '1.2.30' ↓ ext.kotlin_version = '1.2.51' 書き換えたら「再試行」を実行する ■AndroidStudioをベータ版から正式版にしたときのメモ 以下からAndroidStudioをダウンロード&インストール https://developer.android.com/studio/
■Gitの連携
■SourceTreeとの連携 AndroidStduio内でgit操作は可能らしいが、SourceTreeで管理する方法 http://ameblo.jp/hunnyjams/entry-11961454576.html gitのパスは以下のようになる。環境によって変わる可能性はある C:\Program Files\Git\cmd\git.exe AndroidStudioが build/ や app/build/ などに自動でファイルを吐き出すので、 可能なら最初の段階で .gitignore の対象にしておく。一度リポジトリに入れてしまうと、後から外すのは面倒 今は何もしなくてもAndroidStudioが .gitignore を自動作成してくれるみたい? ■.gitignore AndroidStudioが自動で作成するかも。要確認 以下は設定例
*.iml .gradle /local.properties /.idea/caches/build_file_checksums.ser /.idea/libraries /.idea/modules.xml /.idea/workspace.xml .DS_Store /build /captures .externalNativeBuild
以下を参考に作成すると良さそう Androidアプリの.gitignore - Qiita https://qiita.com/jumperson/items/fa66995fb68de2847ffd Android Studioのバージョン管理対象ファイル - ソフトウェアエンジニアリング - Torutk http://www.torutk.com/projects/swe/wiki/Android_Studio%E3%81%AE%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%...
■プロジェクトの作成
■プロジェクトの新規作成(Ver 3.3.2 時点) 以下の手順で作成できる プロジェクトが作成されたら、何も変更せずにいったんエミュレータと実機での起動を確認しておくといい 新規 Android Studio プロジェクトの開始 ↓ プロジェクトの選択 「空のアクティビティ」が選択されているので、そのまま「次へ」 ↓ プロジェクトの構成 名前: helloworld パッケージ名: net.refirio.helloworld (名前をもとに自動入力される) 保存ロケーション: C:\Users\refirio\AndroidStudioProjects\helloworld (アプリケーション名をもとに自動入力される) 言語: Kotlin 最小APIレベル: API 19: Android 4.4 (KitKat) 「完了」
■アプリの作成
■Kotlin入門 JavaプログラマのためのKotlin入門 - Qiita https://qiita.com/koher/items/bcc58c01c6ff2ece658f KotlinでAndroid開発 - Qiita https://qiita.com/paulbarre/items/fb4565c37287d173a78f Android開発を受注したからKotlinをガッツリ使ってみたら最高だった - Qiita https://qiita.com/omochimetaru/items/98e015b0b694dd97f323 正式採用の「Kotlin」で挑戦! 初めてのAndroidアプリ開発 〜ストップウォッチを作ってみよう〜 - エンジニアHub|若手Webエンジニアのキャリアを考える! https://employment.en-japan.com/engineerhub/entry/2017/06/23/110000 最近話題の「Kotlin」は本当に業務に使えるの? ―国内第一人者と「Yahoo!ニュース」Android版開発者が語るKotlin開発実践のコツ:CodeZine(コードジン) https://codezine.jp/article/detail/10730 ■Kotlinメモ 【Kotlin入門】配列よりも実用性のあるListの使い方|茶トラネコ日記 https://itneko.com/kotlin-list/ Kotlin のコレクション使い方メモ - Qiita https://qiita.com/opengl-8080/items/36351dca891b6d9c9687 変数を定義する (val, var) | まくまくKotlinノート https://maku77.github.io/kotlin/basic/var.html Android(Java)のあれ、Kotlinでどう書くの? - Qiita https://qiita.com/tporange/items/6cde7a64b4f41072b1c9 Kotlin の Collection まとめ 〜List編〜 - Qiita https://qiita.com/kiririnyo/items/aee905225902d096f7c0 Kotlin のコレクション使い方メモ - Qiita https://qiita.com/opengl-8080/items/36351dca891b6d9c9687 Kotlinの小技 - Qiita https://qiita.com/kichinaga/items/6d57bb868af6a13496c0 [Kotlin] Set型の特徴とList型との相互変換 | プログラミング日和 https://pouhon.net/kotlin-set/1422/ KotlinでMutableSetを作る - Qiita https://qiita.com/mihyaeru21/items/9a293cd1fefd59116751 [Kotlin] List型の特徴とMutableList | プログラミング日和 https://pouhon.net/kotlin-list/1411/ Kotlin のコレクション・配列早見表 - Qiita https://qiita.com/arumati-yk/items/ad2492f481b1f5ed6cdc [android開発] AndroidManifest.xmlの設定一覧(ネットワーク系) - 行け!偏差値40プログラマー http://hensa40.cutegirl.jp/archives/6501 AndroidStudio付属のエミュレータがネットワークに繋がらない時の対処法(API27で動作確認) | takelab.note https://wandering-engineer.tech/2019/08/23/post-4626/ 【正誤情報】『基本からしっかり身につくAndroidアプリ開発入門 Android Studio 3.x対応』|SBクリエイティブ https://www.sbcr.jp/support/14723/ Kotlinで文字列を数値、日付文字列を日付(java.util.Date)、日付を文字列(String)に変換する - Qiita https://qiita.com/emboss369/items/5a3ddea301cbf79d971a Android アプリの ListView で非同期の画像を表示する方法 - Qiita https://qiita.com/tomalatte001/items/345c55603148b1ce41f2 [Android] Picasso でネット上の画像をGridViewで表示 https://akira-watson.com/android/gridview-picasso.html 【Android】picassoでURLから画像をよしなに扱う【Kotlin】 - wrongwrongな開発日記 https://wrongwrong163377.hatenablog.com/entry/2018/09/09/165250 Androidアプリ開発でImageViewを追加する方法【初心者向け】 | TechAcademyマガジン https://techacademy.jp/magazine/3573 Kotlinで初期化を遅延する | RE:ENGINES https://re-engines.com/2018/11/15/kotlin%E3%81%A7%E5%88%9D%E6%9C%9F%E5%8C%96%E3%82%92%E9%81%85%E5%BB... ActivityとFragmentのライフサイクルと罠 - Qiita https://qiita.com/chibi929/items/78f0d3aa2ab4a0229978 Kotlin スコープ関数 用途まとめ - Qiita https://qiita.com/ngsw_taro/items/d29e3080d9fc8a38691e Android はじめてのFragment イベント編 - Qiita https://qiita.com/Reyurnible/items/d6397f5fbb03ee4fb93b ActivityとFragmentの連携を理解する (Android Kotlin) https://101010.fun/posts/android-try-fragment.html android - Fragmentをreplaceしても一つ前のFragmentが残る - スタック・オーバーフロー https://ja.stackoverflow.com/questions/6342/fragment%E3%82%92replace%E3%81%97%E3%81%A6%E3%82%82%E4%B... ■Kotlinファースト 今後ツールやライブラリは、まずKotlin向けが最初に作られるとのこと (ただしその後、JavaやC++向けのものも作られるとのことなので、他の言語が使えなくなるわけではない) 可能ならKotlinで作るほうが良さそう 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 ■ハローワールド 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でカメラは仕様が大きく変わっているみたい ■プッシュ 「Googleクラウドメッセージング(GCM)」が1年後に廃止、「Firebase Cloud Messaging(FCM)」への移行が必要に:Googleのアプリメッセージング基盤が完全に交代 - @IT http://www.atmarkit.co.jp/ait/articles/1804/13/news051.html
■アプリの作成(フラグメント)
■フラグメント 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」に設定すると、 左右ではなく上下で画面が切り替わるようになる
■アプリの作成(データの保存)
■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() } } }
■アプリの作成(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
■アプリの作成(リスト表示)
■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 ) } }
■アプリの作成(パーミッション)
■パーミッション 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() } } }
■アプリの作成(ドロワーメニュー)
以下で新規にプロジェクトを作成 プロジェクトの選択: 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) } } }
■アプリの作成(テンプレートから作成)
Androidでログイン機能を作る場合、AndroidStudio標準のテンプレートにあるので利用できるかも その他、標準のテンプレートが以下で解説されている テンプレートからコードを追加する | Android Developers https://developer.android.com/studio/projects/templates?hl=ja
■作例(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") } }
■製品用、開発用などの切り分け
パッケージ名が同じアプリは上書きインストールされる つまり、GooglePlayからインストールした本番アプリがあると、その端末には開発版をインストールできない が、フレーバーを使用することによりこの問題を解消できる flavorDimensionsによるflavorの指定方法 - Qiita https://qiita.com/boohbah/items/389b159a1693247b15de ■前提 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\jp\terraport\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で 「設定 → テスターの管理 → リストを作成」 からグループとユーザを追加。すでにグループが存在していれば、該当のリストを編集してメールアドレスを追加
■トラブル
■エミュレータがインターネットに繋がらない 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/