〈kotlin〉Navigation④ ナビゲーションドロワー

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

アプリバー内のドロワーアイコンをタップしたときや画面の左端からスワイプしたときに表示される、
アプリのナビゲーションメニューを表示する UI パネルであるナビゲーションドロワーを実装する。

レイアウトファイル

activity_main.xmlに、DrawerLayout をルートビューとして宣言する。
その中にメイン UI コンテンツのレイアウト(NavHostFragment )と、
ナビゲーション ドロワーのコンテンツを格納するビュー( NavigationView)を追加する。

<androidx.drawerlayout.widget.DrawerLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <androidx.fragment.app.FragmentContainerView
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:id="@+id/nav_host_fragment"
        ...
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        ...
        //どの方向からスワイプすると表示されるか
        android:layout_gravity="start"
        android:fitsSystemWindows="true" />

</androidx.drawerlayout.widget.DrawerLayout>

ドロワーに表示するメニューの作成

resフォルダから「Android Resource File」を開き、
resource typeにmenuを選択して新しいxmlを作成する。

//navdrawer_menu.xml
<menu 
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/first_Fragment"
        android:title="Fragment1" />
    <item
        android:id="@+id/second_Fragment"
        android:title="Fragment2" />
</menu>

itemタグでメニューに表示する項目を追加する。
このときitemのidを、ナビゲーショングラフで指定したフラグメントのidと一致させることで、
メニューで項目を選択したとき同じidのついたフラグメントに遷移することができる。

<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/nav_graph"
    ... >

    <fragment android:id="@+id/first_fragment"
                      //このidと同じにする
         ... />

    <fragment android:id="@+id/second_fragment"
         ... />
</navigation>

作成したらNavigationViewにメニューを指定する。

<com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        ...
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        app:menu="@menu/navdrawer_menu" />

MainActivityの実装

DrawerLayout を AppBarConfiguration に渡してアプリバーと接続する。
アプリバーにドロワーボタンが表示されるようになる。
NavigationViewにはNavControlleを設定する。

lass MainActivity : AppCompatActivity() {
    private lateinit var appBarConfiguration: AppBarConfiguration

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment_container) as NavHostFragment
        val navController = navHostFragment.navController
        val drawerLayout = findViewById<DrawerLayout>(R.id.drawer_layout)

        appBarConfiguration = AppBarConfiguration(navController.graph, drawerLayout)

        findViewById<NavigationView>(R.id.nav_view).setupWithNavController(navController)
        setupActionBarWithNavController(navController, appBarConfiguration)

    }

    override fun onSupportNavigateUp(): Boolean {
        val navController = findNavController(R.id.nav_host_fragment_container)
        return navController.navigateUp(appBarConfiguration)|| super.onSupportNavigateUp()
    }
}

参考https://developer.android.com/guide/navigation/navigation-ui?hl=ja#add_a_navigation_drawer

〈kotlin〉Navigation③ アプリバーと一緒に使う

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

NavigationコンポーネントのNavigation UIクラスを使用することで、
アプリのトップバーと連動させることができる。

AppBarConfiguration

NavigationUIのAppBarConfiguration オブジェクトを使用して、
アプリの表示領域の左上隅にあるアプリバーのナビゲーション ボタンの動作を管理する。
トップレベル(ナビゲーショングラフでホームに設定した)デスティネーションにいるときは
トップ アプリバーに「上へ」ボタン(⇦)は表示されず、
それ以外のデスティネーションにいる時は「上へ」ボタンが表示され、前の画面に戻ることができる。

対象のナビゲーション グラフを渡して AppBarConfiguration オブジェクトを作成する。

val appBarConfiguration = AppBarConfiguration(navController.graph)

デフォルトのアクションバーと一緒に使う場合

onCreate() メソッドから setupActionBarWithNavController() を呼び出したあと、
「上へ」ナビゲーションを処理する onSupportNavigateUp() をオーバーライドする。

private lateinit var appBarConfiguration: AppBarConfiguration
//onCreate()とonSupportNavigateUp() で使用するため最初に定義

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    val navHostFragment =
        supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
    val navController = navHostFragment.navController
    appBarConfiguration = AppBarConfiguration(navController.graph)
    setupActionBarWithNavController(navController, appBarConfiguration)
}

override fun onSupportNavigateUp(): Boolean {
    val navController = findNavController(R.id.nav_host_fragment)
    return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}

ツールバーを作成して使う場合

ツールバーを定義する。

<LinearLayout>
    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar" />
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        ... />
    ...
</LinearLayout>

デフォルトのアクションバーが設定されているため、アクションバーをなくす必要がある。
AndroidManifest.xml 内のandroid:theme="@style/AppTheme" がアクションバーを設定しているため、
res>values>styles.xml(themes.xmlかも)でparentの設定をNoActionBarに変更する。

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        ...
    </style>
</resources>

その後onCreate() メソッドから setupWithNavController() を呼び出す。
ツールバーを使用するときはクリック イベントを Navigation コンポーネントが自動的に処理するため、
onSupportNavigateUp() をオーバーライドする必要がない。

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    val navController = findNavController(R.id.nav_host_fragment)
    val appBarConfiguration = AppBarConfiguration(navController.graph)
    findViewById<Toolbar>(R.id.toolbar)
        .setupWithNavController(navController, appBarConfiguration)
}


参考https://developer.android.com/guide/navigation/navigation-ui?hl=ja

〈kotlin〉Navigation② SafeArgs

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

画面遷移の設定に使用するNavigationコンポーネントのSafe Argsについて。
デスティネーション間を移動する際は、Safe Args Gradle プラグインの使用が推奨されている。
Safe Argsによってデスティネーション間でタイプセーフなナビゲーションと引数の受け渡しができる。

準備
//プロジェクトレベルのgradle
buildscript {
    dependencies {
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.4.1"
    }
}

//アプリレベルのgradle
plugins {
    id "androidx.navigation.safeargs.kotlin"
}

Safe Argsを使用した画面遷移の設定

ナビゲーショングラフ

例としてfirstFragmentからsecondFragmentへ遷移するナビゲーショングラフを以下のように定義する。

<navigation 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/nav_graph"
    app:startDestination="@id/firstFragment">

    <fragment
        android:id="@+id/firstFragment"
        android:name="com.example.sampleApp.firstFragment"
        android:label="fragment_first"
        tools:layout="@layout/fragment_first" >
        <action
            android:id="@+id/action_firstFragment_to_secondFragment"
            app:destination="@id/secondFragment" />
    </fragment>
    <fragment
        android:id="@+id/secondFragment"
        ...
    </fragment>
</navigation>
Safe Argsによって作成されるクラスとメソッド

Safe Argsが有効になっていると、
アクションの発生元となる送信側デスティネーションごとにクラスとメソッドを自動生成する。
(生成されていなければRebuildする。)
上記のナビゲーショングラフ(firstFragment→secondFragment)では
送信側のfragment名を使用してFirstFragmentDirectionsクラスが生成され、
その中にactionFirstFragmentToSecondFragment()メソッドを持つ。
このメソッドはNavDirections オブジェクトを返し、
NavDirections オブジェクトはnavigate()メソッドに直接渡すことができる。

画面遷移

actionにNavDirectionsオブジェクトを取得し、navigate()メソッドに渡すことで画面遷移を設定できる。

binding.button.setOnClickListener{
    val action = FirstFragmentDirections.actionFirstFragmentToSecondFragment()
    this.findNavController().navigate(action)
}

Safe Argsを使用した引数の受け渡し

ナビゲーショングラフ

例としてfirstFragmentからsecondFragmentに遷移する際、
String型の変数dataを受け渡すことにする。
受け渡す引数は受け取り側のFragmentのargumentタグに定義する。

<navigation 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/nav_graph"
    app:startDestination="@id/firstFragment">

    <fragment
        android:id="@+id/firstFragment"
        android:name="com.example.sampleApp.firstFragment"
        android:label="fragment_first"
        tools:layout="@layout/fragment_first" >
        <action
            android:id="@+id/action_firstFragment_to_secondFragment"
            app:destination="@id/secondFragment" />
    </fragment>
    <fragment
        android:id="@+id/secondFragment"
        ...
        <argument
            android:name="data"
            app:argType="string"/>
    </fragment>
</navigation>

SecondFragmentにargumentを設定すると、
SecondFragmentArgsというクラスが生成される。

送信側

FirstFragment側のactionFirstFragmentToSecondFragment()の引数に渡すデータを入れる。

val sampleData = "sample"
binding.button.setOnClickListener{
    val action = FirstFragmentDirections.actionFirstFragmentToSecondFragment(sampleData)
    this.findNavController().navigate(action)
}
受信側

SecondFragmentでは、by navArgs()プロパティデリゲートを使用して引数にアクセスする。

val args: SecondFragmentArgs by navArgs()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    val tv: TextView = view.findViewById(R.id.textView)
    val sampleData = args.data
    //ナビゲーショングラフに記述したargument nameを使用してアクセス
    //args.dataにFirstFragmentで渡したsampleDataが格納されている
    tv.text = sampleData.toString()
}

参考
https://developer.android.com/guide/navigation/navigation-getting-started?hl=ja#ensure_type-safety_by_using_safe_args

https://developer.android.com/guide/navigation/navigation-pass-data?hl=ja#Safe-args

〈kotlin〉Navigation①

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

画面遷移の設定に使用するNavigationコンポーネントの使い方をまとめてみる。

Navigationコンポーネントとは

アプリ内のさまざまなコンテンツ間を移動する操作を実装する。
以下の3つの要素で構成されている。

ナビゲーション グラフ すべてのナビゲーション関連情報を 1 つの場所で集中的に保管する XML リソース
NavHost ナビゲーション グラフからの宛先を表示する空のコンテナ
NavController NavHost 内のアプリ ナビゲーションを管理するオブジェクト

アプリ内を移動するとき、ナビゲーション グラフ内の特定のパスをたどって移動するか、
それとも特定の宛先に直接移動するかを NavController に伝える。
NavController はそれに応じて NavHost に適切な宛先を示す。

実装

build.gradle
implementation "androidx.navigation:navigation-fragment-ktx:2.4.1"
implementation "androidx.navigation:navigation-ui-ktx:2.4.1"
各Fragmentの準備

遷移したい画面ごとにFragmentとレイアウトファイルを作成する。

ナビゲーショングラフの作成
  1. resディレクトリで右クリックし、「New Resource File」を開く。
  2. FileNameを「nav_graph」、Resource typeを「Navigation」としてOKを押す。
  3. 「New Destination」から遷移を設定したいFragmentを追加する。(最初に追加したFragmentがトップのFragmentに設定される🏠)
  4. 「Add Action」から移動方向を追加する。

Fragmentを追加するとコードにid,name,label,layoutは自動で追加される。
actionで移動方向を設定すると、出発地のFragmentにactionタグが作成され、
actionに対するidと移動先のFragment名が設定される。

<navigation 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/nav_graph"
    app:startDestination="@id/firstFragment">

    <fragment
        android:id="@+id/firstFragment"
        android:name="com.example.sampleApp.firstFragment"
        android:label="fragment_first"
        tools:layout="@layout/fragment_first" >
        <action
            android:id="@+id/action_firstFragment_to_secondFragment"
            app:destination="@id/secondFragment" />
    </fragment>
    <fragment
        android:id="@+id/secondFragment"
        ...
        <action
            android:id="@+id/action_secondFragment_to_thirdFragment"
            app:destination="@id/thirdFragment"
    </fragment>
    <fragment
        android:id="@+id/thirdFragment"
        ...>
        <action
            android:id="@+id/action_thirdFragment_to_firstFragment"
            app:destination="@id/firstFragment"
            
            //☆
            //以下の設定については後述
            app:popUpTo="@id/firstFragment"
            app:popUpToInclusive="true"/>
    </fragment>
</navigation>

上記のnav_graphはfirst→second→thirdと遷移したあと、
third→firstに戻るように作成した。
このとき画面はfirst,second,thirdの順にスタックに積まれていて、
コード内の☆の部分を書かずにfisrtへのactionを追加すると
スタックはfirst,second,third,firstと積まれることになる。
(2回目のfirstの画面で戻るボタンを押すとthirdに戻る。)

☆の設定は、
popUpTo:スタックの一番上に残すFragmentを指定。それ以外のバックスタックは破棄。
popUpToInclusive:popUpToで指定したFragmentもバックスタックから破棄するかどうか。

この設定を追加すると、
third→firstの遷移時にバックスタックが破棄されて、
スタックにはfirstだけが残った状態になる。
(2回目のfirstの画面で戻るボタンを押してもthirdに戻らない。)

popUpToInclusiveをFalseにすると、
popUpToで指定したFragment(今回はfirst)はバックスタックから削除されなくなるため、
first→second→third→firstと遷移したときのスタックには
firstが2つ積まれた状態になる(多分)。

NavHostの追加

activity_main.xmlにNavHostFragmentを追加する。

<androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />

defaultNavHostをtrueにすると、
システムの戻るボタンにも画面遷移が対応するようになる。
navGraphに作成したナビゲーショングラフ名を指定する。

NavController

firstFragmentにボタンクリックでsecondFragmentへ移動を実装する。

binding.button.setOnClickListener{
    findNavController().navigate(R.id.action_firstFragment_to_secondFragment)
    //idはnav_graph内で設定したaction id
}

action idを直接指定して実装もできるが、
Safe Argsを使用して実装することが推奨されている。
Safe Argsを使用した実装はまた次回。

参考https://developer.android.com/guide/navigation/navigation-getting-started?hl=ja

〈kotlin〉MVVM③ LiveData

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

LiveData

ViewModel内でのデータの保存にLiveDataを使うことで
ライフサイクルに応じたデータの監視が可能になる。
(値が変更されたらその変更が検知されてUI上のデータも自動で変わる。)

実装

build.gradle
dependencies{
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
}
LiveDataオブジェクトの作成

ViewModelにLiveDataオブジェクトを作成する。

class ViewModelSample : ViewModel(){
    //バッキングプロパティを使う
    //内部で使用する変更可能なMutableLiveData
    private val _sampleData : MutableLiveData<String> = MutableLiveData<String>()

    //外部から参照するLiveData
    val sampleData : LiveData<String> = _sampleData

    //MutableLieDataを使って値を変更する処理を作成
    fun setSampleData(){
        _sampleData.value = "Sample"
        //LiveDataオブジェクトのデータにアクセスする時はvalueを使用
    }
}
LiveDataオブジェクトの監視

LiveDataが変更された時の処理を管理するObserverオブジェクトを作成する。
これをobserve() メソッドを使用して LiveData オブジェクトにアタッチすることで、
Observer オブジェクトが LiveDataに登録され、変更が通知されるようになる。

class MainActivity : AppCompatActivity() {
     private val model: ViewModelSample by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ....

        val observer = Observer<String> { data ->
            dataTextView.text = data
            //データ変更時の処理
        }

        model.sampleData.observe(this, observer)
        //fragmentで使用するときはthisをviewLifecycleOwnerにする
    }
}
Databindingと一緒に使う

observe() メソッドは LifecycleOwnerと変更時の処理をLiveDataに設定しているが、
DataBindingと一緒に使う時はLifecycleOwnerを最初に設定し、変更時の処理はxmlファイルに直接記載する。
(上記のobserve()メソッドの部分が不要になる)

class MainActivity : AppCompatActivity() {
     private val model: ViewModelSample by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this
        binding.viewModel = model
    }
}


参考https://developer.android.com/topic/libraries/architecture/livedata

〈kotlin〉MVVM② ViewModel

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

ViewModel

ViewModelはUIで表示するデータを保存して管理しておくためのクラス。
アクティビティで最初に呼び出されてから終了するまで存在し、
その間データがそのまま保持されている。
UIからはViewModelに保存されたデータにアクセスしてデータを使用する。

実装

build.gradle
dependencies{
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
}
ViewModelを継承したクラスの作成

ViewModel内の変数は外部からの書き換えができないようにする。

class SampleViewModel : ViewModel(){
    private var _sampleData : String= "Sample"
    val sampleData : String = _sampleData

    fun changeText() {
        _sampleData = "Sample Change"
    }

privateをつけた_sampleDataはクラス内でデータを書き変える時に使い、
このデータを書き換えるためのメソッドも用意する(今回はchangeText())。
_sampleDataをpublicな変数sampleData(valで定義)に保存し直して、
外部からはこのsampleDataだけにアクセスするように設計する。
これで外部で直接書き換えができないようになる。


上記に合わせてLiveDataを使用する。
また次回。

参考https://developer.android.com/topic/libraries/architecture/viewmodel?hl=JA#sharing

〈kotlin〉MVVM① DataBinding

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

MVVMとは

「Model」「View」「ViewModel」の3層に分けて実装するアーキテクチャらしい。
調べると解説記事がたくさん出てきた。簡単にするとこんな感じ?

Model データリソースを管理する部分
View UIに関する部分(Activity, Fragment, xmlファイル)
ViewModel ModelとViewをつなぐ部分
Modelからデータを取得してViewに表示するデータを作る

DataBinding

ViewModelからViewにデータの受け渡す方法としてDataBindingがある。

Activityで定義する場合、 findViewById()でxmlに指定したフレームワークを呼び出して値を代入する。

findViewById<TextView>(R.id.sample_text).apply {
    text = viewModel.userName
}

DataBindingを使用するとレイアウトファイル内で直接割り当てができる。

<TextView
    android:text="@{viewmodel.userName}" />

実装

build.gradle

DataBindingを使用できるよう記述を追加。

plugins{
    ...
    id 'kotlin-kapt'
}

android {
    ...
    dataBinding {
        enabled = true
    }
}
XMLファイル

Layoutタグをルートタグとして記述する(はじめのConstraintLayoutのままではだめ)。
そのあとにdataタグとViewの要素のタグを書いていく。
dataタグの中にバインドするデータの記述をする。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
        //nameにxml内で使用する変数名を指定する
        //typeに型を指定する、自分で作ったクラスも入れられる
   </data>
   <LinearLayout
       ...
        <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
           //@{}とdataタグで指定した変数名を使って値を埋め込む
   </LinearLayout>
</layout>

typeに指定したUserのオブジェクト

data class User(val firstName: String, val lastName: String)
Activity内

レイアウトファイルごとに作成されるバインディングクラスによって
レイアウトへのバインディングが保持される。
(activity_main.xmlに対してActivityMainBindingというバインディングクラスができる)

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)

    binding.user = User("Test", "User")
}

Fragment、ListView、RecyclerViewで使用している場合は
inflate()メソッドを使用してバインディングを取得する。

val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
    // or
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)


とりあえずここまで。
参考https://developer.android.com/topic/libraries/data-binding?hl=ja