FragmentでViewPagerを使うときにFragmentPagerAdapterに渡すFragmentManagerの注意点
嵌ったのでメモ。
BottomNavigationBar
のタブの Fragment
内で ViewPager
を使っていて、そのタブから別タブに切り替えて再びそのタブに戻ってきたとき、FragmentPagerAdapter#getItem()
が呼ばれず、 ViewPager
内が再読込みされない現象が起きた。
調査の結果、 FragmentPagerAdapter
のインスタンスを作るときに渡す FragmentManager
が FragmentActivity#getSupportFragmentManager()
だと起きる。
そもそも FragmentPagerAdapter
はメモリ上に全てViewPager
の子Fragment
達を保持する。そのためタブを切り替えて戻ってきたときには以前の Fragment
が使い回される。
FragmentPagerAdapter | Android Developers
実際に FragmentPagerAdapter#getItem()
が呼ばれる付近のコードを見てみると確かに Fragment
が保存されている場合は FragmentPagerAdapter#getItem()
が呼ばれない。
public abstract class FragmentPagerAdapter extends PagerAdapter { ... @NonNull public Object instantiateItem(@NonNull ViewGroup container, int position) { if (this.mCurTransaction == null) { this.mCurTransaction = this.mFragmentManager.beginTransaction(); } long itemId = this.getItemId(position); String name = makeFragmentName(container.getId(), itemId); Fragment fragment = this.mFragmentManager.findFragmentByTag(name); if (fragment != null) { this.mCurTransaction.attach(fragment); } else { fragment = this.getItem(position); this.mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId)); } if (fragment != this.mCurrentPrimaryItem) { fragment.setMenuVisibility(false); fragment.setUserVisibleHint(false); } return fragment; } ... }
BottomNavigationView
のタブ切り替えでは Activity
は生きたままなのでその FragmentManager
もViewPager
内の子Fragment
達を保持したままになり、 FragmentPagerAdapter#getItem()
が呼ばれない。
↓の回答を見ると Fragment#getChildFragmentManager()
をFragmentPagerAdapter
に渡すといいとある。
stackoverflow.com
この場合、保持される ViewPager
内の子Fragment
達はその親Fragment
のFragmentManager
で管理される。タブを切り替えたときには通常、OnNavigationItemSelectedListener#onNavigationItemSelected()
で親Fragment
の新しいインスタンスを作るので、FragmentManager
もリセットされ、 FragmentPagerAdapter#getItem()
が呼ばれて再読込みが始まるようになる。
ViewPager
のスワイプが起きたときは 親 Fragment
が生きている限り、 FragmentManager
内で子 Fragment
達が管理されるので、通常通り効率的にスワイプできるようになる。
ちなみにこの問題は子Fragment
をメモリ上に保持しないFragmentStatePagerAdapter
では発生しない。これはページ数が多いなどメモリ保持するには重い場合に使う。
FragmentStatePagerAdapter | Android Developers
↑StackOverflowの回答の中で一番+が多い案は FragmentStatePagerAdapter
を使う案である。この場合はメモリ上に子Fragment
を保持しないのでスワイプ時の挙動が変わる。どちらが良いかはページ数などの状況で決めるといいと思う。
【Android】続・PreferenceFragmentCompatを使った設定画面
以下の記事を昔に書いたが、内容が古くなってきたようなので書き直す。
Androidの設定画面について
- AndroidではAPI1から設定画面を生成してくれる
PreferenceActivity
が用意されている - Android 3.0で各設定画面をFragmentに分けるための
PreferenceFragment
が追加された - Android 4.0では
PreferenceActivity
が拡張され、PreferenceFragment
と組み合わせることでハンドセットでは1画面、タブレットでは2画面の設定画面を自動的に作成できる - API 28で
PreferenceFragment
はdeprecatedになりPreferenceFragmentCompat
が推奨となった。PreferenceActivity
も使う必要はなく、AppCompatActivity
を使用する。
準備
PreferenceFragmentCompat
を使用するためにはサポートライブラリが必要。AndroidX版もあるがひとまず28.0.0で書く。
def supportLibVer = '28.0.0' implementation "com.android.support:appcompat-v7:$supportLibVer" implementation "com.android.support:preference-v7:$supportLibVer"
テレビ用には preference-leanback-v17
があるがここでは説明しない。
設定項目
クラス | 説明 |
---|---|
v7.preference.CheckBoxPreference | オン/オフの設定項目 |
v14.preference.SwitchPreference | オン/オフの設定項目 |
v7.preference.ListPreference | リストの中から1つ選ぶ設定項目 |
v14.preference.MultiSelectListPreference | リストの中から複数を選ぶ設定項目 |
v7.preference.EditTextPreference | 入力からの設定 |
設定画面
Activity
class SettingsActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supportFragmentManager .beginTransaction() .replace(android.R.id.content, SettingsFragment()) .commit() } }
PreferenceFragmentCompat
- onCreatePreferences() で
addPreferencesFromResource()
から設定画面を定義したXMLを読み込む - 必要であれば各設定項目の初期値等を設定する
- XMLで設定した
Preference
を取得するにはandroid:key
を使い、PreferenceFragmentCompat#findPreference()
で行う
class SettingFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.setting_fragment) } }
ヘッダー
<preference-header>
によるヘッダー作成から<Preference android:fragment="">
で指定するようになった。設定するだけでは遷移してくれず、 Activity
側で PreferenceFragmentCompat.OnPreferenceStartFragmentCallback
を実装して onPreferenceStartFragment()
でハンドリングしてあげる必要がある。
旧
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android"> <!-- These settings headers are only used on tablets. --> <header android:fragment="com.phicdy.preferencesample.GeneralPreferenceFragment" android:title="@string/pref_header_general" android:icon="@drawable/ic_info_black_24dp"/> <header android:fragment="com.phicdy.preferencesample.NotificationPreferenceFragment" android:title="@string/pref_header_notifications" android:icon="@drawable/ic_notifications_black_24dp"/> <header android:fragment="com.phicdy.preferencesample.DataSyncPreferenceFragment" android:title="@string/pref_header_data_sync" android:icon="@drawable/ic_sync_black_24dp"/> </preference-headers>
新
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <!-- These settings Preferences are only used on tablets. --> <Preference android:key="aaa" android:fragment="com.phicdy.preferencesample.GeneralPreferenceFragment" android:title="@string/pref_header_general" android:icon="@drawable/ic_info_black_24dp"/> <Preference android:key="bbb" android:fragment="com.phicdy.preferencesample.NotificationPreferenceFragment" android:title="@string/pref_header_notifications" android:icon="@drawable/ic_notifications_black_24dp"/> <Preference android:key="ccc" android:fragment="com.phicdy.preferencesample.DataSyncPreferenceFragment" android:title="@string/pref_header_data_sync" android:icon="@drawable/ic_sync_black_24dp"/> </PreferenceScreen>
設定画面でそんなに需要があるのかわからないが、切替時に処理を入れたり、アニメーションを入れることができる。
class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { // AndroidX版だとNon-Null override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat?, pref: Preference?): Boolean { // AndroidX版だとval fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, pref.fragment, args) で書ける val fragment = when (pref?.fragment) { GeneralPreferenceFragment::class.java.name -> GeneralPreferenceFragment() NotificationPreferenceFragment::class.java.name -> NotificationPreferenceFragment() DataSyncPreferenceFragment::class.java.name -> DataSyncPreferenceFragment() else -> throw InvalidParameterException("Invalid fragment, fragment name is " + pref?.fragment) } fragment.setTargetFragment(caller, 0) supportFragmentManager.beginTransaction() .replace(android.R.id.content, fragment) .addToBackStack(null) .commit() return true } }
PreferenceFragmentCompatから読み込む設定XML
PreferenceFragmentCompat
から読み込むXMLのトップはPreferenceScreen
でなければならない- 各
Preference
にはandroid:key
を設定する。これが各Preference
を特定するIDとなる。 - カテゴリを設定する場合は
PreferenceCategory
を使う。android:key
は必須ではない。 ListPreference
の場合、選択候補に表示されるリスト(android:entries
)と実際の値のリスト(android:entryValues
)を設定する必要がある
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" > <PreferenceCategory android:title="Category Title" > <ListPreference android:title="Title" android:key="@string/key_list_preference" android:entries="@array/entry_sample" android:entryValues="@array/entry_sample_values"/> </PreferenceCategory> </PreferenceScreen>
その他の属性は以下を参照
http://developer.android.com/reference/android/preference/Preference.html
array.xml
ListPreference
で使う値を定義する
<resources> <array name="entry_sample"> <item>@string/entry1</item> <item>@string/entry2</item> </array> <string-array name="entry_sample_values"> <item>1</item> <item>0</item> </string-array> </resources>
初期値設定
XMLで初期値を設定する場合はandroid:defaultValueに設定する。SharedPreference
から保存した値を読み込んでそれに応じて初期値を設定する場合など、コードの中で初期値を設定を行う場合は Preference
によって設定方法が異なる
SwitchPreference
setChecked()
で行う
val switchPref = findPreference(getString(R.string.key_internal_browser)) as SwitchPreference switchPref.setChecked(true)
ListPreference
ListPreference
のentryValues
のstring-array
の各itemが数字だとsetDefaultValue()
が動かなかったのでsetValueIndex()
を使うことで解決entryValues
をinterger-array
にするとNullPointerException
で落ちてしまう
イベントハンドリング
Preference.OnPreferenceChangeListener
と SharedPreferences.OnSharedPreferenceChangeListener
の2種類がある。 Preferennce
はデフォルトで値を SharedPreferences
に保存する。 PreferenceDataStore
を使うことで好きな場所に保存することもできる。
Preference.OnPreferenceChangeListener
は値が変わる前に呼ばれるのでバリデーションなどに使う。 PreferenceDataStore
を使う場合はここでハンドリングして値を保存する。
SharedPreferences.OnSharedPreferenceChangeListener
は SharedPreferences
のみ使用でき、値が保存された後に呼ばれる。
参考
Clean ArchitecutureにおけるKotlin coroutinesの処理と責務分け
自分の中で混乱してきたので整理する。
環境
- Kotlin 1.2.71
- Kotlin Coroutines 0.30.1
本題
以下のようなClean Architecuteベースの設計のアプリがあるとする。
このときKotlin Coroutineを使って非同期処理を行ってUIを更新するとする。 各クラスの責務を分けると以下のようになる。
クラス | 役割 | やらない・知らない | スレッド |
---|---|---|---|
Activity/Fragment | UIを更新する。インターフェースをPresenterに提供し、イベントの処理をPresenterに移譲する。 | UIロジックを持たない。Presenterが何をするかは知らない | UIスレッド |
Presenter | UseCaseに処理を依頼し処理結果を待つ。その結果に応じてUIを更新する | UIの実態を知らない。UseCaseがどう処理を実行しているか知らない | UIスレッド |
UseCase | Repository/Apiからデータを取得して処理を行う | Repository/Apiがどこからデータを取得しているか知らない。 | UIスレッド/ワーカースレッド |
Repository/Api | 外部・内部からデータを取得する | 返したデータがどう使われるかは知らない | ワーカースレッド |
PresenterはUseCaseがどう処理を実行するか知らないし、UseCaseもRepository/Apiがどう処理をするかを知らない。めちゃくちゃ早いCPUで処理をメインスレッド上で5秒以内に実行しているかもしれないし、ワーカースレッドで待ち合わせをして結果を返しているかもしれない。現実的にはUseCaseがRepository/Apiの結果を待ち合わせをし、処理を行って結果を返す。
Activity/FragmentはAndroid SDKに基づくUIスレッドで実行するコルーチンビルダーを呼ぶ。
class MyActivity: AppCompatActivity(), MyView, CoroutineScope { private lateinit var presenter: MyPresenter private val job = Job() override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main override fun onCreate() { presenter = MyPresenter(this) launch(context = coroutineContext) { presenter.onCreate() } } override fun showSuccess() { ... } override fun showFail() { ... } }
Presenterでは呼び出し元のCoroutineScopeで処理を実行し、結果に応じてUIを更新する。UseCaseがどのスレッドで処理をしているかは意識しない。
class MyPresenter(private view: MyView) { suspend fun onCreate() = corountineScope { val result = useCase.fetchAndExecute() if (result.isSuccess()) { view.showSuccess() } else { view.showFail() } } }
UseCaseは中でRepositoryからデータを取得する。Repositoryがどのスレッドで処理をしているかは意識しない。呼び出し側のUseCaseは非同期を意識せずに同期的に書くことができる。返ってきた結果を別メソッドで処理するが、ここでもスレッドを意識しない。実際には withContext()
でワーカースレッド上で処理される。
class MyUseCase { suspend fun fetchAndExecute(): Result = corountineScope { val data = repository.fetchData() return onFetch(data) } private suspend fun onFetch(data: List<Model>): Result = coroutineScope { return@coroutineScope withContext(Dispatchers.Default) { // なんか重たい処理 return@withContext ... } } } }
Repository/Apiでは withContext()
でIOスレッドに切り替えて処理を返す。
class MyRepository { suspend fun fetchData(): List<Model> = coroutineScope { return@coroutineScope withContext(Dispatchers.IO) { // データベース等からなにかを取得する処理 return@withContext ... } } }
Presenterのテスト
テストではUseCaseとViewをモックし、 runBlocking
内で実行することでAndroid SDKからの依存をなくし、同期的に実行する。
@Test fun `when fetch and execute succeeds then show success`() = runblocking { val view = mock(MyView::class.java) val useCase = mock(MyUseCase::class.java) val presneter = MyPresneter(view) `when`(useCase.fetchAndExecute()).thenReturn(...) presenter.useCase = useCase presenter.onCreate() } verify(view, times(1)).showSuccess() }
おわりに
0.26.1の破壊的変更でいろいろ調べていたら時間がかかってしまった。もうそろそろKotlin 1.3が出てCoroutinesが1.0になる。また破壊的変更が来ないことを願う。
Kotlin Fest 2018に参加した
2018/8/25に品川で開催されたKotlin Fest 2018に参加してきました。
飲み物とか食事。パンプディング美味しかった。
mixiさんブースでやってKotlinクイズの景品。7問中3問正解でプランニングポーカーもらいました。
以下参加したセッションの感想
Kotlin で改善する Android アプリの品質
非技術系の上の人にJava -> Kotlin を説得しにくいよねという話から始まり、Effective JavaにならってKotlinでの言語仕様での対応が聞けた。 kotlinにするだけで自然にEffective Javaのエッセンスを導入できる!というのが確かに・・・となってそれだけでもコストをかけて移行する価値があると感じた。Javaのつらみを書かなくていいし・・・。Effective Java読み直そう。
Kotlinアプリのリファクタリングポイント
Refactoring point of Kotlin application
nullが何を表しているかを考えることは重要だなと思った。状態数の話がまさに今直面している問題で、名前をつけたりすることを考えるきっかけになりそう
Kotlin linter
カスタムルールは自然言語処理的なつらさがありそう。PsiViewerでだいぶ楽になる。カスタムフォーマットは更につらそう。android-lintが型が見れて強い。
Kotlin コルーチンを理解しよう
Kotlin コルーチンを 理解しよう - Speaker Deck
コルーチンを全然理解しないまま使っていたので、この発表でかなりすっきりした。実際にはステートマシンに変換されていることや直列・並列実行、待ち合わせなどがどう実現されているかの理解がかなり深まったと思う。
LT大会
3分短いなーと。個人的には5分くらいあってもよかったんじゃないかと。
おわりに
まずKotlinやっている人こんなにいるんだな〜というのが感想だった。Androidだけじゃなくサーバサイドの人も増えてきている印象だった。Java -> Kotlinに変換する話題が多かったのはなんだろう・・・まだ移行期で完全に移行できていない、もしくは上の人を説得できなくてJavaのままのプロダクトがまだまだあるのかなと感じました。 2019はまだ未定とのことですが、どのセッションも面白い話が多くて今日からでも使えそうな考え方や知見が得られたのでぜひ来年もやってほしい!
会場設備もよく、飲み物や軽食が充実していた。これだけの人数だと仕方ないけど立ち見はつらそうだった。 わいわいした雰囲気で楽しかったので、2019があればまた参加しようと思う。
【Android】他のアプリの上に重ねて表示まとめ
Androidの世界では基本的に1アプリのみが前面に出る。
しかし android.permission.SYSTEM_ALERT_WINDOW
を宣言することで他のアプリが前面の場合でも Viewを表示できる
Android 6.0未満
制限はなく、 android.permission.SYSTEM_ALERT_WINDOW
を宣言するだけで利用可能
Android 6.0以上
攻撃者でもこの権限を悪用し、ランサムウェアなどで常に画面表示することができてしまうため、ユーザの明示的な許可が必要になった。 ただしAndroid 6.0.1以上でGoogle Playアプリ6.0.5以上が入っている場合、Google Playからインストールしたアプリは「他のアプリの上に重ねて表示」の権限がデフォルトオンになる。恐らくGoogle的にはGoogle Playにあるアプリは安全という前提がある。 https://stackoverflow.com/questions/36016369/system-alert-window-how-to-get-this-permission-automatically-on-android-6-0-an/36019034
この権限はRuntime Perissionではなく設定の奥にあるのでIntentを飛ばし、 onActivityResult()
でハンドリングする。
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")) startActivityForResult(intent, REQUEST_CODE_OVERLAY_PERMIISSION)
設定が有効かどうかは Settings.canDrawOverlays()
を使う。
Android 8.0以上
targetSdkVersion 26以上にてViewを表示するときの指定でTYPE_APPLICATION_OVERLAY
を使わなければいけなくなった。これによりシステムUIなどの重要なViewの上に描画できなくなり、アプリインストール時に権限の上にViewを表示して隠すなどの攻撃が不可能になった。
https://developer.android.com/about/versions/oreo/android-8.0-changes
Android 8.0では権限取得にバグがあり、変更がすぐに反映されないので監視する。 https://stackoverflow.com/questions/46173460/why-in-android-o-method-settings-candrawoverlays-returns-false-when-user-has
Android 8.1では修正済み。https://issuetracker.google.com/issues/66072795
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O) { val appOpsManager = getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager appOpsManager.startWatchingMode(AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW, packageName, object : AppOpsManager.OnOpChangedListener { override fun onOpChanged(op: String?, packageName: String?) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O) { appOpsManager.stopWatchingMode(this) onOverlayPermissionResult(Settings.canDrawOverlays()) } } }) }
【Android】Google AnalyticsからFirebaseへの乗り換え
Firebaseを使ってみたかったので置き換えてみた。
プロジェクトのbuild.gradle
dependencies { ... classpath 'com.google.gms:google-services:4.0.0' }
アプリのbuild.gradle
dependencies { ... implementation "com.google.firebase:firebase-core:16.0.1" } apply plugin: 'com.google.gms.google-services'
イベントを送るクラスの置き換え
companion object DefaultTracker { - fun setUp(context: Context): Tracker { - val analytics = GoogleAnalytics.getInstance(context) - return analytics.newTracker(R.xml.global_tracker) + fun setUp(context: Context): FirebaseAnalytics { + return FirebaseAnalytics.getInstance(context)
必要ないコードの削除
スクリーンは screen_view
というイベントで勝手に送ってくれるので削除
- tracker.setScreenName(screenName) - tracker.send(HitBuilders.ScreenViewBuilder().build())
クリックイベントの置き換え
Firebase側である程度のイベントを用意してくれている。FirebaseAnalytics.Param.CONTENT_TYPE
や FirebaseAnalytics.Param.VALUE
で色々指定できそうなのでヘルパーメソッドを作った。
fun sendButtonEvent(itemId: String) { sendEvent(itemId, "button", FirebaseAnalytics.Event.SELECT_CONTENT) } fun sendSettingEvent(setting: String, value: String) { sendEvent(setting, "setting", FirebaseAnalytics.Event.SELECT_CONTENT, value) } private fun sendEvent(itemId: String, contentType: String, event: String, value: String = "") { if (BuildConfig.DEBUG) return val params = Bundle().apply { putString(FirebaseAnalytics.Param.ITEM_ID, itemId) putString(FirebaseAnalytics.Param.CONTENT_TYPE, contentType) putString(FirebaseAnalytics.Param.VALUE, value) } tracker.logEvent(event, params) }
デバッグ
コンソールでDebugViewを開く。あとは端末を繋いで adb shell setprop debug.firebase.analytics.app <package_name>
を実行すればデバッグができる。
ErgoDoxのキースイッチを交換する
最近どうもdが押しにくく、ファームウェアをアップデートしても直らなかったため、キースイッチを交換することにしました。
を参考にやりました。
必要なもの
キースイッチ($10+送料$15=$25=2732円) *1ドル約109円として計算
Cherry MX Black Keyswitch - Linear - 5 pack by Cherry
- キースイッチを引き抜く器具 ($6.99+送料$15=$21.99=2403円) *1ドル約109円として計算
MX Switch Top Removal Tool (Set of 2) by Mechanical Keyboards Inc
当初↑の記事を読んで100均のクリップを改造すればOKと書いてあったので、キースイッチを引き抜く器具は買っていなかったが、私には難しすぎたので結局注文。送料がもったいない・・・ 海外発送なので届くまでに約1ヶ月ほどかかるので注意。
手順
1.キーキャップを取る
2 キースイッチを取る
3 ばねとばねについているパーツを取る
4 新しいキースイッチを分解する
5 新しいキースイッチのばねとパーツをつけ、カバーをする
おわりに
100均のクリップを改造したものでは1時間かかってもキースイッチが取れなかったが、器具を買ってやったら1分でできたので素直に器具を買うことをおすすめします・・・