iOSエンジニアのつぶやき

毎朝8:30に iOS 関連の技術について1つぶやいています。まれに釣りについてつぶやく可能性があります。

知識ゼロからの Kotlin Android アプリリリースへの軌跡 / Day4【Codelabs 1-3編】

前回のコードラボに続き、今回は01.3のコードラボを学習していきたいと思います。

前回の記事

yamato8010.hatenablog.com

学ぶこと

  • アプリのリソースにファイルを追加する方法
  • アプリのレイアウトで画像を使用する方法
  • アプリのコードで View をより効率的に見つける方法
  • XML のネームスペースを使用してアプリのデザインでプレースホルダー画像を使用する方法
  • アプリの Android API レベルと、最小、ターゲット、およびコンパイル済みの API レベルを理解する方法
  • アプリで Jetpack ライブラリを使用して、古いバージョンの Android をサポートする方法

やること

  • 前回のコードラボの DiceRoller アプリを変更して、数値ではなくダイの値の画像を含めます。
  • アプリのリソースに画像ファイルを追加します。
  • 数値ではなくダイの値に画像を使用するようにアプリのレイアウトとコードを更新します。
  • コードを更新して、View をより効率的に見つけます。
  • アプリの起動時に空の画像を使用するようにコードを更新します。
  • 古いバージョンの Android との下位互換性のために Android Jetpack ライブラリ を使用するようにアプリを更新します。

アプリの概要

今回は、前回のコードラボで作成した DiceRoller アプリに基づいてアプリを再構築し、サイコロを振ると変化する画像を追加します。最終的なアプリは下記のようなイメージになります。

画像リソースを追加および更新する👨‍💻

画像を追加する

  1. 前回の DiceRoller アプリプロジェクトを開きます。前回のプロジェクトがない場合はこちらからダウンロードすることが可能です。

  2. res の中にある drawable ディレクトリを開きます。

アプリは、画像やアイコン、色、文字列、XML レイアウトなど、様々なリソースを使用します。これらのリソースは全て res フォルダに保存されます。また、画像リソースなどは drawable フォルダに保存します。drawable フォルダー内にはすでに、アプリのランチャーアイコンリソースが存在しています。

  1. ic_launcher_background.xml をクリックします。これらはアイコンをベクター画像として記述する XML ファイルであることに注意してください。ベクトルを使用すると、画像をさまざまなサイズと解像度で描画できます。PNGGIF などのビットマップ画像は、デバイスごとにスケーリングする必要があり、品質が低下する可能性があります。

=> Xcode とかだと Png イメージとかは 3サイズ用意して、ある程度の解像度を担保させることができるけど Android だとどんな感じでインポートできるんだろう🤔

  1. こちらからアプリのサイコロ画像をダウンロードします。アーカイブを解凍すると次のような XML ファイルのフォルダーが表示されます。

  1. Android Studio で、現在 Android と表示されているプロジェクトビューの上部にあるドロップダウンメニューをクリックし、Project を選択します。

  1. DiceRoller > app > src > main > res > drawable. でファイルを開きます。

  2. ダウンロードした XML ファイルを drawable フォルダーにドラックアンドドロップし(フォルダーごとインポートしない)、OK をクリックします。

  3. プロジェクトを Android ビューに戻し、サイコロイメージの XML ファイルが drawable フォルダーにあることを確認します。

  4. dice_1.xml をクリックし、下記のように見えることを確認します。

=> AndroidXML ファイルで定義した色や形をベクター画像として扱えるんですね🙃

画像を使用できるようにレイアウトを更新する

res/drawables フォルダーにサイコロ画像ファイルができたので、アプリのレイアウトとコードからそれらのファイルにアクセスできます。このステップでは、数字を表示するために使用していた TextViewImageView に置き換えて画像を表示できるようにします。

  1. activity_main.xml を開きます。

  2. <TextView> 要素を削除します。

  3. 下記のような属性を持つ <ImageView> 要素を追加します。

<ImageView
   android:id="@+id/dice_image"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:layout_gravity="center_horizontal"
   android:src="@drawable/dice_1" />

ImageView を使用し、レイアウトに画像を表示します。ここで登場する新しい属性 android:srcでは、画像のリソースを指定します。@drawable/dice_1 の場合は、システムが res/drawable フォルダーの dice_1 ファイルを調べることを意味しています。

  1. レイアウトをプレビューすると下記のように表示されるようになります。

コードを更新する

  1. MainActivity を開いて、rollDice() メソッドを見ると、R.id.result_text が赤で強調表示されていることが確認できるかと思います。これは TextView をレイアウトから削除し、その ID が参照できなくなっているためです。
private fun rollDice() {
   val randomInt = (1..6).random()

   val resultText: TextView = findViewById(R.id.result_text)
   resultText.text = randomInt.toString()
}
  1. TextView を使用しなくなったので、resultText 変数に関するコードを削除します。

  2. findViewById() を使い、Layout ID(R.id.dice_image) から ImageView への参照を取得します。

var diceImage: ImageView = findViewById(R.id.dice_image)
  1. when ブロックを追加し、特定の数値に基づいたイメージを選択できるようにします。
val drawableResource = when (randomInt) {
   1 -> R.drawable.dice_1
   2 -> R.drawable.dice_2
   3 -> R.drawable.dice_3
   4 -> R.drawable.dice_4
   5 -> R.drawable.dice_5
   else -> R.drawable.dice_6
}

ID と同様に、R クラスの値を使用して、drawable フォルダー内のサイコロ画像を参照できます。

  1. ImageViewsetImageResource() メソッドで ImageView の画像を更新します。

  2. アプリをコンパイルして実行します。Roll ボタンをクリックすると適切な画像が更新されるのが確認できると思います。

View を効率的に見つける👨‍💻

このタスクでは、アプリをより効率的にする1つの方法について学びます。

  1. MainActivity を開き、rollDice()メソッドの中の diceImage 変数に注目してください。
val diceImage : ImageView = findViewById(R.id.dice_image)

この部分の処理では、rollDice() メソッドが呼ばれるたびに findViewById()ImageView への参照を取得します。findViewById() は、システムが毎回 View の階層全体を検索するためコストがかかる操作です。そのため呼び出しは極力最小限に抑える必要があります。

これらは今回のような小さなアプリでは、大きな問題ではありませんが、より性能の低い Androidバイスなどを使用している場合にはアプリが遅れる原因になる可能性があります。そのため、findViewById() を一回だけ呼び出して View オブジェクトをフィールドに格納することがベストプラクティスです。ImageView フィールドへの参照を保持することで、システムはいつでも View いアクセスができるようになり、パフォーマンスが向上します。

  1. クラスの上部で onCreate() メソッドの前に、ImageView を保持するフィールドを作成します。
var diceImage : ImageView? = null

理想的には、この変数を宣言するとき、またはコンストラクターでこの変数を初期化しますが、Android Activity ではコンストラクターを使用しません。実際、レイアウト内の View は setContentView() の呼び出しによって onCreate() メソッドで拡張された後まで、メモリ内のアクセス可能なオブジェクトにはなりません。

1つの方法は、この例のように、変数を nullable として定義することです。これによって、onCreate() 時に findViewById()ImageView を割り当てることができます。ただし、nullable のため使用する時に毎回値を確認する必要があるため、コードが複雑になります。下記でもっといい方法があります。

  1. diceImagelateinit を使用するように宣言を変更し、null 割り当てを削除します。
lateinit var diceImage : ImageView

この lateinit キーワードはコードが変数に対する操作を呼び出す前に変数が初期化されることを Kotlin コンパイラーに約束します。したがって、null で初期化する必要がなく、使用時に null を許容しない変数として扱うことができます。このように、View を保持するフィールドで lateinit を使用するのがベストプラクティスです。

=> Swift には無い表現ですね🤔 似てるとしたら force unwrap での変数を定義するとかですかね🤔

  1. では、onCreate() メソッド内の setContentView() メソッドの後で findViewById() を使って ImageView を取得します。
diceImage = findViewById(R.id.dice_image)
  1. rollDice() 内で定義した ImageView を削除し、クラスで定義した diceImage を使用するに変更します。
val diceImage : ImageView = findViewById(R.id.dice_image)
  1. アプリを実行して、期待通りに動作することを確認します。

デフォルトの画像を使用する👨‍💻

現在は、dice_1 を初期イメージとして使用していますが、最初にサイコロを振るまでは画像をまったく表示したくないとします。これを実現するにはいくつか方法が存在します。

  1. activity_main.xmlCode タブをクリックします。

  2. <ImageView> 要素の android:src 属性に "@drawable/empty_dice" をセットします。

android:src="@drawable/empty_dice" 

この empty_dice 画像は、ダウンロードして drawable フォルダーに追加した画像の1つです。これは、追加した他の画像と同じサイズですが、空です。この画像は、アプリが最初に起動された時に表示されるものになります。

  1. Design タブでプレビューを確認すると、空の画像がセットされている状態が確認できます。

  1. activity_main.xml で、android:src 行をコピーして、下記のように tools 属性にペースとします。
android:src="@drawable/empty_dice" 
tools:src="@drawable/empty_dice" />

XML のネームスペースを android から tools に変更しました。プレビューや Android Studio のデザインエディタで使用されているプレースホルダのコンテンツを定義する場合、tools ネームスペースが使用されます。tools を使用する属性は、アプリをコンパイルすると削除されます。

=> これはプレビューでレイアウトをデバックする時にめちゃめちゃ便利ですね🥺 Xcode にはこのような機能は無いので(基本的にはプログラムで動的に入れ替えるしかない)、ぜひ取り入れて欲しい。

ネームスペースは、同じ名前の属性を参照する時の曖昧さを解決するために使用されます。例えば、<ImageView> タグ内のこれらの属性は両方とも同じ名前(src)を持っていますが、ネームスペースが異なります。

  1. レイアウトファイルのルートにある <LinearLayout> 要素を調べ、ここで定義されている2つのネームスペースに注目してください。
<LinearLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   ...
  1. ImageViewtools:src 属性の値を dice_1 に変更してみてください。
android:src="@drawable/empty_dice" 
tools:src="@drawable/dice_1" />

プレビューのプレースホルダーが dice_1 になっていることに注目してください。

  1. アプリをコンパイルして実行します。実際のアプリでは、roll をクリックするまでダイのイメージは空です。

API レベルと互換性を理解する👨‍💻

Android 向けの開発の優れた点の1つは、Nexus One から Pixel、タブレットなどのフォームファクター、Pixelbooks、時計、テレビ、自動車まで、コードを実行できるデバイスの数が膨大であることです。

Android 用に作成する場合、これらの異なるデバイスごとに完全に別個のアプリを作成することはありません。溶けやテレビなどの根本的に異なるフォームファクターで実行されるアプリでもコードを共有することができます。ただし、これら全てをサポートするために注意する必要がある制約と互換性戦略があります。

このタスクでは、特定の Android API レベル(バージョン)をアプリのターゲットにする方法と、Android Jetpack ライブラリを使用して古いデバイスをサポートする方法を学びます。

API レベルを調べる

前回のコードラボでは、プロジェクトを作成する時に、アプリがサポートする必要がある特定の Android API レベルを指定しました。Android OS には、アルファベット順の美味しいおやつにちなんで名付けられたさまざまなバージョン番号があります。各 OS バージョンには、新しい機能が搭載されています。たとえば、Android Oreo はピクチャーインピクチャー がサポートされ、Android Pie は Slicesが導入されました。API レベルは、Android バージョンに対応しています。例えば、API 19 は Android4.4(KitKat)に対応しています。

ハードウェアがサポートできるもの、ユーザがデバイスを更新することを選択するかどうか、メーカーが異なる OS レベルをサポートするかどうかなど、多くの要因により、ユーザは必然的に異なる OS バージョンを実行するデバイスを使用します。

プリプロジェクトを作成する時に、アプリがサポートする最小 API レベルを指定します。つまり、アプリがサポートする最も古い Android バージョンを指定します。アプリには、コンパイルするレベルと、ターゲットとするレベルもあります。これらの各レベルは、Gradle ビルドファイルの構成パラメーターです。

  1. Gradle Scriptsbuild.gradle(Module:app) ファイルを開きます。

このファイルは、アプリモジュールに固有のビルドパラメータと依存関係を定義します。build.gradle(Project: DiceRoller) ファイルは、プロジェクト全体のビルド・パラメータを定義します。多くの場合、アプリモジュールはプロジェクト内の唯一のモジュールであるため、この分割は恣意的に見える場合があります。ただ、アプリがより複雑になり、それをいくつかのパートに分割した場合やアプリが Android Watch などのプラットフォームをサポートしている場合、同じプロジェクトで頃なるモジュールに遭遇する可能性があります。

  1. build.gradle ファイルの上部にある android セクションを調べます。(以下のサンプルはセクション全体ではありませんが、このコードラボで最も関心のあるものが含まれています。)
android {
   compileSdkVersion 28
   defaultConfig {
       applicationId "com.example.android.diceroller"
       minSdkVersion 19
       targetSdkVersion 28
       versionCode 1
       versionName "1.0"
   }
  1. compileSdkVersion パラメータを調べます。
compileSdkVersion 28

このパラメーターは、Gradle がアプリのコンパイルに使用する Android API レベルを指定します。これは、アプリがサポートできる Android の最新バージョンです。つまり、アプリはこの API レベル以下に含まれる API 機能を使用できます。この場合、アプリは、Android9(Pie) に対応する API28 をサポートします。

  1. defaultConfig セクション内にある targetSdkVersion パラメータを調べます。
targetSdkVersion 28

この値は、アプリをテストした最新の API です。多くの場合、これはcompileSdkVersion と同じ値です。

  1. minSdkVersion パラメーターを調べます。
minSdkVersion 19

このパラメーターは、アプリが実行される Android の最も古いバージョンを決定するため、3つのパラメーターの中で最も重要です。この API レベルより古い Android OS を実行するデバイスは、アプリをまったく実行できません。

アプリの最小 API レベルを選択するのは難しい場合があります。APIレベルを低く設定しすぎると、Android OS の新しい機能を見逃してしまいます。高すぎる値に設定すると、アプリは新しいデバイスでのみ実行される可能性があります。

プロジェクトを設定し、アプリの最小 API レベルを定義する場所に到達したら、Help me choose をクリックし、API Version Distribution ダイアログを表示します。ダイアログには、さまざまな OS レベルを使用するデバイスの数、および OS レベルで追加または変更された機能に関する情報が表示されます。Android ドキュメントのリリースノートとダッシュボードをチェックして、さまざまな API レベルをサポートすることの影響に関する詳細情報を確認することもできます。

互換性を調べる

さまざま Android API レベル用に作成することは、アプリ開発者が直面する共通の課題であるため、Android フレームワークチーム はこれらの開発者を支援するために多くの作業を行ってきました。

2011年、チームは最初のサポートライブラリをリリースしました。これは、下位互換性のあるクラスと便利な関数を提供する Google が開発したライブラリです。2018年に、GoogleAndroid Jetpack を発表しました。これは、サポートライブラリのクラスと関数を多く含み、サポートライブラリも拡張するライブラリコレクションです。

  1. MainActivity を開きます。
  2. MainActivity クラスが Activity クラス自体からではなく、AppCompatActivity() から拡張されていることに注目してください。
class MainActivity : AppCompatActivity() { 
...
}

AppCompatActivity は、Activity が異なるプラットフォームの OS レベルで同じに見えるようにする互換性クラスです。

  1. import をクリックして、クラスのインポートを展開します。AppCompatActivity クラスは androidx.appcompat.app パッケージからインポートされることに注意してください。Android Jetpack ライブラリの名前空間androidx です。

  2. build.gradle(Module: app) を開き、依存関係セクションまでスクロールします。

dependencies {
   implementation fileTree(dir: 'libs', include: ['*.jar'])
   implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version""
   implementation 'androidx.appcompat:appcompat:1.0.0-beta01'
   implementation 'androidx.core:core-ktx:1.0.1'
   implementation 'androidx.constraintlayout:constraintlayout:1.1.2'
   testImplementation 'junit:junit:4.12'
   androidTestImplementation 'androidx.test:runner:1.1.0-alpha4'
   androidTestImplementation 
        'androidx.test.espresso:espresso-core:3.1.0-alpha4'
}

androidx の一部であり、AppCompatActivity クラスを含む appcompat ライブラリへの依存関係に注意してください。

Vector Drawables の互換性を追加する

ネームスペース、Gradle、互換性に関する新しい知識を使用して、アプリに最後の調整を1つ加えます。これにより、古いプラットフォームでのアプリのサイズが最適化されます。

  1. res/drawable の中の dice イメージを1つクリックします。

すでに学んだように、全てのサイコロ画像は実際にはサイコロの色と形を定義する XML ファイルです。これらの種類のファイルは、Vector Drawables と呼ばれます。Vector Drawables が PNG などのビットマップイメージ形式よりも優れている点は、Vector Drawable が品質を損なうことなくスケーリングできることです。また、Vector Drawable は通常、ビットマップ形式の同じ画像よりもはるかに小さいファイルです。

Vector Drawable について注意すべき重要な点は、それらが API21以降でサポートされていることです。ただし、アプリの最小 SDK は API19 に設定されています。API19 デバイスまたはエミュレーターでアプリを試した場合、アプリは正常にビルドおよび実行されているように見えますが、これはどのように機能するのでしょうか。

アプリをビルドすると、Gradle ビルドプロセスによって各 Vector ファイルから PNG ファイルが生成され、それらの PNG ファイルは 21 未満の全ての Androidバイスで使用されます。これらの余分な PNG ファイルにより、アプリのサイズが大きくなります。不必要に大きいアプリはダウンロードが遅くなり、デバイスの限られたスペースをより多く使用します。大規模なアプリは、アンインストールされたり、ユーザがそれらのアプリのダウンロードに失敗したり、ダウンロードをキャンセルしたりする可能性も高くなります。

幸いなことに、API レベル 7までずっと Vector Drawable 用の AndroidX 互換ライブラリがあることです。

  1. build.gradle(Module: app) を開きます。defaultConfig セクションに次の行を追加します。
vectorDrawables.useSupportLibrary = true
  1. Editor 上部の Sync Now ボタンをクリックします。build.gradle ファイルが変更されるたびに、ビルドファイルをプロジェクトと同期する必要があります。

  2. activity_main.xml レイアウトファイルを開きます。このネームスペースを <LinearLayout> タグの tools ネームスペースの下に追加します。

xmlns:app="http://schemas.android.com/apk/res-auto"

app のネームスペースは、カスタムコードまたはライブラリからの属性であり、コアの Android フレークワークではありません。

  1. <ImageView>android:src 属性を app:srcCompat に変更します。
app:srcCompat="@drawable/empty_dice"

この app:srcCompat 属性は、AndroidX ライブラリを使用して、古いバージョンの Android Vector Drawable をサポートし、APIレベル7 に戻します。

  1. アプリをビルドして実行します。画面には何も表示されませんが、アプリはどこで実行されても、サイコロイメージに生成された PNG ファイルを使用する必要がないため、アプリファイルが小さくなります。

まとめ

アプリリソース

  • アプリのリソースには、画像とアイコン、アプリで使用される標準色、文字列、XML レイアウトを含めることができます。これらのリソースは全て res フォルダに保存されます。

  • アプリの全ての画像リソースを drawable フォルダに置く必要があります。

ImageView で Vector Drawable を使用する

  • Vector Drawable は、XML形式で記述された画像です。Vector Drawable は、任意のサイズまたは解像度にスケリングできるため、ビットマップイメージ(PNGファイルなど)よりも柔軟性があります。

  • drawable をアプリのレイアウトに追加するには、<ImageView> 要素を使用します。画像のそーすは android:src 属性にあります。リソースを参照するには @drawable を使用します。(例: @drawable/image_name)

  • イメージを表示するために、MainActivity では ImageView を使用します。setImageResource() を使用して、View の画像を別のリソースに変更できます。(例: setImageResource(R.drawable.image_name))

lateinit キーワード

  • lateinit を使用することで、View を参照する際に Null ではないことを Kotlin コンパイラに約束します。また、それらの View は onCreate() 内の setContentView() 後に findViewById() を使用して初期化する必要があります。

tools 設計時属性のネームスペース

  • <ImageView>tools:src を使用して、画像をセットすると、Android Studio のプレビューまたは、デザインエディターのみに画像を表示します。その後、android:src で実際にアプリに組み込む画像をセットします。

  • tools Android レイアウトファイルのネームスペースを使用して、Android Studio でのレイアウトのプレースホルダーコンテンツまたはヒントを作成します。tools 属性によって宣言されたデータは、最終的なアプリでは使用されません。

API レベル

  • Android OS には正式なバージョン番号と名前(Android9.0 Pie)とAPIレベル(API28) があります。アプリの Gradle ファイルの API レベルを使用して、アプリがサポートする Android のバージョンを示します。

  • ファイル内の compileSdkVersion パラメータは、build.gradle がアプリのコンパイルに使用する Android API レベルを指定します。

  • targetSdkVersion パラメーターは。アプリをテストした最新の API レベルを指定します。多くの場合、このパラメーターの値は compileSdkVersion と同じ値になります。

  • minSdkVersion パラメーターは、アプリを実行できる最も古い API レベルを指定します。

Android Jetpack

  • Android Jetpack は、Google が開発したライブラリのコレクションであり、旧バージョンの Android をサポートするための下位互換性のあるクラスと便利な関数を提供します。Jetpack は、以前は Androidサポートライブラリと呼ばれていた一連のライブラリを置き換えて拡張します。

  • androidx パッケージからインポートされたクラスは、Jetpack ライブラリを参照します。build.gradle ファイルの Jetpack への依存関係も androidx で始まります。

Vector Drawable の下位互換性

  • Vector Drawable は、API21以降のバージョンの Android でのみサポートされています。古いバージョンでは、Gradleは、アプリのビルド時にこれらの Drawable の PNG 画像を生成します。

  • build.gradle ファイルの vectorDrawables.useSupportLibrary = true パラメーターを使用して、古い API バージョンの Vector Drawable に Android サポートライブラリを使用するように指定できます。

  • Vector Drawable のサポートライブラリを有効にしたら、<ImageView>android:src 属性は app:srcCpmpat に置き換えます。

app ネームスペース

  • XML レイアウトファイル のネームスペースは、コアコードである Android フレームワークからではなく、カスタムコードまたはライブラリからの属性用です。

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com