2021年1月8日金曜日

<fragment> と FragmentContainerView では Fragment のライフサイクルメソッドの呼ばれるタイミングが違う

動作確認は androidx.fragment:fragment-ktx:1.2.5 でしています。今後のバージョンで動作が変わる可能性があります。

fragment-ktx:1.2.5 で <fragment> を使うと FragmentContainerView を使うようにメッセージがでます。


このメッセージに従って FragmentContainerView に変えたとして、大体は問題ないと思いますが、問題が起こる場合もあります。

<fragment> と FragmentContainerView で Fragment のライフサイクルメソッドの呼ばれる順番を確認してみます。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val f = supportFragmentManager .findFragmentById(R.id.mainFragment) as MainFragment Log.d("MainActivity", "onCreate : ${f.view}") } } <?xml version="1.0" encoding="utf-8"?> <fragment xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/mainFragment" android:name="yanzm.sample.myapplication.MainFragment" android:layout_width="match_parent" android:layout_height="match_parent" /> class MainFragment : Fragment() { override fun onAttach(context: Context) { super.onAttach(context) Log.d("MainFragment", "onAttach") } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d("MainFragment", "onCreate") } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { Log.d("MainFragment", "onCreateView") return View(inflater.context) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) Log.d("MainFragment", "onActivityCreated") } override fun onStart() { super.onStart() Log.d("MainFragment", "onStart") } override fun onResume() { super.onResume() Log.d("MainFragment", "onResume") } } ↑ の <fragment> の場合のコードを実行すると次のようになります。 D/MainFragment: onAttach D/MainFragment: onCreate D/MainFragment: onCreateView D/MainActivity: onCreate : android.view.View{7009652 V.ED..... ......I. 0,0-0,0 #7f0800ca app:id/mainFragment} D/MainFragment: onActivityCreated D/MainFragment: onStart D/MainFragment: onResume Fragment の onAttach(), onCreate(), onCreateView() が呼ばれた後に Activity の onCreate() が呼ばれています。そのため、Activity の onCreate() で Fragment の View にアクセスすると null ではありません。


では FragmentContainerView に変えて見てみましょう。タグ以外は変更なしです。 <?xml version="1.0" encoding="utf-8"?> <androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/mainFragment" android:name="yanzm.sample.myapplication.MainFragment" android:layout_width="match_parent" android:layout_height="match_parent" /> D/MainFragment: onAttach D/MainFragment: onCreate D/MainActivity: onCreate : null D/MainFragment: onCreateView D/MainFragment: onActivityCreated D/MainFragment: onStart D/MainFragment: onResume なんと Fragment の onCreateView() の呼ばれるタイミングが変わっています。 Activity の onCreate() の後に呼ばれるように変わってしまいました。そのため Activity の onCreate() で Fragment の View にアクセスすると null になっています。

Activity の onCreate() の時点で Fragment の onCreateView() がすでに呼ばれている前提の処理だと、FragmentContainerView に変更したときに意図しない動きになってしまうので注意が必要です。



2020年12月26日土曜日

Dispatchers.Main.immediate ってなに?

Dispatchers.Main は MainCoroutineDispatcher です。 package kotlinx.coroutines ... public actual object Dispatchers { ... @JvmStatic public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher ... } Dispatchers.Main.immediate は MainCoroutineDispatcher に定義されており、Dispatchers.Main.immediate 自体も MainCoroutineDispatcher です。 package kotlinx.coroutines ... public abstract class MainCoroutineDispatcher : CoroutineDispatcher() { ... public abstract val immediate: MainCoroutineDispatcher ... } kotlinx-coroutines-android では HandlerDispatcher が MainCoroutineDispatcher を継承し、 package kotlinx.coroutines.android ... public sealed class HandlerDispatcher : MainCoroutineDispatcher(), Delay { ... public abstract override val immediate: HandlerDispatcher } HandlerContext が HandlerDispatcher を継承しています。 package kotlinx.coroutines.android ... internal class HandlerContext private constructor( private val handler: Handler, private val name: String?, private val invokeImmediately: Boolean ) : HandlerDispatcher(), Delay { ... @Volatile private var _immediate: HandlerContext? = if (invokeImmediately) this else null override val immediate: HandlerContext = _immediate ?: HandlerContext(handler, name, true).also { _immediate = it } override fun isDispatchNeeded(context: CoroutineContext): Boolean { return !invokeImmediately || Looper.myLooper() != handler.looper } ... } HandlerContext では immediate にセットされる HandlerContext は invokeImmediately プロパティが true になる、ということがわかります。

invokeImmediately は isDispatchNeeded() で使われます。isDispatchNeeded() の実装をみると、invokeImmediately が false のときは常に isDispatchNeeded() が true を返すことがわかります。また Looper.myLooper() != handler.looper のときも isDispatchNeeded() が true を返すことがわかります。つまり、invokeImmediately が true かつ Looper.myLooper() == handler.looper のときだけ isDispatchNeeded() は false を返します。

このことから、immediate にセットされる HandlerContext では、Looper.myLooper() が handler.looper と同じだと isDispatchNeeded() が false を返すということがわかります。

isDispatchNeeded() は coroutine を dispatch メソッドで実行するべきかどうか判定するときに呼ばれます。デフォルトは true を返すようになっています。 package kotlinx.coroutines ... public abstract class CoroutineDispatcher : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { ... public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true ... }

つまり、(kotlinx-coroutines-android の) Dispatchers.Main.immediate はすでに UI スレッドにいる場合(現在の Looper.myLooper() が handler.looper と同じ場合)そのまますぐに実行される Dispatcher ということです。

例えば Dispatchers.Main を使った以下のコードだと class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) CoroutineScope(Dispatchers.Main).launch { println("1 : ${Thread.currentThread().name}") } println("2 : ${Thread.currentThread().name}") } } 出力は 2 が 1 より先になります。 I/System.out: 2 : main I/System.out: 1 : main CoroutineScope の dispatcher を Dispatchers.Main.immediate に変えると class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) CoroutineScope(Dispatchers.Main.immediate).launch { println("1 : ${Thread.currentThread().name}") } println("2 : ${Thread.currentThread().name}") } } 1 が先に出力されるようになります。 I/System.out: 1 : main I/System.out: 2 : main UI スレッドにいない場合はすぐには実行されず dispatch されます。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) CoroutineScope(Dispatchers.Default).launch { println("3 : ${Thread.currentThread().name}") CoroutineScope(Dispatchers.Main.immediate).launch { println("1 : ${Thread.currentThread().name}") } println("4 : ${Thread.currentThread().name}") } println("2 : ${Thread.currentThread().name}") } } I/System.out: 2 : main I/System.out: 3 : DefaultDispatcher-worker-2 I/System.out: 4 : DefaultDispatcher-worker-2 I/System.out: 1 : main

viewModelScope, lifecycleScope は dispatcher として Dispatchers.Main.immediate が指定されています。 val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope get() = lifecycle.coroutineScope val Lifecycle.coroutineScope: LifecycleCoroutineScope get() { while (true) { ... val newScope = LifecycleCoroutineScopeImpl( this, SupervisorJob() + Dispatchers.Main.immediate ) ... } } val ViewModel.viewModelScope: CoroutineScope get() { ... return setTagIfAbsent(JOB_KEY, CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)) }



2020年12月24日木曜日

android:fontFamily="sans-serif-medium" の文字の太さを動的に変更する

android:fontFamily に "sans-serif" または "sans-serif-medium"、android:textStyle に "normal" または "bold" を指定したときに生成される Typeface の weight, style, isBold は次のようになります。 <TextView ... android:fontFamily="sans-serif" or "sans-serif-medium" android:textStyle="normal" or "bold" />
fontFamilytextStyleweightstyleisBold
sans-serifnormal4000false
sans-serifbold7001true
sans-serif-mediumnormal5000false
sans-serif-mediumbold8001true

Typeface の style は textStyle の設定が反映されます。style が 1 だと Typeface.BOLD, 2 だと Typeface.ITALIC, 3 だと Typeface.BOLD_ITALIC です。なので style が 1 のとき isBold が true になっています。

"sans-serif-medium" + normal はちょっと太いですが isBold は false です。


android:textStyle の値をプログラムから変更するには TextView.setTypeface() メソッドを使います。このメソッドには引数の異なる2種類があります。 public void setTypeface(@Nullable Typeface tf, @Typeface.Style int style) { if (style > 0) { if (tf == null) { tf = Typeface.defaultFromStyle(style); } else { tf = Typeface.create(tf, style); } setTypeface(tf); // now compute what (if any) algorithmic styling is needed int typefaceStyle = tf != null ? tf.getStyle() : 0; int need = style & ~typefaceStyle; mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0); mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0); } else { mTextPaint.setFakeBoldText(false); mTextPaint.setTextSkewX(0); setTypeface(tf); } } public void setTypeface(@Nullable Typeface tf) { if (mTextPaint.getTypeface() != tf) { mTextPaint.setTypeface(tf); if (mLayout != null) { nullLayouts(); requestLayout(); invalidate(); } } } Typeface.Style を渡さない方では mTextPaint に Typeface をセットして invalidate しています。

Typeface.Style を渡す方では、最終的に setTypeface(@Nullable Typeface tf) を呼び出しています。

style が 0 より大きい場合、つまり bold, italic, bold_italic のいずれかのときは Typeface.defaultFromStyle() または Typeface.create() を使って style に対応する Typeface を生成します。 生成した Typeface が style に対応していない場合だけ FakeBold や TextSkewX がセットされます。

style が 0 (= Typeface.NORMAL)の場合、style に対応する Typeface を生成せず、渡された Typeface をそのままセットします。

これによりどういうことが起こるかと言うと、
"sans-serif-medium" + normal な TextView に setTypeface(tv.typeface, Typeface.BOLD) すると bold になるのに、
"sans-serif-medium" + bold な TextView に setTypeface(tv.typeface, Typeface.NORMAL) すると normal にならない!
のです。 mediumNormalTextView.setTypeface(mediumNormalTextView.typeface, Typeface.BOLD) mediumBoldTextView.setTypeface(mediumBoldTextView.typeface, Typeface.NORMAL)

これは "sans-serif-medium" + bold のときに生成される Typeface 自体が bold になっていて(FakeBold を使わない)、style が 0 (= Typeface.NORMAL)の場合は渡した bold な Typeface をそのままセットされてしまうからです。

これを防ぐには、Typeface.create() で Typeface.NORMAL を適用した Typeface を生成して setTypeface() に渡します。 val tf = Typeface.create(mediumBoldTextView.typeface, Typeface.NORMAL) mediumBoldTextView.setTypeface(tf, Typeface.NORMAL)