次の2つの事項について扱う。
DataBindingにおけるデータの受け渡し Navigationを用いた、異なるFragment間におけるデータの受け渡し さらに具体的に言うと、次の機能を持つアプリを作る
MainFragment
にはEditTextが1つある。EditTextが入力されると、TextViewが"String: [EditTextの文字列]“に変わる。 Buttonが押されると、ReverseFragment
に遷移する ReverseFragment
は、MainFragment
のテキストフィールドの文字列を受け取って、それを逆順にした文字列を表示する。Android Kotlin Fundamentals Course での05辺りを勉強した記録なので、詳しいことはそちらに載っている。
プロジェクト初期設定# “Empty Project"として作成して、名前を"DataTest"とする。
build.gradle(module:App)
について、dataBinding
の設定をしておく。
次のようにMainFragment
とReverseFragment
を作成しておく。作成時、“Create layout XML?“にのみチェックをつけておく。
MainFragmentの設定# fragment_main.xml
を次のようにする。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android= "http://schemas.android.com/apk/res/android"
xmlns:tools= "http://schemas.android.com/tools"
tools:context= ".main.MainFragment" >
<data>
<variable
name= "myMsg"
type= "com.example.datatest.main.MainFragment.MyMsg" />
</data>
<LinearLayout
android:layout_width= "match_parent"
android:layout_height= "wrap_content"
android:orientation= "vertical"
android:layout_gravity= "center_vertical" >
<EditText
android:id= "@+id/edit_text"
android:layout_width= "wrap_content"
android:layout_height= "wrap_content"
android:hint= "@string/hello_blank_fragment"
android:layout_gravity= "center_horizontal"
android:textAlignment= "center" />
<TextView
android:id= "@+id/text_view"
android:layout_width= "wrap_content"
android:layout_height= "wrap_content"
android:text= "@={myMsg.text}"
android:layout_gravity= "center_horizontal"
android:textAlignment= "center" />
<Button
android:id= "@+id/button"
android:layout_width= "wrap_content"
android:layout_height= "wrap_content"
android:layout_gravity= "center_horizontal"
android:text= "@string/to_reverse_fragment"
android:textAllCaps= "false" />
</LinearLayout>
</layout>
重要なのは以下の部分で、これはMainFragment.kt
で定義されたMyMsg
というクラスのインスタンスをこのファイルではmyMsg
として扱う、という意味。
1
2
3
4
5
<data>
<variable
name= "myMsg"
type= "com.example.datatest.main.MainFragment.MyMsg" />
</data>
実際には、@={variableで定義したname.メンバ}
という形でアクセスする。
1
2
3
4
<TextView
...
android:text= "@={myMsg.text}"
... />
さてMyMsg
というクラスを作っていないので、それも兼ねてMainFragment.kt
の編集をする。以下のようにする。
addTextChangedListener
を用いると、EditText
の変化を捕捉できる。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MainFragment : Fragment () {
data class MyMsg ( var text : String = "" )
private lateinit var binding : FragmentMainBinding
override fun onCreateView (
inflater : LayoutInflater , container : ViewGroup ?,
savedInstanceState : Bundle ?
): View ? {
// Inflate the layout for this fragment
binding = DataBindingUtil . inflate ( inflater , R . layout . fragment_main , container , false )
binding . myMsg = MyMsg ( "Hello" )
binding . editText . addTextChangedListener {
binding . myMsg ?. text = "String: " + it . toString ()
binding . invalidateAll ()
}
return binding . root
}
}
Kotlinでは、メンバ変数のみで作られたクラスを次のように簡潔に書ける。C言語におけるstructみたいなもの。
1
data class MyMsg ( var text : String = "" )
main_fragment.xml
で定義したvariable
要素は以下の部分に対応している。name
属性がmyMsg
だったのでbinding.myMsg
でアクセスする。
1
binding . myMsg = MyMsg ( "Hello" )
(寄り道) apply関数の利用# apply
を利用すると、binding.[メンバ]
という書式が単に[メンバ]
だけで記述できるようになる。このような関数を「スコープ関数」と呼ぶ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MainFragment : Fragment () {
data class MyMsg ( var text : String = "" )
private lateinit var binding : FragmentMainBinding
override fun onCreateView (
inflater : LayoutInflater , container : ViewGroup ?,
savedInstanceState : Bundle ?
): View ? {
// Inflate the layout for this fragment
binding = DataBindingUtil . inflate ( inflater , R . layout . fragment_main , container , false )
binding . apply {
myMsg = MyMsg ( "Hello" )
editText . addTextChangedListener {
myMsg ?. text = "String: " + it . toString ()
invalidateAll ()
}
}
return binding . root
}
}
とりあえずのactivity_main.xml# MainFragment
が正しく機能しているかどうか確認したいので、activity_main.xml
を編集する。Navigationと連携させるときに書き直す。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<layout 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"
tools:context= ".MainActivity" >
<LinearLayout
android:layout_width= "match_parent"
android:layout_height= "wrap_content"
android:layout_gravity= "center_vertical"
android:orientation= "vertical" >
<fragment
android:id= "@+id/myNavHostFragment"
android:name= "com.example.datatest.main.MainFragment"
android:layout_width= "match_parent"
android:layout_height= "wrap_content" />
</LinearLayout>
</layout>
この時点でアプリを起動してみると、以下のようになる。EditTextにabcd
を入力すると、その下にString: abcd
と出力される 。
Navigationの作成# とりあえず遷移できるところまで作る。
fragment_reverse.xml
の内容を以下の通りにする。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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= ".reverse.ReverseFragment" >
<TextView
android:id= "@+id/reverse_text_view"
android:layout_width= "match_parent"
android:layout_height= "match_parent"
android:text= "@string/hello_blank_fragment" />
</FrameLayout>
ReverseFragment.kt
については、とりあえず次のようにしておく。
1
2
3
4
5
6
7
8
9
10
class ReverseFragment : Fragment () {
private lateinit var binding : FragmentReverseBinding
override fun onCreateView (
inflater : LayoutInflater , container : ViewGroup ?,
savedInstanceState : Bundle ?
): View ? {
binding = DataBindingUtil . inflate ( inflater , R . layout . fragment_reverse , container , false )
return binding . root
}
}
NavigationのResource Fileを作成する(詳しいことは(2)の記事で話した通り)。次に、res/navigation/navigation.xml
の内容を以下の通りにする。MainFragment
からReverseFragment
へ接続する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<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/navigation.xml"
app:startDestination= "@id/mainFragment" >
<fragment
android:id= "@+id/mainFragment"
android:name= "com.example.datatest.main.MainFragment"
android:label= "fragment_main"
tools:layout= "@layout/fragment_main" >
<action
android:id= "@+id/action_mainFragment_to_reverseFragment"
app:destination= "@id/reverseFragment" />
</fragment>
<fragment
android:id= "@+id/reverseFragment"
android:name= "com.example.datatest.reverse.ReverseFragment"
android:label= "fragment_reverse"
tools:layout= "@layout/fragment_reverse" />
</navigation>
MainFragment.kt
のbinding.apply { ... }
の部分を以下のように修正する。Buttonを押した時に遷移する。
1
2
3
4
5
6
7
8
9
10
binding . apply {
myMsg = MyMsg ( "Hello" )
editText . addTextChangedListener {
myMsg ?. text = "String: " + it . toString ()
invalidateAll ()
}
button . setOnClickListener {
it . findNavController (). navigate ( R . id . action_mainFragment_to_reverseFragment )
}
}
Gradleの設定# Safe Argsの有効化# 異なるFragment間でデータのやり取りをする際、以前はBundleという形式で行っていたらしい。現在は、型安全な状態でデータを受け渡すことができるSafe Argsという仕組みがあるので、そちらを利用する。
build.gradle(Project: ...)
のbuildscript-dependencies
に以下のclasspath
を追加する。
1
2
3
4
5
6
7
8
buildscript {
...
dependencies {
...
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0"
...
}
}
build.gradle(Module: app)
の先頭のapply ...
の行に、さらに追加する。
1
2
...
apply plugin: "androidx.navigation.safeargs"
これを追加すると、例えばnavigation.xml
で指定していたMainFragment
のaction
要素は、MainFragmentDirections
のメンバ変数としてアクセスできるようになる。例えば、action_mainFragment_to_reverseFragment
というidのaction
はMainFragmentDirections.actionMainFragmentToReverseFragment()
としてアクセスできる。これは、Fragment遷移時にデータを渡したい時に必要になる。
lifecycle extentionの有効化# build.gradle(Module: app)
の先頭のdependencies { ... }
の行に、さらに追加する。
1
2
3
4
dependencies {
...
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
}
これはViewModelProviders
を利用するために必要になる。
データを渡す# 受け渡すデータの設定# navigation.xml
の、ReverseFragment
の部分を以下のように編集する。
ここでは、MainFragment
からReverseFragment
へ、値str
を渡すことを考える。このとき、受け取り手にargument
要素を追加する。
1
2
3
4
5
6
7
8
9
10
<fragment
android:id= "@+id/reverseFragment"
android:name= "com.example.datatest.reverse.ReverseFragment"
android:label= "fragment_reverse"
tools:layout= "@layout/fragment_reverse" >
<argument
android:name= "str"
android:defaultValue= "0"
app:argType= "string" />
</fragment>
編集し終えたら、Rebuildを行う 。僕の環境の場合、これをやらないとあとで説明するaction.str
が解決されなかった。
データを渡す処理# MainFragment.kt
のbutton.setOnClickListener
の部分を以下のように編集する。MainFragmentDirections.actionMainFragmentToReverseFragment()
でaction
を取得し、action.str = 値
で値を渡している。
1
2
3
4
5
button . setOnClickListener {
val action = MainFragmentDirections . actionMainFragmentToReverseFragment ()
action . str = editText . text . toString ()
it . findNavController (). navigate ( action )
}
(寄り道)ViewModelの基礎# Android Kotlin Fundamentals Course05-1では、ViewModel
とViewModelFactory
を作り、それを利用してデータを受け取っていた。今回のような小規模なアプリだと、この方法はやや大げさな気がするが、とりあえずこのチュートリアル通りの方法で作ることにする。
そもそもViewModelとは# DataBinding同様、新しい仕組みらしい。この辺りの話はAndroid Architecture Component に記されている。
ViewModel単体の話はViewModelの概要 に載っている。例え画面遷移や回転などでUIが再描画されたとしても、データを保持することができるらしい。ViewModelにはView(ActivityやFragmentのこと)に表示するためのデータを保持しておく。このようにViewとViewModelという役割分担を行うことで、View中に書くコードの肥大化を防げる。
ViewModelを試しに作ってみる# ReverseFragment.kt
の内容を以下のようにする。
TestViewModel
はViewModel
を継承している。TestViewModel
のインスタンスを取得するために、ViewModelProviders
を利用する。ViewModelProviders
を介せず直接TestViewModel
をインスタンス化すれば良いのでは?と思ったが、それだと恐らくアプリがViewModelの存在を検知してくれないのだと思う(つまりViewの再生成のときにデータを保持してくれない)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class TestViewModel : ViewModel () {
var name : String = "Taro"
var age : Int = 123
}
class ReverseFragment : Fragment () {
private lateinit var viewModel : TestViewModel
private lateinit var binding : FragmentReverseBinding
override fun onCreateView (
inflater : LayoutInflater , container : ViewGroup ?,
savedInstanceState : Bundle ?
): View ? {
binding = DataBindingUtil . inflate ( inflater , R . layout . fragment_reverse , container , false )
viewModel = ViewModelProviders . of ( this )
. get ( TestViewModel :: class . java )
binding . reverseTextView . text = viewModel . name
return binding . root
}
}
これでアプリを起動すると、“To ReverseFragment"ボタンを押すと"Taro"と書かれたFragmentに遷移する。
データを受ける# 以上を踏まえて、実際に書いてみる。
データの受け方# ReverseFragmentArgs.fromBundle(arguments!!).str
で、ReverseFragment
で設定したargument
(android:name:str
)にアクセスできる。この値をReverseViewModel
のコンストラクタの初期値として設定しよう。
ReverseViewModelの作成# reverse/ReverseViewModel.kt
を作成して、内容は次のようにする。受け取る予定のstr
をコンストラクタ引数として設定する。reverseStr
は反転後の文字列とする
1
2
3
4
5
6
7
package com.example.datatest.reverse
import androidx.lifecycle.ViewModel
class ReverseViewModel ( private val str : String ) : ViewModel () {
var reverseStr : String = str . reversed ()
}
ViewModelを初期化するときはViewModelFactoryを経由しなければならない。reverse/ReverseViewModelFactory.kt
を作成して、内容は次のようにする。
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.datatest.reverse
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
class ReverseViewModelFactory ( private val str : String ) : ViewModelProvider . Factory {
override fun < T : ViewModel ?> create ( modelClass : Class < T >): T {
if ( modelClass . isAssignableFrom ( ReverseViewModel :: class . java )) {
return ReverseViewModel ( str ) as T
}
throw IllegalArgumentException ( "Unknown ViewModel class" )
}
}
reverse/ReverseFragment.kt
を以下のように編集する。ViewModelProviders.of
の第2引数にViewModelFactory
を指定する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ReverseFragment : Fragment () {
private lateinit var viewModel : ReverseViewModel
private lateinit var viewModelFactory : ReverseViewModelFactory
private lateinit var binding : FragmentReverseBinding
override fun onCreateView (
inflater : LayoutInflater , container : ViewGroup ?,
savedInstanceState : Bundle ?
): View ? {
binding = DataBindingUtil . inflate ( inflater , R . layout . fragment_reverse , container , false )
viewModelFactory = ReverseViewModelFactory ( ReverseFragmentArgs . fromBundle ( arguments !! ). str )
viewModel = ViewModelProviders . of ( this , viewModelFactory )
. get ( ReverseViewModel :: class . java )
binding . reverseTextView . text = viewModel . reverseStr
return binding . root
}
}
アプリを起動する。EditTextに文字列を入力して"To ReverseFragment"ボタンを押すと、遷移後のFragmentではその逆順のものが出力される。
とりあえず形になった。今回はここまで。