UdacityでKotlin入門してみる(途中まで)
背景
プロジェクトでAndoroidアプリで超軽量なサンプルページを作成してみるというお仕事が舞い降りてきた。
アプリ開発は初心者だったので、まずはググってみてチュートリアルをやってみた。。がしかし、、
HellorWorld的なものができたところで何も理解できやしなかったので、やはり公式から学習したほうがよいかな?と思い立ちました
(英語ではあるけど、日本語自動翻訳の字幕で十分理解できそう)
Udacity
Kotlinを使用してアプリケーションを作成する方法を学びたい場合
1. 最初のアプリを作る:ダイスローラー
- 開発環境セットアップ(エミュレーター起動)
- Androidアプリの基本構造
- activity_main.xml: アプリが実際にどのように見えるかを定義するXMLファイル
- MainActivity.kt: アプリの動的部分
- LayoutとActivityはユニークIDによるViewオブジェクトの検索プロセスによって接続される
- R(Resource)で接続
- ex). android:id="@+id/roll_button" -> R.id.roll_button
- ex). android:id="@+id/dice_image" -> R.id.dice_image
- findViewByIdで検索 (layout側で定義したViewGroupの方に合わせて宣言する. lateinit var diceImage: ImageView)
- R(Resource)で接続
- Gragleの概要: プロジェクトと、モジュール毎に配置されている。AndroidVerの互換性とライブラリの依存関係を定義する
- Android Studioの各パネルの説明/操作方法
レイアウトについて: About Meアプリ
-
Androidでの視覚要素は全てViewの子クラス
-
dp(Density Independent Pixel): 画面の物理的なピクセル密度に基づく単位。画面サイズによって変動。
- On 160dpi(ドットインチ) screeen: 1dp == 1pixel
- On a 480dpi: 1dp == 3pixels
-
レイアウトを構成するViewは、Viewの階層として編成される
- 他のViewを含めることが主な役割のViewは、ViewGroupと呼ばれる
- 通常、レイアウトには最上位のViewGroupと、その中に任意の数のViewとViewGroupがある
-
LinearLayout: Viewを水平または垂直に配置できるViewGroup
-
View階層が深くなりすぎると、アプリの挙動が重くなってしまうので注意
- Androidではフラットビュー階層を促進している
- 最適化された制約レイアウトを提供している(複雑なレイアウトで少数のView/ViewGroupを配置するのに適している)
-
Android Studioのレイアウトエディタの基本
-
属性パネル: 設定値を用いる際には"New Dimension Value Resource"で定義するように
- res/values/dimens.xmlに定義される
-
コンポーネント・ツリー: リファクタリング>スタイルの抽出
- res/values>styles.xmlに定義される
-
ほとんどのユーザインタフェースには、いくつかの画像リソースが含まれている
- ImageView選択時に選べる
-
ScrollView: 子として追加されたViewを1つだけ含めることができる(通常LinearLayout)
-
Palette>text: 下線が引いてるアイコンは、編集可能なTextボックスという意味
-
clickHandler: findViewByIdで呼び出したViewオブジェクトでsetOnClickListenerを実行
- this -> it
- この方法で検索する度にAndroidはView階層をトラバースするため、使い過ぎに注意
-
データバインディング: ↑を解消するためにデータバインディングの手法がとられる
- 構造: layout<->binding<-> Activity
- moduleのbuild.gradleにdataBinding(enabled=true)を設定する必要がある
- activity_main.xml: <layout>で囲いxmlの名前空間を移動定義させる必要がある
- MainActivity.kt: setContentViewをDataBindingUtilのそれに置き換える必要がある
- そうすると、findしなくともbinding変数から呼び出し可能となる
- Data Bindingの場合、UIを新しいデータで更新するにはinvalidateAll()を呼び出す必要がある
- binding.apply {} で囲うと、変数指定を省略できる
-
データクラス: 通常、Resource情報はstring.xmlから直接layoutが呼ぶが、それとは別にデータクラスを介することができる
- layoutファイルの<layout>内の上部に<data>ブロックを準備する(内に<variable>ブロックも用意する)
-
データバインディング-アイデア
- データバインディングの大きなアイデアは、コンパイル時に2つの離れた情報を接続/マッピング/バインドするオブジェクトを作成することです。これにより、実行時にそれを探す必要がなくなります。
- これらのバインディングを表示するオブジェクトは、Bindingオブジェクトと呼ばれます。これはコンパイラによって作成され、内部でどのように機能するかを理解することは興味深いですが、データバインディングの基本的な使用法を知る必要はありません。
-
データバインディングとfindViewById
- findViewByIdは、呼び出されるたびにビュー階層をトラバースするため、コストのかかる操作です。
- データバインディングを有効にすると、コンパイラは<layout>IDを持つ内のすべてのビューへの参照を作成し、それらをBindingオブジェクトに収集します。
- コードでは、バインディングオブジェクトのインスタンスを作成し、追加のオーバーヘッドなしでバインディングオブジェクトを介してビューを参照します。
-
データバインディングビューとデータ
- データを更新してからビューに表示されるデータを更新するのは面倒であり、エラーの原因になります。データをビューに保持することも、データと表示の分離に違反します。
- データバインディングは、これらの問題の両方を解決します。データはデータクラスに保持します。に<data>ブロックを追加<layout>して、ビューで使用する変数としてデータを識別します。ビューは変数を参照します。
- コンパイラは、ビューとデータをバインドするバインディングオブジェクトを生成します。
- コードでは、バインディングオブジェクトを介してデータを参照および更新します。これにより、データが更新され、ビューに表示される内容が更新されます。
- ビューをデータにバインドすると、データバインディングを使用したより高度な手法の基盤が確立されます。
レイアウトについて: ColorMyViewsアプリ
- Ratio: 縦横の固定比率設定が可能
- チェーン: Viewがリンク(h/v方向)されているViewをチェーンとみなす
- Spread Chain(デフォルト): 要素を均等にする
- Spread Inside Chain: 利用可能なスペースはすべて使用される(頭と尾が親要素に張り付く)
- Weighted Chain: 重み属性で設定された値に基づいてスペースを使い果たす
- Packed Chain: 最小限のスペースを使用する
- Packed Chain with Bias: Biasを追加して、軸上の要素のグループを移動することができる
- 微調整はViewのマージンでおこなうよう
- ベースライン: フォントサイズが異なるTextViewの水平方向位置を合わせる際に便利
- layoutにて、選択した要素のコンテキストメニューに「BaseLine」がある
- どうやら水平方向の位置をあわせることができるみたい
- 垂直方向には制約がないので別途セットする必要がある
- ベースライン制約のある要素は、別の下部制約を開始できないので注意
いまの所、プロジェクトのサブ案件が求めているのは単純なWebView画面呼び出しなので、一旦ここまでのレッスンでの知識で問題なく対応できそう。
次は↓のページを参照してまずは案件終わらせておこう。
(もしかすると、、Lesson8は必要??)
以下のレッスンはまた追々対応することにする。(できればFlutter開発もやりたいし)
- Lesson3. アプリのナビゲーション: 雑学クイズアプリ
- Lesson4. アクティビティとフラグメントのライフサイクル: デザートプッシャーアプリ
- Lesson5. アプリアーキテクチャ(UIレイヤー): 推測ゲームアプリ
- Lesson6. アプリアーキテクチャ(永続性): Room
- Lesson7. RecyclerView: アプリ改善
- Lesson8. インターネットに接続する: RetrofitでAPIサービスと通信+Glide利用でWebから画像表示
- Lesson9. 舞台裏: 独自のバックグラウンドサービスとタスクの構築
- Lesson10.みんなのためのデザイン: アプリ設計-作成
Lesson3. アプリのナビゲーション: 雑学クイズアプリ
- 画面上部のアクションバーの戻るボタンは、デバイスディスプレイの下部にあるシステムボタンと同機能
- アプリ内各画面や宛先はフラグメントと呼ばれるUIコンポーネントを使用して構築される
- フラグメントは、そのアクティビティのユーザインターフェースの一部と考えることができる
- アクティビティには、1つ以上のフラグメントを含めることができる
- フラグメントを他のフラグメントと交換することができる(アクティビティで複数の画面を作成する方法)
- フラグメントを作成してから、ナビゲーションを作成する
- Action Bar: アプリケーション画面の上部に表示されます。オーバーフローメニューやアプリケーションドロワーボタンなどのアプリケーションブランディングおよびナビゲーション機能が含まれています。
- App Button: アクションバーに表示され、ユーザーがアプリ内で移動した前の画面に戻ります。
- Overflow Menu: ナビゲーションの宛先を含めることができるアクションバー内のアイテムのドロップダウンリスト。
- Navigation Drawer: アプリの側面からスライドするヘッダー付きのメニュー。
- Navigation Graph(Flagment?): すべての宛先(単一のアクティビティからナビゲートできる画面)がこれに含まれています
- フラグメント: APIレベル11から導入
- アクティビティは、UIフラグメントを含むフレームとして動作し、フラグメントを囲むUI要素を提供できる
- UIフラグメントは通常、アクティビティのレイアウト内のビューのように動作しますが、アクティビティのように、それを使用するにはフラグメントのサブクラスを作成する必要がある
- フラグメントを使用すると、ほとんどのUIがフラグメントに実装されるため、アクティビティをアプリへのOSへのエントリポイントとして扱うことができる
- OSはアクティビティのみ開くことができる
- アクティビティ内で、onCreateでsetContentViewを呼び出して、使用するレイアウトをAndroidに指示します
- フラグメントを使用すると、onCreateとは独立したonCreateViewメソッド内で、手動で膨らませて膨らませたレイアウトを返す
- アクティビティはコンテキストクラスを継承するが、フラグメントは継承しない
- 通常コンテキストに関連付けられている文字列や画像リソースなどのAppDataにアクセスするには、フラグメント内のコンテキストプロパティを使用する必要がある
- context!!.getString(R.string.app_name)
- context!!.getDrawable(R.drawable.ic_launcher_background)
- 通常コンテキストに関連付けられている文字列や画像リソースなどのAppDataにアクセスするには、フラグメント内のコンテキストプロパティを使用する必要がある
- アクティビティは、UIフラグメントを含むフレームとして動作し、フラグメントを囲むUI要素を提供できる
- ナビゲーション: 異なるアクティビティ間およびアクティビティ内のフラグメント間を移動できる
- Androidでアクティビティをナビゲートすると、アプリ内およびアプリからの以前のアクティビティが、バックスタックと呼ばれるスタックに配置される
- バックスタック:
- バックスタックは、各アクティビティが開かれた順序に基づいて順序付けられる
- Next画面でシステムのバックキーを押すと、スタックからポップされて、元画面(タイトル)のアクティビティに戻る。もう一度押すと、アプリがリモート終了しますしてアクティビティが終了し、前のアプリ(おそらくランチャー)に戻る。
- バックスタックは、各アクティビティが開かれた順序に基づいて順序付けられる
- フラグメントバックスタック:
- フラグメントマネージャクラスによって制御される
- フラグメントがフラグメントマネージャによってインスタンス化されると、オプションでフラグメントトランザクションをフラグメントバックスタックに追加できる
- フラグメントバックスタックにトランザクションがあると、システムのバックキーはフラグメントバックスタックからポップされ、なくなるとバックキーはアクティビティのバックスタックに渡される
フラグメントの作成
- ファイル > 新規 > Fragment > Fragment(blank) で作成
- 以下がシンプルなフラグメントのコード
/**
* A simple [Fragment] subclass.
*/
class TitleFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentTitleBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_title, container, false
)
return binding.root
}
}
- アクティビティのメインレイアウトに直接追加可能
- id/nameは指定する必要あり
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<fragment
android:id="@+id/titleFragment"
android:name="com.example.android.navigation.TitleFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
ナビゲーションの原則
- アプリの開始先が固定されている
- ログインが必要なアプリには、このルールに対する1回限りの例外がある
- この開始先は、ユーザが戻るボタンを押した後にランチャーに戻ったときに表示される最後の画面でもある必要がある
- アプリのナビゲーション状態を後入れ先出し構造で表す必要がある -> Back Stackのこと
- スタックの1番下に開始先(Destination)があり、スタックの1番上に現在の宛先(Destination)がある
- ナビゲーションスタックを変更する操作は、New Destination を最上位にpushするか、最上位Destinationをスタックからpopするかで、常にナビゲーションスタックの最上位で動作する必要がある
- ユーザが前のDistinationに戻る方法が含まれていること
- アクションバーのAppボタンと、システムの戻るボタン(バックキー)は、どちらもアプリのタスク内を移動するときに同じように機能する
- システムの戻るボタンは開始先で押下すると、アプリから他のアプリ(通常はランチャー)に移動する
- アクションバーのAppボタンは開始先にいるときには表示させないようにすること
アプリのナビゲーション
-
プロジェクトへのナビゲーションコンポーネントの追加
- プロジェクトのbuild.gradleとモジュール(アプリ)のbuild.gradleにて夫々追加
-
プロジェクトへのナビゲーショングラフの追加
- プロジェクトウィンドウで、resディレクトリを右クリックし、[新規]> [Androidリソースファイル]
- リソースタイプとして[ナビゲーション]を選択し、ナビゲーションのファイル名を付けます。修飾子がないことを確認してください。resの下の新しいナビゲーションディレクトリでnavigation.xmlファイルを選択し、[デザイン]タブが選択されていることを確認します。
-
activity_main.xmlのandroid:nameをandroidx.navigation.fragment.NavHostFragmentに切り替える
- あと、navGraphに追加した@navigation/navigationを指定する
- 最後に、defaultNavHostをtrueに指定: このナビゲーションホストがシステムの戻るボタンをインターセプトする。(複数のナビゲーションホスト同時に(activityが)持つことができるので、明示的に指定する必要がある)
- NavHostFragment: 各アクティビティナビゲーショングラフの各フラグメントのホストとして機能する
-
アクティビティレイアウトでタイトルフラグメントをナビゲーションホストフラグメントに置き換えます
-
タイトルとゲームフラグメントをナビゲーショングラフに追加する
-
タイトルとゲームフラグメントをアクションで接続する
-
再生ボタンが押されたときのナビゲート
** ※環境差異でエラーが発生するため、公式の環境準備も参照したほうが良い **
条件付きナビゲーション
- Kotoinコードで条件分岐させるだけ。結果 navigation.xmlで設定したリソースidへnavigationできればよい
バックスタック操作
- gameWon画面またはgameOver画面に遷移したあとに戻る場合は、gameFragmentではなくタイトルに戻りたい
- そんな時には、gameWon/Overをバックスタックにpushする前にgameFragmentを削除する方法をとる
- navigation.xmlでアクションを選択し、Pop Behavior(ポップ動作属性)を使用する
- Pop To: none -> gameFragmentに変更
- gameFragmentを返すものが見つかるまで、フラグメントバックスタック上にFragmentTransactionsをポップする
- inclusive: チェック
- gameFragmentTransactionもポップする
- チェックしない場合、gameFragmentTransactionがgameFragmentからgameWonFragmentへのアクションに対して同じことを実行できるようになる
- (おそらく、)gameOverアクションでチェックしないとgameWonアクションで定義できない。。?
- gameFragmentTransactionもポップする
- Pop To: none -> gameFragmentに変更
- 上記の設定で、結果画面の戻るを押下するとgameFragmentをスキップしてTitleFragmentに移動するようになる
- Overの場合、TitleでなくgameFragmentに戻りたい場合
- navigation.xmlでOver->gameFragmentへ接続するアクションを作成
- その際、gameWonFragmentTransactionsでGameOverをポップオフする必要がある
- アクション毎に非包括形式のpopを使用してみる
- Over->gameFragmentのアクションのPop ToをtitleFragmentに設定+非包括
- Wonにも同様に適用
- KotlinコードにボタンonClick時に同アクションを設定
- navigation.xmlでOver->gameFragmentへ接続するアクションを作成
- Pop Behavior
- PopToInclusive: 参照されているFragmentTransactionを含む、バックスタック上のすべてのものをポップオフする
- PopTo Not-Inclusive: 参照されているFragmentTransactionがみつかるまで、バックスタック上のすべてのものをポップオフする
- アクションバーにナビゲーションアイコンを設ける
- MainActivity.ktにて実装
val navController = this.findNavController(R.id.myNavHostFragment)
NavigationUI.setupActionBarWithNavController(this, navController)
-
アクティビティがオーバーライドできるfunctionを検索する(Ctrl + O)
- onSupportNavigateUp() は、Up Buttonを押したときに何がおきるかをナビゲーションする処理を実装する
override fun onSupportNavigateUp(): Boolean {
val navController = this.findNavController(R.id.myNavHostFragment)
return navController.navigateUp()
}
安全な引数の追加(型安全のため)
- Android(Fragment間)ではKeyValueStoreでやりとりする
- Bandleを生成し、型に合わせてsetする。渡した側で型に合わせてgetする
- ただ、↑の方法だと型安全ではないので SafeArgs を利用する。(型を誤るとnullや0といったデフォ値になる)
-
SafeArgs : 両側の引数が一致することを保証するのに役立つコードを生成するGradleプラグイン
-
- ナビゲーションのSafeArgsGradleプラグインの依存関係をプロジェクトのGradleファイル(dependencies)に追加する必要がある
-
- モジュールのGradleファイルに、AndroidXナビゲーションのsafe argsプラグインを使用してapplyプラグインステートメントを追加する
- ↑の2つの準備で、safe引数の仕様を開始できる
- 以下のようにナビゲーションを置き換え
-
// view.findNavController().navigate(R.id.action_gameFragment_to_gameWonFragment)
view.findNavController().navigate(GameFragmentDirections.actionGameFragmentToGameWonFragment())
-
navigation.xmlのArgumentsで設定
-
デフォルトなしで引数を追加したので、gameFragmentはコンパイルされなくなったので注意
- ナビゲーションのコンストラクターに引数のインデックスパラメータが必要になる
- 型安全!
-
使い方
- 遷移元
view.findNavController().navigate(GameFragmentDirections.actionGameFragmentToGameWonFragment(numQuestions, questionIndex))
- navigation.xmlに引数追加(navigateのDeirections->action内の引数にセットするもの)
- 遷移先
val args = GameWonFragmentArgs.fromBundle(arguments!!)
Intents(意図)とシェアリング
- Intestsとはアプリの意図(大きく2種類)
- Explicit(明示的意図): ターゲットアクティビティクラスの
- Implicit(暗黙的意図):
Intentsでシェアを追加する
※いったん中止※
公式の概要の最後に「Kotlin での Android の基礎コースをご検討ください。」とあるので覗いてみたら
結構丁寧にチュートリアルと解説がなされてそうだった。(Udacityはなんだったんだ..)
短い動画と日本語訳の解説付きで、楽しそうなアプリのハンズオンもついているので、こちらの方がよいのかも。。
なので、いったんUdacityのレッスンは中止する。