2019年9月5日木曜日

Kotlin メモ : count

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/count.html


大文字数を数えるダメな例 val String.upperCaseCount: Int get() = filter { it.isUpperCase() }.length

大文字数を数える良い例 val String.upperCaseCount: Int get() = count { it.isUpperCase() }

2019年8月6日火曜日

AlertDialog の Negative ボタンの文字色を変える

AndroidX の AlertDialog で Negative ボタンと Positive ボタンを出すと、両方文字が colorAccent の色になります。 import androidx.appcompat.app.AlertDialog class SampleActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_sample) button.setOnClickListener { AlertDialog.Builder(this) .setMessage("メッセージ") .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(android.R.string.ok, null) .show() } } }

Negative ボタンの文字色を変えるには Widget.AppCompat.Button.ButtonBar.AlertDialog を継承した style を用意し、android:textColor を指定します。 <style name="Widget.Sample.Button.ButtonBar.AlertDialog.Negative" parent="Widget.AppCompat.Button.ButtonBar.AlertDialog"> <item name="android:textColor">?android:attr/textColorSecondary</item> </style> ThemeOverlay.AppCompat.Dialog.Alert を継承したテーマを用意し、その中で buttonBarNegativeButtonStyle に上で定義した style を指定します。 <style name="ThemeOverlay.Sample.Dialog.Alert" parent="ThemeOverlay.AppCompat.Dialog.Alert"> <item name="buttonBarNegativeButtonStyle">@style/Widget.Sample.Button.ButtonBar.AlertDialog.Negative</item> </style> 用意したテーマを AlertDialog.Builder のコンストラクタで指定するか AlertDialog.Builder(this, R.style.ThemeOverlay_Sample_Dialog_Alert) .setMessage("メッセージ") .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(android.R.string.ok, null) .show() alertDialogTheme に指定します。 <style name="Theme.Sample.Light" parent="Theme.AppCompat.Light"> <item name="alertDialogTheme">@style/ThemeOverlay.Sample.Dialog.Alert</item> </style>


AppCompat の設定と解説

alertDialogStyle に指定されている AlertDialog.AppCompat をたどっていくと、 <style name="Base.V7.Theme.AppCompat" parent="Platform.AppCompat"> … <item name="alertDialogStyle">@style/AlertDialog.AppCompat</item> </style> <style name="AlertDialog.AppCompat" parent="Base.AlertDialog.AppCompat"/> <style name="Base.AlertDialog.AppCompat" parent="android:Widget"> <item name="android:layout">@layout/abc_alert_dialog_material</item> <item name="listLayout">@layout/abc_select_dialog_material</item> <item name="listItemLayout">@layout/select_dialog_item_material</item> <item name="multiChoiceItemLayout">@layout/select_dialog_multichoice_material</item> <item name="singleChoiceItemLayout">@layout/select_dialog_singlechoice_material</item> <item name="buttonIconDimen">@dimen/abc_alert_dialog_button_dimen</item> </style> layout に @layout/abc_alert_dialog_material が指定されています。この中で button bar 部分は @layout/abc_alert_dialog_button_bar_material に分割されています。

これを見ると Negative Button には style="?attr/buttonBarNegativeButtonStyle" が指定されていることがわかります。

@layout/abc_alert_dialog_button_bar_material <ScrollView …> <android.support.v7.widget.ButtonBarLayout …"> … <Button android:id="@android:id/button2" style="?attr/buttonBarNegativeButtonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <Button android:id="@android:id/button1" style="?attr/buttonBarPositiveButtonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </android.support.v7.widget.ButtonBarLayout> </ScrollView> buttonBarNegativeButtonStyle には buttonBarButtonStyle が指定されており、buttonBarButtonStyle には Widget.AppCompat.Button.ButtonBar.AlertDialog が指定されています。 <style name="Base.V7.Theme.AppCompat" parent="Platform.AppCompat"> … <item name="buttonBarButtonStyle">@style/Widget.AppCompat.Button.ButtonBar.AlertDialog</item> <item name="buttonBarPositiveButtonStyle">?attr/buttonBarButtonStyle</item> <item name="buttonBarNegativeButtonStyle">?attr/buttonBarButtonStyle</item> <item name="buttonBarNeutralButtonStyle">?attr/buttonBarButtonStyle</item> </style> Widget.AppCompat.Button.ButtonBar.AlertDialog は最終的に Widget.AppCompat.Button.Borderless.Colored を継承しており、これによりボタンの文字が colorAccent 色になっています。 <style name="Widget.AppCompat.Button.ButtonBar.AlertDialog" parent="Base.Widget.AppCompat.Button.ButtonBar.AlertDialog"/> <style name="Base.Widget.AppCompat.Button.ButtonBar.AlertDialog" parent="Widget.AppCompat.Button.Borderless.Colored"> <item name="android:minWidth">64dp</item> <item name="android:minHeight">@dimen/abc_alert_dialog_button_bar_height</item> </style> Widget.AppCompat.Button.ButtonBar.AlertDialog を継承した style を用意し、文字色を上書きして buttonBarButtonStyle にセットすれば Negative Button の文字色を変えることができます。



2019年8月5日月曜日

SavedStateHandle を Dagger で生成させてる ViewModel で使う方法(AssistedInject を使わない方法)

(AssistedInject を使った方法はぐぐれば出てくるのでぐぐってください)

この内容は技術書典7で頒布予定の Master of Dagger(仮)の正式版にも掲載予定です。
ここには詳しい解説は書かないので、ぜひ Master of Dagger(仮)の正式版を買ってください。



0.

このような ViewModel があります。(Dagger で生成させてる MainViewModel のコンストラクタに SavedStateHandle を追加した) class MainViewModel @Inject constructor( private val api: MyApi, private val handle: SavedStateHandle ) : ViewModel() { ... } MyApi は Dagger で管理しています。 @Module class AppModule { @Singleton @Provides fun provideApi(): MyApi { return ... } } MainViewModel は ViewModel の Map Multibindings の1要素になっています。 @Module interface BindModule { @Binds @IntoMap @ViewModelKey(MainViewModel::class) fun bindMainViewModel(viewModel: MainViewModel): ViewModel }

1.

SavedStateHandle をオブジェクトグラフに含む Subcomponent を用意します。 @Module(subcomponents = [SavedStateViewModelComponent::class]) class SavedStateViewModelComponentModule @Subcomponent interface SavedStateViewModelComponent { @Subcomponent.Factory interface Factory { fun create( @BindsInstance handle: SavedStateHandle ): SavedStateViewModelComponent } fun providers(): Map<Class<out ViewModel>, Provider<ViewModel>> }

2.

次に AbstractSavedStateViewModelFactory を継承した Factory を用意します。

ポイントはコンストラクタで先ほど作った SavedStateViewModelComponent.Factory を受け取るようにし、create() でそれを利用して ViewModel の Map Multibindings を受け取るようにすることです。 class SavedStateViewModelFactory @Inject constructor( private val factory: SavedStateViewModelComponent.Factory, owner: SavedStateRegistryOwner, defaultArgs: Bundle? = null ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) { override fun <T : ViewModel?> create( key: String, modelClass: Class<T>, handle: SavedStateHandle ): T { val providers = factory.create(handle).providers() val found = providers.entries.find { modelClass.isAssignableFrom(it.key) } val provider = found?.value ?: throw IllegalArgumentException("unknown model class $modelClass") try { @Suppress("UNCHECKED_CAST") return provider.get() as T } catch (e: Exception) { throw RuntimeException(e) } } }

3.

SavedStateRegistryOwner と Bundle? をオブジェクトグラフに含む Subcomponent を用意します。
SavedStateComponent の modules に SavedStateViewModelComponentModule を指定します。 @Module(subcomponents = [SavedStateComponent::class]) class SavedStateComponentModule @Subcomponent(modules = [SavedStateViewModelComponentModule::class]) interface SavedStateComponent { @Subcomponent.Factory interface Factory { fun create( @BindsInstance owner: SavedStateRegistryOwner, @BindsInstance defaultArgs: Bundle? ): SavedStateComponent } fun viewModelFactory(): SavedStateViewModelFactory }

4.

AppComponent の modules に SavedStateComponentModule を指定します。 @Singleton @Component( modules = [ AppModule::class, BindModule::class, SavedStateComponentModule::class ] ) interface AppComponent { fun savedStateComponentFactory(): SavedStateComponent.Factory }

5.

SavedStateComponentから取得したFactoryを使います。 class MainActivity : AppCompatActivity() { private val viewModel: MainViewModel by viewModels { (application as MyApplication).appComponent .savedStateComponentFactory() .create(this, null) .viewModelFactory() } ... }

2019年7月3日水曜日

BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 編 : ViewPager + FragmentPagerAdapter での onResume() の挙動

ViewPager + FragmentPagerAdapter での setVisibleUserHint の挙動」の BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT 編です。


FragmentPagerAdapter の getItem() で返す Fragment に次のようにログを入れました。 class SimpleFragment : Fragment() { override fun onStart() { super.onStart() println("$position : onStart") } override fun onResume() { super.onResume() println("$position : onResume") } override fun onPause() { super.onPause() println("$position : onPause") } override fun onStop() { super.onStop() println("$position : onStop") } }

1. レイアウトより前のタイミング(例えば onCreate())で adapter をセットしている場合 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewPager.adapter = MyPager(supportFragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) } } この場合、ログは次のようになります。 System.out: 0 : onStart System.out: 1 : onStart System.out: 0 : onResume つまり
  • 1. position 0 の Fragment の onStart()
  • 2. position 1 の Fragment の onStart()
  • 3. position 0 の Fragment の onResume()
という流れです。



2. レイアウトより前のタイミング(例えば onCreate())で adapter をセットし、currentItem を変更している場合

onCreate() で ViewPager に adapter をセットした後 currentItem を 1 にしてみます。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewPager.adapter = MyPager(supportFragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) viewPager.currentItem = 1 } } この場合のログはこうなります。 System.out: 1 : onStart System.out: 0 : onStart System.out: 2 : onStart System.out: 1 : onResume つまり
  • 1. position 1 の Fragment の onStart()
  • 2. position 0 の Fragment の onStart()
  • 3. position 2 の Fragment の onStart()
  • 4. position 1 の Fragment の onResume()
という流れです。



3. レイアウトより後のタイミングで adapter をセットしている場合 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Handler().postDelayed( { viewPager.adapter = MyPager(supportFragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) }, 1000 ) } } onCreate() から1秒後に adapter をセットしてみます。この場合ログは次のようになります。 System.out: 0 : onStart System.out: 1 : onStart System.out: 0 : onResume 「1. レイアウトより前のタイミング(例えば onCreate())で adapter をセットしている場合」と同じですね。
  • 1. position 0 の Fragment の onStart()
  • 2. position 1 の Fragment の onStart()
  • 3. position 0 の Fragment の onResume()
という流れです。



4. レイアウトより後のタイミングで adapter をセットし、currentItem を変更している場合

onCreate() から1秒後に adapter をセットし、currentItem を 1 にしてみます。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Handler().postDelayed( { viewPager.adapter = MyPager(supportFragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) viewPager.currentItem = 1 }, 1000 ) } } この場合ログは次のようになります。 System.out: 0 : onStart System.out: 1 : onStart System.out: 0 : onResume System.out: 2 : onStart System.out: 0 : onPause System.out: 1 : onResume 「2. レイアウトより前のタイミング(例えば onCreate())で adapter をセットし、currentItem を変更している場合」と同じになりません!

まず「3. レイアウトより後のタイミングで adapter をセットしている場合」と同じことが起こります。

つまり
  • 1. position 0 の Fragment の onStart()
  • 2. position 1 の Fragment の onStart()
  • 3. position 0 の Fragment の onResume()
という流れが最初にあります。

その後
  • 4. position 2 の Fragment の onStart()
  • 5. position 0 の Fragment の onPause()
  • 6. position 1 の Fragment の onResume()
という流れになります。

adapter をセットしたタイミングで 1 〜 3 までの流れが起こり、currentItem を 1 にしたタイミングで 4 〜 6 の流れが起こります。

このように、レイアウトよりも後のタイミングで adapter をセットすると、その時点で position 0 の Fragment を PrimaryItem として onResume() まで呼ばれてしまいます。

そのため、レイアウトよりも後のタイミングで adapter をセットして currentItem を 0 以外に変更する場合、「Fragment がユーザーに表示されたタイミングで xx したい」処理のトリガーとして onResume() を使うには注意が必要です。

adapter がセットされている間の onResume() に反応してしまうと、その後 currentItem が変更されて実際にはユーザーにはほぼ見えない Fragment でも処理が走ってしまうため、adapter がセットされている間だけフラグを立てておいて onResume() で反応しないような工夫が必要になります。


androidx.fragment:fragment:1.1.0-alpha07 で userVisibleHint は deprecated になりました

ViewPager + FragmentPagerAdapter での setVisibleUserHint の挙動」の冒頭で軽く言及しましたが、androidx.fragment:fragment:1.1.0-alpha07 で FragmentPagerAdapter および FragmentStatePagerAdapter に変更が入っています。

1.1.0-alpha08 でも変更が入っており、以下の挙動は 1.1.0-rc01 で確認しています。


今まで FragmentPagerAdapter および FragmentStatePagerAdapter のコンストラクタでは FragmentManager だけを渡していましたが、1.1.0-alpha07 からはフラグ(int)も渡すように変わりました。

ここで指定できるフラグとして以下の2つが用意されています。
  • BEHAVIOR_SET_USER_VISIBLE_HINT
  • BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
今までの FragmentManager だけを渡すコンストラクタは deprecated になり、内部ではフラグとして BEHAVIOR_SET_USER_VISIBLE_HINT が指定されます。 public abstract class FragmentPagerAdapter extends PagerAdapter { ... @Deprecated public FragmentPagerAdapter(@NonNull FragmentManager fm) { this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT); } ... } つまり BEHAVIOR_SET_USER_VISIBLE_HINT を指定すると、1.1.0-alpha06 までと同じ挙動になるということです。

ただし、この BEHAVIOR_SET_USER_VISIBLE_HINT 自体も deprecated です。 public abstract class FragmentPagerAdapter extends PagerAdapter { ... @Deprecated public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0; public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1; ... } さらに Fragment の setUserVisibleHint(), getUserVisibleHint() も deprecated になりました。


では新しい BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT を指定するとどうなるのか、挙動を確認してみましょう。

FragmentPagerAdapter の getItem() で返す Fragment に次のようにログを入れました。 class SimpleFragment : Fragment() { override fun setUserVisibleHint(isVisibleToUser: Boolean) { super.setUserVisibleHint(isVisibleToUser) println("$position : setUserVisibleHint : $isVisibleToUser") } override fun onStart() { super.onStart() println("$position : onStart") } override fun onResume() { super.onResume() println("$position : onResume") } override fun onPause() { super.onPause() println("$position : onPause") } override fun onStop() { super.onStop() println("$position : onStop") } } class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewPager.adapter = MyPager(supportFragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) } } この場合、ログは次のようになります。 System.out: 0 : onStart System.out: 1 : onStart System.out: 0 : onResume setUserVisibleHint() は呼ばれません。

position 0 と 1 の Fragment の onStart() が呼ばれ、その後 position 0 の Fragment だけ onResume() が呼ばれます。


この状態(position 0 が表示されいる状態)で右にスワイプして position 1 を表示すると、次のようなログが出ます。 System.out: 2 : onStart System.out: 0 : onPause System.out: 1 : onResume position 2 の Fragment が生成されて onStart() が呼ばれ、その後 position 0 の Fragment の onPause() が呼ばれて、position 1 の Fragment の onResume() が呼ばれます。


この状態(position 1 が表示されいる状態)で左にスワイプして再度 position 0 を表示すると、次のようなログが出ます。 System.out: 2 : onStop System.out: 1 : onPause System.out: 0 : onResume position 2 の Fragment が detach されるので onStop() が呼ばれています。
その後 position 1 の Fragment の onPause() が呼ばれて、position 0 の Fragment の onResume() が呼ばれます。


つまり、BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT を指定すると、PrimaryItem の Fragment だけが Lifecycle.State.RESUMED に達し、その他の Fragment は Lifecycle.State.STARTED で止まるという挙動になります。

onResume() が呼ばれるのは PrimaryItem の Fragment だけなので、「Fragment がユーザーに表示されたタイミングで xx したい」という場合に onResume() をトリガーとすることができるようになりました。



ViewPager + FragmentPagerAdapter での setVisibleUserHint の挙動

以下は androidx.fragment:fragment:1.0.0 での挙動です。1.1.0-alpha07 で別の挙動にするためのオプションが追加されています。



ViewPager + FragmentPagerAdapter の構成はよく使うと思います。

FragmentPagerAdapter では、ViewPager のページが切り替わる処理の中で、
以前の PrimaryItem の Fragment に対して setUserVisibleHint(false) を呼び、
新しい PrimaryItem の Fragment に対して setUserVisibleHint(true) を呼んでいます。


挙動を確認するために、FragmentPagerAdapter の getItem() で返す Fragment に次のようにログを入れました。 class SimpleFragment : Fragment() { ... override fun setUserVisibleHint(isVisibleToUser: Boolean) { println("$position : setUserVisibleHint : before super : to = $isVisibleToUser, current = $userVisibleHint") super.setUserVisibleHint(isVisibleToUser) println("$position : setUserVisibleHint : after super : to = $isVisibleToUser, current = $userVisibleHint") } override fun onStart() { super.onStart() println("$position : onStart : $userVisibleHint") } }

1. レイアウトより前のタイミング(例えば onCreate())で adapter をセットしている場合 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewPager.adapter = MyPager(supportFragmentManager) } } この場合、ログは次のようになります。 System.out: 0 : setUserVisibleHint : before super : to = false, current = true System.out: 0 : setUserVisibleHint : after super : to = false, current = false System.out: 1 : setUserVisibleHint : before super : to = false, current = true System.out: 1 : setUserVisibleHint : after super : to = false, current = false System.out: 0 : setUserVisibleHint : before super : to = true, current = false System.out: 0 : setUserVisibleHint : after super : to = true, current = true System.out: 0 : onStart : true System.out: 1 : onStart : false まず position 0 と 1 の Fragment の setUserVisibleHint が false にセットされます。

super.setUserVisibleHint() が呼ばれる前の時点で取得した userVisibleHint が true ですね。 実は Fragment の mUserVisibleHint はデフォルトが true です。 package androidx.fragment.app; ... public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener, LifecycleOwner, ViewModelStoreOwner { ... // Hint provided by the app that this fragment is currently visible to the user. boolean mUserVisibleHint = true; ... }
次に position 0 の Fragment の setUserVisibleHint が true にセットされます。

その後、Fragment の onStart() が呼ばれます。

つまり
  • 1. position 0 の Fragment の setUserVisibleHint() : true → false
  • 2. position 1 の Fragment の setUserVisibleHint() : true → false
  • 3. position 0 の Fragment の setUserVisibleHint() : false → true
  • 4. position 0 の Fragment の onStart() : setUserVisibleHint は true
  • 5. position 1 の Fragment の onStart() : setUserVisibleHint は false
という流れです。



2. レイアウトより前のタイミング(例えば onCreate())で adapter をセットし、currentItem を変更している場合

onCreate() で ViewPager に adapter をセットした後 currentItem を 1 にしてみます。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewPager.adapter = MyPager(supportFragmentManager) viewPager.currentItem = 1 } } この場合のログはこうなります。 System.out: 1 : setUserVisibleHint : before super : to = false, current = true System.out: 1 : setUserVisibleHint : after super : to = false, current = false System.out: 0 : setUserVisibleHint : before super : to = false, current = true System.out: 0 : setUserVisibleHint : after super : to = false, current = false System.out: 2 : setUserVisibleHint : before super : to = false, current = true System.out: 2 : setUserVisibleHint : after super : to = false, current = false System.out: 1 : setUserVisibleHint : before super : to = true, current = false System.out: 1 : setUserVisibleHint : after super : to = true, current = true System.out: 1 : onStart : true System.out: 0 : onStart : false System.out: 2 : onStart : false まず position 1, 0, 2 の Fragment の setUserVisibleHint が false にセットされ、
次に position 1 の Fragment の setUserVisibleHint が true にセットされ、
その後、Fragment の onStart() が呼ばれます。

つまり
  • 1. position 1 の Fragment の setUserVisibleHint() : true → false
  • 2. position 0 の Fragment の setUserVisibleHint() : true → false
  • 3. position 2 の Fragment の setUserVisibleHint() : true → false
  • 4. position 1 の Fragment の setUserVisibleHint() : false → true
  • 5. position 1 の Fragment の onStart() : setUserVisibleHint は true
  • 6. position 0 の Fragment の onStart() : setUserVisibleHint は false
  • 7. position 2 の Fragment の onStart() : setUserVisibleHint は false
という流れです。



3. レイアウトより後のタイミングで adapter をセットしている場合 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Handler().postDelayed( { viewPager.adapter = MyPager(supportFragmentManager) }, 1000 ) } } onCreate() から1秒後に adapter をセットしてみます。この場合ログは次のようになります。 System.out: 0 : setUserVisibleHint : before super : to = false, current = true System.out: 0 : setUserVisibleHint : after super : to = false, current = false System.out: 1 : setUserVisibleHint : before super : to = false, current = true System.out: 1 : setUserVisibleHint : after super : to = false, current = false System.out: 0 : setUserVisibleHint : before super : to = true, current = false System.out: 0 : setUserVisibleHint : after super : to = true, current = true System.out: 0 : onStart : true System.out: 1 : onStart : false 「1. レイアウトより前のタイミング(例えば onCreate())で adapter をセットしている場合」と同じですね。
  • 1. position 0 の Fragment の setUserVisibleHint() : true → false
  • 2. position 1 の Fragment の setUserVisibleHint() : true → false
  • 3. position 0 の Fragment の setUserVisibleHint() : false → true
  • 4. position 0 の Fragment の onStart() : setUserVisibleHint は true
  • 5. position 1 の Fragment の onStart() : setUserVisibleHint は false
という流れです。



4. レイアウトより後のタイミングで adapter をセットし、currentItem を変更している場合

onCreate() から1秒後に adapter をセットし、currentItem を 1 にしてみます。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Handler().postDelayed( { viewPager.adapter = MyPager(supportFragmentManager) viewPager.currentItem = 1 }, 1000 ) } } この場合ログは次のようになります。 System.out: 0 : setUserVisibleHint : before super : to = false, current = true System.out: 0 : setUserVisibleHint : after super : to = false, current = false System.out: 1 : setUserVisibleHint : before super : to = false, current = true System.out: 1 : setUserVisibleHint : after super : to = false, current = false System.out: 0 : setUserVisibleHint : before super : to = true, current = false System.out: 0 : setUserVisibleHint : after super : to = true, current = true System.out: 0 : onStart : true System.out: 1 : onStart : false System.out: 2 : setUserVisibleHint : before super : to = false, current = true System.out: 2 : setUserVisibleHint : after super : to = false, current = false System.out: 0 : setUserVisibleHint : before super : to = false, current = true System.out: 0 : setUserVisibleHint : after super : to = false, current = false System.out: 1 : setUserVisibleHint : before super : to = true, current = false System.out: 1 : setUserVisibleHint : after super : to = true, current = true System.out: 2 : onStart : false 「2. レイアウトより前のタイミング(例えば onCreate())で adapter をセットし、currentItem を変更している場合」と同じになりません!

まず「3. レイアウトより後のタイミングで adapter をセットしている場合」と同じことが起こります。

つまり
  • 1. position 0 の Fragment の setUserVisibleHint() : true → false
  • 2. position 1 の Fragment の setUserVisibleHint() : true → false
  • 3. position 0 の Fragment の setUserVisibleHint() : false → true
  • 4. position 0 の Fragment の onStart() : setUserVisibleHint は true
  • 5. position 1 の Fragment の onStart() : setUserVisibleHint は false
という流れが最初にあります。

その後
  • 6. position 2 の Fragment の setUserVisibleHint() : true → false
  • 7. position 0 の Fragment の setUserVisibleHint() : true → false
  • 8. position 1 の Fragment の setUserVisibleHint() : false → true
  • 9. position 2 の Fragment の onStart() : setUserVisibleHint は false
という流れになります。 adapter をセットしたタイミングで 1 〜 5 までの流れが起こり、currentItem を 1 にしたタイミングで 6 〜 9 の流れが起こります。

しかも position 1 が表示されているのに、onStart() 時の setUserVisibleHint 値を見ると position 0 が表示されているかのようです。


このように、レイアウトよりも後のタイミングで adapter をセットすると、その時点で position 0 の Fragment を PrimaryItem として onStart() まで呼ばれてしまいます。
そのため、レイアウトよりも後のタイミングで adapter をセットし、currentItem を 0 以外に変更する場合は setUserVisibleHint() や onStart() が呼ばれるタイミングに注意が必要です。


2019年6月30日日曜日

Google Document で文字の上にドット(V̇ など)を入れる

1. ドットをつけたい文字を入力します。



2. 次に [挿入] - [特殊文字] を開きます。



3. キーワードに U+0307 を入力します。



4. COMBINING DOT ABOVE (U+0307) が出てくるのでクリックします。



5. 先ほどの文字の上にドットがつきます。







2019年6月15日土曜日

RecyclerView 内の TextView で textIsSelectable が効かない問題に対応する

TextView に android:textIsSelectable="true" を指定すると、TextView 長押しで文字列を選択するモードになります。

しかし RecyclerView 内の TextView にこの指定をしても、
"TextView does not support text selection. Selection cancelled."
というメッセージがログに出て、文字列を選択するモードになりません。
(問題は Android 5.0, 5.1 では起こりません。Android 6.0 以降では起こります。)

これは Android platform の既知のバグのようです。
https://code.google.com/p/android/issues/detail?id=208169

対象方法は、一度 TextView を disabled にしてから enabled にします。

https://issuetracker.google.com/issues/37095917#comment11
https://stackoverflow.com/questions/37566303/edittext-giving-error-textview-does-not-support-text-selection-selection-canc

class MainAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ... override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) { super.onViewAttachedToWindow(holder) if (holder is ViewHolder) { holder.itemView.textView.isEnabled = false holder.itemView.textView.isEnabled = true } } }


2019年5月5日日曜日

Kotlin メモ : useLines

Reader.useLines public inline fun <T> Reader.useLines(block: (Sequence<String>) -> T): T = buffered().use { block(it.lineSequence()) } val reader: Reader = ... reader.buffered().use { val sequence : Sequence<String> = it.lineSequence() ... } val reader: Reader = ... reader.useLines { val sequence : Sequence<String> = it ... }

2019年5月3日金曜日

AndroidX Preference の SummaryProvider

以下は「TechBoosterプログラミングブック ~0から学ぶ最新技術とアプリ開発テクニック~【C95新刊】」に寄稿した内容を元にしています。

-------------------

AndroidX で Preference のライブラリもアップデートされました。この Preference ライブラリを使うと、Material Design に沿った設定画面を簡単に作ることができます。

API Level 1 からある android.preference.PreferenceActivity は今では使用しません。 また、API Level 11 で追加された android.preference.PreferenceFragment は API level 28 で deprecated になっています。

設定

androidx.preference release note

最新のバージョンは 1.1.0-alpha04、stable のバージョンは 1.0.0 です。 dependencies { ... // stable は 1.0.0 implementation "androidx.preference:preference:1.1.0-alpha04" }

基本的な使い方

androidx.preference.PreferenceFragmentCompat を使います。

preference 階層を設定する方法として次の3つの方法があります。
  • XML で指定する
  • Activity の meta-data として XML を指定する
  • PreferenceScreen オブジェクトを指定する
XML で preference 階層を設定するには setPreferencesFromResource() または addPreferencesFromResource() を使います。 class SettingFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.pref, rootKey) } } 1.0.0 で PreferenceThemeOverlay がデフォルトで適用されるようになったため、カスタマイズしない場合 Preference のテーマの指定をしなくてもよくなりました。また、PreferenceThemeOverlay.v14 およびPreferenceThemeOverlay.v14.Material テーマは deprecated になっています。

Summary 機能

1.1.0-alpha01 で Summary に関係する機能が入りました。

app:useSimpleSummaryProvider

ListPreference と EditTextPreference で app:useSimpleSummaryProvider という属性を指定できるようになりました。
この属性に true を指定すると、preference の値が変わったときにその値(ListPreference の場合は対応する android:entries または android:entryValues の値)が Summary に表示されます。値が保存されていないときは "Not Set" と表示されます。 <EditTextPreference ... app:useSimpleSummaryProvider="true" /> <ListPreference ... app:useSimpleSummaryProvider="true" />
SummaryProvider

SummaryProvider を使うと、任意の Preference で値が変わったときに Summary を更新できます。

app:useSimpleSummaryProvider="true" を指定した ListPreference では ListPreference.SimpleSummaryProvider が、EditTextPreference では EditTextPreference.SimpleSummaryProvider が SummaryProvider として利用されます。

CheckBoxPreference に SummaryProvider をセットすると、チェックの状態の応じて Summary が変わるようにすることができます。 findPreference("check").setSummaryProvider { if ((it as CheckBoxPreference).isChecked) "する!" else "しない..." } いままでは以下のように起動時に Summary をセットし、さらに PreferenceChangeListener を登録して値変更時に Summary をセットするような処理が必要だったので、SummaryProvider を使うととてもシンプルにかけます。 with(findPreference("check") as CheckBoxPreference) { summary = if (isEnabled) "する!" else "しない..." setOnPreferenceChangeListener { preference, newValue -> preference.summary = if ((newValue as Boolean)) "する!" else "しない..." true } }


2019年3月10日日曜日

Kotlin メモ : File.outputStream(), OutputStream.writer()

fun File.outputStream(): FileOutputStream
fun OutputStream.writer(charset: Charset = Charsets.UTF_8): OutputStreamWriter
fun File.writer(charset: Charset = Charsets.UTF_8): OutputStreamWriter

val writer = OutputStreamWriter(FileOutputStream(file), charset) writer.write(...) writer.close() file.outputStream() .writer(charset) .use { it.write(...) } file.writer(charset) .use { it.write(...) }

2019年2月27日水曜日

ユースケースの理解を深めるために「ユースケース駆動開発実践ガイド」を読んでみた



話は twitter で FizzBuzz アンケートを取ったことに遡る。



なぜこんなアンケートを取ったかというと、DroidKaigi 2019 のあるセッションで「これが UseCase だ」と言っていたものを自分はそうは思わなかった(domain layer のロジックだと思った)からである。



いろんな人の意見を聞けてとてもよかった。



このアンケートを起点としたやりとりの中でユースケース図の話があり、



ユースケース図、およびユースケースについて理解を深めたかったで「ユースケース駆動開発実践ガイド」を読んでみた。

読んだ結果の理解は



「ユースケースはユーザーとシステムとの対話である」





現状、私は Clearn Architecture の UseCase は 依存方向を規定するたの構成レイヤーの一つと理解している。



2019年2月20日水曜日

Intent の equals 判定には filterEquals() を使う

val intent1 = Intent(Intent.ACTION_VIEW) val intent2 = Intent(Intent.ACTION_VIEW) これに対し assertThat(intent1).isEqualTo(intent2) // fail は失敗します。Intent で equals() は override されていません。

では Intent の比較はどうするかというと filterEquals() を使います assertThat(intent1.filterEquals(intent2)).isTrue() // success Intent の構成をチェックしたいだけなら IntentSubject を使うという方法もあります。 IntentSubject.assertThat(intent1).hasAction(Intent.ACTION_VIEW) // success


class Navigator(private val context: Context) { fun moveTo() { context.startActivity( Intent( Intent.ACTION_VIEW, Uri.parse("https://developer.android.com/") ) ) } } このクラスに対して context.startActivity() の呼び出しをチェックする以下のテストは失敗します。Intent の equals() が false になるからです。 val context = mock(Context::class.java) Navigator(context).moveTo() // fail verify(context).startActivity( Intent( Intent.ACTION_VIEW, Uri.parse("https://developer.android.com/") ) ) argThat を使って filterEquals() を使えば成功します。 val context = mock(Context::class.java) Navigator(context).moveTo() // success verify(context).startActivity(argThat { intent -> intent.filterEquals( Intent( Intent.ACTION_VIEW, Uri.parse("https://developer.android.com/") ) ) })


2019年2月19日火曜日

Kotlin メモ : reduce

reduce
reduceIndexed
reduceRight
reduceRightIndexed

reduce

inline fun <S, T : S> Iterable<T>.reduce(operation: (acc: S, T) -> S): S 蓄積値(acc)の初期値を最初の要素の値する
2番目の要素から順番に、現在の蓄積値(acc)と要素の値に対して operation を実行し、結果を次の蓄積値(acc)とする

やっていることを for で書くとこんな感じ var acc = list[0] for (index in 1 until list.size) { acc = operation(acc, list[index]) } ↓ だと operation は 4回呼ばれ、
1回目 : acc = list[0] = 1, i = list[1] = 2
2回目 : acc = 1 + 2 = 3, i = list[2] = 3
3回目 : acc = 3 + 3 = 6, i = list[3] = 4
4回目 : acc = 6 + 4 = 10, i = list[4] = 5
戻り値 : 10 + 5 = 15 val reduce = listOf(1, 2, 3, 4, 5).reduce { acc, i -> acc + i } println(reduce) ↓ 要素値をそのまま返せば前後の要素に対する処理(ここでは前後の要素の差分をリストにする)を行える val result = mutableListOf<Int>() listOf(1, 2, 3, 4, 5).reduce { acc, i -> result.add(i - acc) i } println(result) // [1, 1, 1, 1]

reduceIndexed

fun <S, T : S> Iterable<T>.reduceIndexed(operation: (index: Int, acc: S, T) -> S): S 処理の流れは reduce と同じで、operation に index も渡されるところが異なる

reduceRight

fun <S, T : S> List<T>.reduceRight(operation: (T, acc: S) -> S): S reduce と違って Iterable ではなく List の拡張関数(Array もある)

蓄積値(acc)の初期値が最後の要素の値で、最後から2番目の要素から戻る順番で operation が呼ばれる

↓ だと operation は 4回呼ばれ、
1回目 : acc = list[4] = 5, i = list[3] = 4
2回目 : acc = 5 + 4 = 9, i = list[2] = 3
3回目 : acc = 9 + 3 = 12, i = list[1] = 2
4回目 : acc = 12 + 2 = 14, i = list[0] = 1
戻り値 : 14 + 1 = 15 val reduce = listOf(1, 2, 3, 4, 5).reduceRight { acc, i -> acc + i } println(reduce)

reduceRightIndexed

fun <S, T : S> List<T>.reduceRightIndexed(operation: (index: Int, T, acc: S) -> S): S 処理の流れは reduceRight と同じで、operation に index も渡されるところが異なる


2019年2月8日金曜日

DroidKaigi 2019 で「LiveData と Coroutines で 実装する DDD の戦術的設計」について話してきました。

前回前々回に引き続き、スピーチ原稿と合わせて公開します。

(講演ではアドリブもあるので原稿とは微妙に異なることをご了承ください)

みなさん、こんにちは。あんざいゆきです。 Y.A.Mの雑記帳というブログを書いています。Android の Google Developer Experts もしています。あと TechBooster というところで Android の同人誌書いてます。

twitter の id は yanzm でやんざむと読みます。
あんざいでも、やんざむでもお好きな方で呼んでください。

DroidKaigi でドメイン駆動設計について話をするのは3回目になります。はじめて聞くよという方も、いままで続けて聞いているよ、という方もどうもありがとうございます。

では、はじめましょう。
まずは、これまでの復習をしたいと思います。
前々回の「ドメイン駆動設計とはなにか」というところから。
詳しくは私のブログに前々回のまとめがあるので、そちらをお読みください。
ドメイン駆動設計とは

ドメインエキスパートの言葉を観察し、ドメインを構成するユビキタス言語を見つけ、
次にユビキタス言語を使ってドメインを適切に反映した、我々のソフトウェアに役立つドメインモデルを作り、
そして作ったドメインモデルを正確に表現するようコードを実装し、これを繰り返し、ドメインモデルと実装の両方を洗練させていく設計手法です。
ドメイン駆動設計では、実践するために役立つさまざまな手法が出てきます。これらは主に戦略的設計と戦術的設計の2つに分けることができます。
ドメインモデルを作り上げるために役立つ手法が戦略的設計、ドメインモデルからそれを表現した実装を行っていくのに役立つ手法が戦術的設計です。
戦略的設計のユビキタス言語、境界づけられたコンテキスト、コンテキストマップについては前々回にお話しました。
ドメイン駆動設計では、ドメインモデルをそのまま表現するように実装します。
究極的にはコードを読めばそのドメインモデルがわかるし、そのドメインモデルを理解しているならエンジニアじゃなくてもなんとなくテストコードが読めるという状態です。
具体的にどうすれば、ドメインモデルをそのまま表現した実装にできるのか。
そのための、こういうふうに実装するとうまくドメインモデルを表現できたよ、という実装パターンが戦術的設計です。

ここに一部載せていますが、戦術的設計としてたくさんパターンが紹介されています。
これらを全て取り入れないといけないというものではありません。
むしろ、一つも取り入れなくても、ドメインモデルをうまく表現した実装ができるなら、それはちゃんとドメイン駆動設計です。

前回は何の話をしたかというと、
ドメイン駆動設計を念頭に既存のAndroidアプリをリファクタリングしてこれはよかったなという戦術的設計パターンを紹介しました。
「ドメインを隔離する」という戦術的設計パターンは、Android ではgradle のモジュールで分けるという方法があり、モジュールに分けることで依存方向を強制できて、ドメインが UI などのその他の部分を知らない状態にできます。
値オブジェクトの話もしました。同じ属性値をもっているなら区別する必要がないモデルは値オブジェクトとして表現しましょう。

文字列で取り回している要素が値オブジェクトではないか考えましょう。

IDとか、種類やタイプ、日付やサイズなどは値オブジェクトを導入するよいスタート地点です。
エンティティについても少し話ました。属性値ではなく同一性で区別されるものがエンティティです。
例えば、AさんとBさんが同じ山田太郎という名前であっても、別の人として区別されます。
ここまでが復習です。
今回は、この講演のなかで、実際にモデリングと実装を考えてみたいと思います。

対象とするドメインはみなさんに関係あるこの DroidKaigi 2019 とします。
マスターデータはサーバにあって、APIを介してアプリがサーバーからデータをもらい、そのAPI仕様は変えられないという前提です。
このあと出てくるコードを github で公開しています。
まず、戦略的設計の
コンテキストマップを書いてみました。

DroidKaigi 2019 というドメインには、応募されたセッションを管理したり、スタッフの管理をしたりなど、カンファレンスを管理するという問題空間があります。 一方、参加者がカンファレンスの情報を閲覧するという問題空間もあります。

カンファレンスを管理する特定のソリューションとしてサーバー上のなんらかの実装があり、カンファレンス情報を閲覧するためのソリューションとしての Android アプリがそれとやりとりをします。

Android アプリ側はサーバーとのやりとりの仕様を変えられないので、コンテキストの統合の関係は Android側が順応者になります。

まとめると、
DroidKaigi 2019 という対象ドメインの カンファレンス情報を閲覧するという問題に対して、Android アプリというソリューションを考えます ドメインエキスパートはカンファレンスの参加者や運営者です。
次にユビキタス言語をチェックします。
ドメインエキスパートはカンファレンスの参加者や運営者ですが、自分も参加者なので、まずは運営者の言葉をチェックしましょう。

運営者にインタビューするのが一番いいのですが、運営者忙しいので、運営からのお知らせや Webサイトの言葉をチェックすることにします。
スポンサー、タイムテーブル、セッション、エンジニア、Android、カンファレンス、開催概要などなど

ざっくりチェック先を洗い出せたらさっそくモデリングしましょう。
カンファレンス情報を閲覧するという問題に対して、一番重要なのはやはりセッションの情報でしょう。
ということで、セッションについて考えます。
まずは言葉のチェックから。
タイトルとか発表者とか、実際に使われている言葉がなにかをチェックします。
DroidKaigi 2019 という比較的複雑ではないドメインであって、言葉のゆらぎは起こってしまうようで、
セッションの内容を説明する項目は、募集要項だとアブストラクトなのですが、Sessionize というセッション情報を管理するサービスを DroidKaigi は利用しているのですが、これの項目名は Description になっています。 ここは募集要項を優先してアブストラクトを使うことにします。
セッションの内容に対するカテゴリを応募時に選択するのですが、これも場所によってカテゴリだったりトピックだったりしています。
これも募集要項を優先してカテゴリ、ただし英語のほうも Category を使うことにします。
SessionFormat は発表時間、30分か50分か、
Language は発表言語、日本語か英語か
の項目なのですが、対応する日本語が Web やメールにはなかったので、そのまま英語表記のみで進めます。 本来ならここで運営者にインタビューして、日本語でなんと言っているのか確認しましょう。
これでセッションに関する項目名が出揃いました。
セッションをモデリングするにあたって、セッションが値オブジェクトなのかエンティティなのかを考えましょう。

最初の復習のところでも触れましたが、属性値によって区別されるされるものが値オブジェクト、同一性によって区別されるものがエンティティです。
まったく同じタイトルなら同じセッションかというと、別々のセッションで同じタイトルはありえるので No

カンファレンス当日までの間にアブストラクトが変わったとしても同じセッションなので Yes

よってセッションはエンティティです。
同一性のあるセッションをドメインモデルとして表現するには一意に識別するための ID が必要になります。
ということで、セッションを構成するものが出揃いました。
これを実際に実装してみましょう。
id の型は値オブジェクトです。ここでは data class にしていますが、Kotlin の inline class を使うのも手です。
タイトル、
アブストラクト

プロパティ名にはユビキタス言語をつかいます。
発表者は、複数人の場合がありえるので List にしました。
発表者についてまだモデリングしていないので Speaker は空クラスにしておきます。
SessionFormat は選択肢が30分か50分と決まっているので enum にしました。
Language も同様に enum
カテゴリも同様に enum にしました。
同時翻訳は、対象か、対象でないかの2択なので Boolean にしました。
部屋も選択肢が決まっているので enum にしました。
日付・時間についてはまだモデリングしていないので Speaker と同じく空クラスにしておきます。
できました。
これを使ってダミーデータを用意しておきます。
いま作った Session クラスおよびそのダミーデータを使って、セッション詳細画面を作ってみましょう。
TextView を並べて、String の項目はそのままセットして、それ以外は toString() をセットすると
こんな感じになります。
タイトルとアブストラクト以外のところの文字列表現を考えないといけないですね。まずは enum から考えましょう。
この種別のテキスト表現をどこに置くか問題ですが、3つほど案があります。
まず、enum に string resources を持たせる方法はどうでしょうか
UI 側は TextView の setText() に string resrouce の id をそのまま渡せばいいのでシンプルです。
ただし、この enum を置く domain module が Android Framework に依存することになります。
Kotlin multiplatform project ではこの enum を shared code に置けません。
enum に文字列を持たせるのはどうでしょうか。

多言語対応として Locale に応じて文字を出し分ける必要があります。
UI 側から Locale を渡す必要があるので案1より UI側は複雑になります。

また、java.util パッケージ の Locale に依存していると、Kotlin multiplatform project の shared code に置けません。置くなら Locale に相当するものを expect で用意するか、
このように Locale による判定を UI 側で行うようにする必要があります。
3つめの UI に string resoures を持たせる方法はどうでしょうか。
拡張関数として string resoure id を返す関数を定義すれば、UI側からは enum が string resources を持っているかのように記述できます。
いちいち Locale を渡さなくて済みますが、Kotlin multiplatform project の場合、各プラットフォームごとに文字列を定義しなければなりません。
enum のテキスト表現をどこに置くのか
ドメインモデルの表現としてモデルが文字列を持つのが自然なら enum に文字列を持たせればいいし
文字列表現は UI の関心事とするのが自然なら UI に文字列を定義すればよいのです
カンファレンス情報を閲覧するドメインではどうかという視点もあるし、Kotlin multiplatform project にするかどうかという実装からのフィードバックがモデルに影響するということでもあります。

一概にこれと決められるものではありません。
実装の前提として Kotlin multiplatform project ではないので、ここでは UI に string resources を持たせることにしましょう。
SessionFormat, Language, カテゴリー、部屋 の種別を文字列リソースにマッピングする拡張関数を用意して、
それを利用するように UI 側を変えると
種別部分の表示も良くなりました
残りは発表者と日付·時間部分です。
まず、日付・時間について考えましょう。
セッションには開始日時と終了日時があるので、
単純に start と end を持たせて見ました。
セッションの日時は東京での時間、つまり関心ごとは現地時間なので
LocalDateTime を使ったり、”Asia/Tokyo” ゾーンに固定した ZonedDateTime を使った方がよさそうです。
DroidKaigi 2019 のドメインでは日をまたぐセッションはないので、日付と時間を別々にすると、日をまたぐセッションはないということを表現できます。

日に LocalDate、時間に LocalTime を使えば関心ごとは現地時間ということも表現できます。
これでセッションの日付と時間を表現できるようになりましたが、TimeAndDate の文字列表現をどこで行うかを考えないといけません。
日付・時間のテキスト表現がモデルの関心事なら、TimeAndDate に文字列を返すメソッドを用意すればよいでしょう。

この場合、テキスト表現のロジックが変わったら、それを利用しているすべての画面に影響します。影響して OK というか、影響しなければならない、という状況なら日付・時間のテキスト表現はモデルの関心事です。
一方、画面によってテキスト表現がことなるなら、日付・時間のテキスト表現は UI の関心事でしょう。

日付・時間のテキスト表現が UI の関心事なら、UI 側に文字列を返すメソッドを用意します。
いずれの方法をとるにせよ、これで日付·時間の表示もよくなりました。
残っていた発表者について考えてみましょう。
発表者はエンティティですね。発表者Aさんと発表者Bさんが同じ名前でも別の発表者です。これはもう大丈夫ですよね。
発表者に関する言葉をチェックしました。
あいまいな言葉として「氏名」と「表示名」がありましたが、ここでは 氏名:Name を使うことにします。
Speaker の id も値オブジェクトとして用意しましょう。
ダミーデータを更新して
発表者の表示もよくなりました。
そろそろダミーデータをやめたいですよね。
そこで、次に導入するのがリポジトリーです。
ドメイン駆動設計じゃなくてもリポジトリーって名前わりと聞ききますけど、リポジトリーって結局なんなんでしょう。

「エリック・エヴァンスのドメイン駆動設計」ではリポジトリーについて何と言っているか見てみましょう。
リポジトリは、特定の型のオブジェクトを、すべて概念上の集合(通常は、それを模したもの)として表現する。これはコレクションのように動作するが、より手の込んだクエリ機能を持っている。
グローバルアクセスを必要とするオブジェクトの各型に対して、あるオブジェクトを生成し、その型のすべてのオブジェクトで構成されるコレクションが、メモリ上にあると錯覚させることができるようにすること。
よく知られているグローバルインタフェースを経由してアクセスできるようにすること。… クライアントをモデルに集中させ、あらゆるオブジェクトの格納をアクセスをリポジトリに委譲すること。

ちょっと、よくわからないですよね。

エリック・エヴァンスのドメイン駆動設計で言っていることを簡単に解説します。
モデルのオブジェクトを使って何かをするにはそのオブジェクトへの参照が必要になります。

参照を手に入れる方法として、1つ目はオブジェクトを生成するというのがあります。
これまではセッションのダミーデータを生成していたので、それを使って画面に表示することができました。

2つ目が関連をたどるというものです。セッションからその発表者オブジェクトへたどることができます。

3つ目がデータベースに格納されているオブジェクトを取り出すことです。

3つ目の方法で、もしクライアントが直接クエリを構築してデータベースからデータを取得し、オブジェクトを作っていたら、エンティティと値オブジェクトはただのデータコンテナになってしまいます。
そこで、技術的なインフラストラクチャやデータベースアクセスの仕組みをカプセル化し、クライアントからは、特定の型のオブジェクトの概念上の集合のように見えるようにしたのがリポジトリです。
つまり、クライアントが、どう永続化されているかとかどういうインフラストラクチャを使っているかという技術的な詳細を意識せず、必要なオブジェクトの参照を取得するためのあれこれを一手に引き受けてカプセル化するのがリポジトリです。

「エリック・エヴァンスのドメイン駆動設計」ではデータベースアクセスが念頭にある感じですが、内部で行なっていることがサーバーアクセスだとしても、永続化先がサーバーなだけで役割としてはあっています。

セッションの概念上の集合として SessionRepository を考えてみましょう。
カンファレンス情報を閲覧するために、各カンファレンス日にあるセッションのコレクションを返す機能と特定のセッション ID に一致するセッションを返す機能が必要です。
セッションの追加と削除は今回のドメインでは必要ないので、
インタフェースはこのようになります。
この SessionRepository を実装した AssetsSessionRepository があるとします。
DetailActivity で AssetsSessionRepository インスンタンスを生成するとAssetsSessionRepository を利用することになってしまいます。
クライアントはグローバルなリポジトリインターフェースを利用するべきですし、実装面からみても、これだとテスト時にリポジトリを差し替えることができなってしまいます。
DetailActivity で AssetsSessionRepository インスンタンスを生成するのではなく、外部から SessionRepository インスタンスを渡すようにし、DetailActivity は SessionRepository の実態が何であるかを知らないようにします。
これを実現するには、SessionRepository のインスタンスをどう UI 側に渡すかという問題を解決しないといけません。
Android では UI 側からグローバルにアクセスできるところとしてよく Application を使います。
管理するものや画面が増えてきたら、自分でやるのは大変なので適宜 Dagger など DI コンテナを導入しましょう。
Application で SessionRepository のインスタンスを保持し、テスト時には差し替えられるようにしておきます。

拡張関数を用意しておくと、Activity から SessionRepository のインスタンスを取得するコードがシンプルになります。
リポジトリーでデータベースアクセスやサーバーアクセスの処理をカプセル化することで、クライアント側、つまり UI 側はオブジェクトが再構成されるときの技術的詳細を気にしなくてよくなりました。
しかし、Android ではデータベースアクセスやサーバーアクセスなどの時間のかかる処理を UI スレッドから呼び出すのはよくありません。
そのためリポジトリーからモデルを取得する処理はバックグランドで行うようにする必要があります。
そこで、Coroutines を使ってこの問題に対応してみたいと思います。
Activity で Coroutines を使うときのパターンをものすごくざっくり紹介しますので、詳しくは本日 15時40分に Room3 である mhidaka のセッションを見てください。
まず Activity があるとして
この Activity に Job を持たせます
次に Activity が CoroutineScope を実装するようにします
CoroutineScope は CoroutineContext プロパティを持ったインタフェースです。 Activity では Dispathers.Main と先ほど持たせた Job から構成される CoroutineContext を返すようにします。
onCreate() で job のインスタンスを生成し、onDestroy() でキャンセルします。
リポジトリーからモデルを取得して TextView にセットしている部分を
launch 内に移動し、
さらにリポジトリーからモデルを取得する部分は withContext() 内に移動します。
withContext() 内の処理はバックグラウンドで実行したいので Dispatchers.Default を指定します。
こうすると、リポジトリーからモデルを取得する部分はバックグラウンドで実行され、その処理が終わると UI スレッドで TextView にセットする部分が実行されます。
さて、リポジトリーからモデルを取得している途中でユーザーが Activity を閉じると、 onDestory() に記述した job の cancel() が呼ばれます。
このとき withContext() から抜けた後の処理、つまり TextView にセットしている部分の処理は実行されません。
しかし、リポジトリーからモデルを取得する処理が終わるまで、つまり SessionRepository の sessionId() メソッドが値を返すまで Coroutine は止まりません。
Coroutine のキャンセル処理は cooperative であり、キャンセルされる側がキャンセルシグナルが来ているかどうかをチェックして、行なっている処理を中止する必要があります。
リポジトリーが内部で行なっているディクスアクセスやサーバーアクセス処理を Coroutine のキャンセルシグナルに応じて中止するには、リポジトリーのメソッドを suspend 関数にします。
suspend 関数内では Coroutine にキャンセルシグナルが来ているかチェックできるため、それに応じて内部で行なっている処理を中止できます。
これで onDestory() で処理がキャンセルされるようになりましたが、まだ問題があります。

onCreate() で処理を開始して onDestory() でキャンセルしているので、画面回転すると以前の処理をキャンセルして再度バックグラウンド処理を行ってしまいます。
この問題に対応するために ViewModel を使いましょう。
ViewModel は Android Architecture Components で提供されている機能です。
画面回転や画面サイズが変わるなど Configuration の変更がおこると Activity が再生成されますが、ViewModel のインスタンスはこの再生成を超えて保持されます。
ViewModel のインスタンスを取得するには ViewModelProviders を利用します。
ViewModelProviders から ViewModel のインスタンスを取得する際、対応するものがない場合は生成され、画面が回転して Activity が再生成されても ViewModel のインスタンスはそのまま保持され、Activity が finish して破棄されると ViewModel の onCleard() が呼ばれて破棄されます。
ViewModel を利用するには、ViewModel または AndroidViewModel を継承したクラスを作ります。
ViewModel のインスタンス生成タイミングはライブラリがハンドリングするため、任意の引数を ViewModel のコンストラクタで渡したいときは、factory を指定します。
DetailViewModel のコンストラクタでセッションの ID とリポジトリーを渡せるように Factory を用意します。
Activity では、用意した Factory を指定して DetailViewModel を取得します。
そして、リポジトリーからセッションを取得する処理を ViewModel に移動します。
ViewModel で Coroutines を使うときのパターンもざっくり紹介します。
Activity のときと同じように ViewModel が CoroutineScope を実装するように、Dispatchers.Main と パラメータで持つ Job から構成される CoroutineContext を返すようにします。
ViewModel 生成時に Job のインスタンスも生成し、onCleared() で job をキャンセルします。
あとは初期化時にリポジトリーからセッションを取得する処理を開始し、取得した結果を保持してリスナーに通知します。
ViewModel のインスタンスは画面回転がおこっても保持されるので、ViewModel で取得処理を行えば画面回転がおこっても処理が継続されます。
リスナーをセットしたときにデータ取得済みならすぐ通知してほしいので、その処理もいれておきます。
しかしこのコードはリスナーの呼び出しが2ヶ所になっていますし、すでにデータを取得済みかどうかを自分で判定してリスナーを呼び出していて複雑です。
データ取得ずみならすぐに通知し、そうでなければ取得されたタイミングで通知したいという場合、LiveData を使うのがぴったりです。
LiveData は一言でいうと Observe できるデータホルダーです。
Lifecycle に応じて自動で Observer を解除するという特徴があります。
ViewModel が LiveData を持ち、Activity がそれを Observe するようにすると、LiveData の値が更新されたときに Activity が onStart() から onStop() の間であれば通知し、そうでなければ次に Activity が onStart() になったときに最新の値を通知します。
DetailViewModel でセッションを LiveData で持つようにするには、セッションを保持するための MutableLiveData インスタンスを生成し、リポジトリーから取得したセッションを MutableLiveData にセットします。
さらに Activity へは、 MutableLiveData ではなく LiveData 型で公開します。
Activity では LiveData の observe() に LifecycleOwner、この場合 this を渡して observe します。
さて、これでモデリング、リポジトリー、Coroutines、ViewModel、LiveData と一通りの実装ができました。
リポジトリーにはすでにカンファレンスの1日目、または2日目のセッション一覧を取得する口があるので、
TimetableViewModel で Coroutines を使ってバックグラウンドでリポジトリーからセッション一覧を取得し、LiveData で保持して Activity や Fragment に通知するようにすると
このようなタイムテーブル画面ができます。
さて、このタイムテーブル画面をよくみると、ウェルカムトークや Codelabs がありません。公募されたセッションしか表示されていないのです。
ここで、ちょっと考えてみましょう。
タイムテーブルを構成する要素として、公募セッション以外にウェルカムトークや Fireside Chat、Codelabs、パーティー、ランチがあります。 これらはセッションでしょうか?

応募されたものではないので、もちろん採択されたセッション一覧にはのっていません。
ぞれぞれを構成するものをまとめてみました。
SessionFormat、Language、カテゴリーは応募されたセッションにしかありません。
一方、日付・時間はすべてにありますし、部屋もランチ以外はあります。
そのため種別を導入すると、データベースで同じテーブルに保存できます。
しかし、保存に適した形式をそのままモデルにしていいのでしょうか?
これはサーバーのレスポンスをそのままモデルにしていいのか?という問題とも共通していますよね。
ウェルカムトークも Session で表現するために、
ウェルカムトークにない属性を Nullable にすると
公募セッションに誤って null がセットされるようなコードを書いてしまうことを防げないし、
モデルの、公募セッションなら絶対 SessionFormat があることを表現できません。
種別に対象外を追加する方法はどうでしょう。
これも公募セッションに誤って対象外がセットされるようなコードを書いてしまうことを防げないし、
公募セッションなら絶対 SessionFormat が対象外でないことを表現できません。
保存形式とモデルの表現が同じとは限らないのです。

ではどうしましょうか...
まずセッションっぽいやつとセッションっぽくないやつを分けてみました。
セッションっぽいやつは詳細画面でアブストラクトなどの情報をみたいやつです。
セッションっぽいやつを Session と定義して、これまで Session にしていた応募されたセッションは PublicSession とし、ぞれぞれ Session を継承するようにしてみました。
Session を sealed class で実装し、PublicSession や WelcomeTalk は data class で実装し、共通するパラメータは Session の方に abstract で定義します。
では、セッションっぽくないやつはどうしましょうか。
パーティーとランチをそれぞれクラスで定義するとして、セッションっぽいやつとの関係をどう表現するのか。
これら全体の共通点は、タイムテーブルを構成する要素であるという点です。すべて日付・時間が決まっています。
そこで、タイムテーブルを構成する要素を表現する TimetableItem を定義し、Party, Lunch, Session がこれを継承するようにしました。
関係を図示すると、このような関係になります。
あとはリポジトリーが TimetableItem の一覧を返すように変更し、モデルに応じて表示を変える処理を UI 側で行えば
タイムテーブルにウェルカムトークや Codelabs、ランチ、Fireside Chat、パーティを表示するようにできます。

しかし、私はこのモデリングに自信がありません。なぜなら TimetableItem という言葉がユビキタス言語にないからです。モデリングはいつもすごく悩みます。Timetableクラスを用意して、そこに PublicSession のコレクションとパーティとランチを持たせたらどうだろう、なども考えました。
ユビキタス言語を探し、クラスや属性名にはユビキタス言語を使いましょう。
DroidKaigi のようなわりとシンプルなドメインでもあいまいな言葉がありました。
あいまいな言葉を見つけたらどれを使うのかチームで決め、常にそれを使うようにしましょう。

ViewModel と LiveData はとても便利です。ぜひ使ってください。

リポジトリーはデータベースアクセスやサーバーアクセスなどをカプセル化し、技術ではなくドメインのモデルに焦点あわせるようにするためのものです。

データベースアクセスやサーバーアクセスがあるため、Android ではリポジトリーからモデルを取得する処理はバックグラウンドで呼び出す必要があります。

この問題を解決するのに Coroutines を使うという方法があります。

さらに、リポジトリーが提供するメソッドを suspend 関数として定義すると、非同期処理が求められていることをクライアントに伝えることができ、Coroutines キャンセル処理に応じてリポジトリー内部で行なっている処理をキャンセルする実装が可能になります。

最後に、

今回 DroidKaigi 2019 を題材にモデリングをしてみましたが、モデリングに正解はないと思っています。
とくにタイムテーブルの部分については、種別を持たせるようなモデリングもありだと思います。

今回の題材がみなさんのモデリングの一助になれば幸いです。

ありがとうございました。