2019年8月6日火曜日

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

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


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



AppCompat の設定と解説

alertDialogStyle に指定されている AlertDialog.AppCompat をたどっていくと、
  1. <style name="Base.V7.Theme.AppCompat" parent="Platform.AppCompat">  
  2.     …  
  3.     <item name="alertDialogStyle">@style/AlertDialog.AppCompat</item>  
  4. </style>  
  1. <style name="AlertDialog.AppCompat" parent="Base.AlertDialog.AppCompat"/>  
  2.   
  3. <style name="Base.AlertDialog.AppCompat" parent="android:Widget">  
  4.     <item name="android:layout">@layout/abc_alert_dialog_material</item>  
  5.     <item name="listLayout">@layout/abc_select_dialog_material</item>  
  6.     <item name="listItemLayout">@layout/select_dialog_item_material</item>  
  7.     <item name="multiChoiceItemLayout">@layout/select_dialog_multichoice_material</item>  
  8.     <item name="singleChoiceItemLayout">@layout/select_dialog_singlechoice_material</item>  
  9.     <item name="buttonIconDimen">@dimen/abc_alert_dialog_button_dimen</item>  
  10. </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
  1. <ScrollView …>  
  2.   
  3.     <android.support.v7.widget.ButtonBarLayout  
  4.         …">  
  5.   
  6.         …  
  7.   
  8.         <Button  
  9.             android:id="@android:id/button2"  
  10.             style="?attr/buttonBarNegativeButtonStyle"  
  11.             android:layout_width="wrap_content"  
  12.             android:layout_height="wrap_content"/>  
  13.   
  14.         <Button  
  15.             android:id="@android:id/button1"  
  16.             style="?attr/buttonBarPositiveButtonStyle"  
  17.             android:layout_width="wrap_content"  
  18.             android:layout_height="wrap_content"/>  
  19.   
  20.     </android.support.v7.widget.ButtonBarLayout>  
  21.   
  22. </ScrollView>  
buttonBarNegativeButtonStyle には buttonBarButtonStyle が指定されており、buttonBarButtonStyle には Widget.AppCompat.Button.ButtonBar.AlertDialog が指定されています。
  1. <style name="Base.V7.Theme.AppCompat" parent="Platform.AppCompat">  
  2.   
  3.     …  
  4.   
  5.     <item name="buttonBarButtonStyle">@style/Widget.AppCompat.Button.ButtonBar.AlertDialog</item>  
  6.     <item name="buttonBarPositiveButtonStyle">?attr/buttonBarButtonStyle</item>  
  7.     <item name="buttonBarNegativeButtonStyle">?attr/buttonBarButtonStyle</item>  
  8.     <item name="buttonBarNeutralButtonStyle">?attr/buttonBarButtonStyle</item>  
  9.   
  10. </style>  
Widget.AppCompat.Button.ButtonBar.AlertDialog は最終的に Widget.AppCompat.Button.Borderless.Colored を継承しており、これによりボタンの文字が colorAccent 色になっています。
  1. <style name="Widget.AppCompat.Button.ButtonBar.AlertDialog" parent="Base.Widget.AppCompat.Button.ButtonBar.AlertDialog"/>  
  2.   
  3. <style name="Base.Widget.AppCompat.Button.ButtonBar.AlertDialog" parent="Widget.AppCompat.Button.Borderless.Colored">  
  4.     <item name="android:minWidth">64dp</item>  
  5.     <item name="android:minHeight">@dimen/abc_alert_dialog_button_bar_height</item>  
  6. </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 を追加した)
  1. class MainViewModel @Inject constructor(  
  2.     private val api: MyApi,  
  3.     private val handle: SavedStateHandle  
  4. ) : ViewModel() {  
  5.     ...  
  6. }  
MyApi は Dagger で管理しています。
  1. @Module  
  2. class AppModule {  
  3.   
  4.     @Singleton  
  5.     @Provides  
  6.     fun provideApi(): MyApi {  
  7.         return ...  
  8.     }  
  9. }  
MainViewModel は ViewModel の Map Multibindings の1要素になっています。
  1. @Module  
  2. interface BindModule {  
  3.   
  4.     @Binds  
  5.     @IntoMap  
  6.     @ViewModelKey(MainViewModel::class)  
  7.     fun bindMainViewModel(viewModel: MainViewModel): ViewModel  
  8. }  

1.

SavedStateHandle をオブジェクトグラフに含む Subcomponent を用意します。
  1. @Module(subcomponents = [SavedStateViewModelComponent::class])  
  2. class SavedStateViewModelComponentModule  
  3.   
  4. @Subcomponent  
  5. interface SavedStateViewModelComponent {  
  6.   
  7.     @Subcomponent.Factory  
  8.     interface Factory {  
  9.         fun create(  
  10.             @BindsInstance handle: SavedStateHandle  
  11.         ): SavedStateViewModelComponent  
  12.     }  
  13.   
  14.     fun providers(): Map<Class<out ViewModel>, Provider<ViewModel>>  
  15. }  

2.

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

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

3.

SavedStateRegistryOwner と Bundle? をオブジェクトグラフに含む Subcomponent を用意します。
SavedStateComponent の modules に SavedStateViewModelComponentModule を指定します。
  1. @Module(subcomponents = [SavedStateComponent::class])  
  2. class SavedStateComponentModule  
  3.   
  4. @Subcomponent(modules = [SavedStateViewModelComponentModule::class])  
  5. interface SavedStateComponent {  
  6.   
  7.     @Subcomponent.Factory  
  8.     interface Factory {  
  9.         fun create(  
  10.             @BindsInstance owner: SavedStateRegistryOwner,  
  11.             @BindsInstance defaultArgs: Bundle?  
  12.         ): SavedStateComponent  
  13.     }  
  14.   
  15.     fun viewModelFactory(): SavedStateViewModelFactory  
  16. }  

4.

AppComponent の modules に SavedStateComponentModule を指定します。
  1. @Singleton  
  2. @Component(  
  3.     modules = [  
  4.         AppModule::class,  
  5.         BindModule::class,  
  6.         SavedStateComponentModule::class  
  7.     ]  
  8. )  
  9. interface AppComponent {  
  10.   
  11.     fun savedStateComponentFactory(): SavedStateComponent.Factory  
  12. }  

5.

SavedStateComponentから取得したFactoryを使います。
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     private val viewModel: MainViewModel by viewModels {  
  4.         (application as MyApplication).appComponent  
  5.             .savedStateComponentFactory()  
  6.             .create(thisnull)  
  7.             .viewModelFactory()  
  8.     }  
  9.   
  10.     ...  
  11. }