通过使用 FragmentScenario 停止和恢复来测试 androidx.fragment 生命周期,调用了两次 onCreateView() 但此错误已在 1.3.1 中修复

testing androidx.fragment lifecycle by stopping and resuming with FragmentScenario, onCreateView() called twice but this bug is already fixed in 1.3.1

我正在使用 androidx.fragment:fragment-testing 为我的应用程序编写仪器测试。其中一个测试用例是检查当 Fragment 停止和恢复时所有底层逻辑是否正确运行,以模拟应用程序被最小化(主页按钮)并再次返回。这些测试使用 FragmentScenario.moveToState()。首先,我使用 androidx.fragment:fragment-testing:1.2.5 编写测试,它们都通过了。但是当我将 androidx.fragment:fragment-testing 更新为 1.3.1 时,上述测试开始失败。

我已经检查出了什么问题,结果发现 Fragment.onCreateView() 在生命周期更改期间再次被调用,即使它不应该被调用(如果返回到 CREATED 并返回到 RESUMED), 导致视图 'reset' 到布局中声明的初始状态。我查了一下,发现一个错误,描述中提到“onCreateView() 生命周期方法被调用两次”https://issuetracker.google.com/issues/143915710 (it's also mentioned in https://medium.com/androiddevelopers/fragments-rebuilding-the-internals-61913f8bf48e)。问题是它已经在 Fragment 1.3.0-alpha08 中修复,所以它不应该在 1.3.1 中发生。这意味着我的项目配置一定有问题。

这是重现问题的示例代码。它表明视图不保留其文本,也不保留生命周期更改 RESUMED -> CREATED -> RESUMED 的可见性。手动测试不会重现此问题,它只会影响仪器测试。

class LifecycleBugFragment : Fragment() {

    lateinit var textView: TextView
    lateinit var editText: EditText
    lateinit var button: Button

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val view =  inflater.inflate(R.layout.fragment_lifecycle_bug, container, false)
        textView = view.findViewById<TextView>(R.id.textView)
        textView.setOnClickListener { textView.text = "I was clicked" }
        editText = view.findViewById<EditText>(R.id.editText)
        button = view.findViewById<Button>(R.id.button)
        button.setOnClickListener { button.visibility = View.GONE }
        return view
    }
}

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".fragmenttesting.LifecycleBugFragment">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="default text" />

    <EditText
        android:id="@+id/editText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:inputType="text"
        />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="click to hide me"
        />
</LinearLayout>

const val TYPED_TEXT = "some example text"
const val DEFAULT_TEXT = "default text"
const val CLICKED_TEXT = "I was clicked"

class LifecycleBugFragmentTest {

    lateinit var fragmentScenario: FragmentScenario<LifecycleBugFragment>

    @Before
    fun setUp() {
        fragmentScenario = FragmentScenario.launchInContainer(LifecycleBugFragment::class.java)
    }

    @Test
    fun whenTextViewclickedAndFragmentLifecycleStoppedAndResumed_ThenTextViewTextIsStillChanged() {
        onView(withId(R.id.textView)).check(matches(withText(DEFAULT_TEXT)))
        onView(withId(R.id.textView)).perform(click())
        onView(withId(R.id.textView)).check(matches(withText(CLICKED_TEXT)))
        stopAndResumeFragment()
        onView(withId(R.id.textView)).check(matches(withText(CLICKED_TEXT)))
    }

    // this test passes, others fail
    @Test
    fun whenEditTextIsEditedAndFragmentLifecycleStoppedAndResumed_ThenEditTextTextIsStillChanged() {
        onView(withId(R.id.editText)).perform(typeText(TYPED_TEXT))
        stopAndResumeFragment()
        onView(withId(R.id.editText)).check(matches(withText(TYPED_TEXT)))
    }

    @Test
    fun whenButtonIsClickedAndFragmentLifecycleStoppedAndResumed_ThenButtonISStillNotVisible() {
        onView(withId(R.id.button)).perform(click())
        onView(withId(R.id.button)).check(matches(not(isDisplayed())))
        stopAndResumeFragment()
        onView(withId(R.id.button)).check(matches(not(isDisplayed())))
    }

    private fun stopAndResumeFragment() {
        fragmentScenario.moveToState(Lifecycle.State.CREATED)
        fragmentScenario.moveToState(Lifecycle.State.RESUMED)
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.31"
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    testImplementation 'junit:junit:4.13'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    androidTestImplementation "androidx.test:runner:1.3.0"
    androidTestImplementation "androidx.test:core:1.3.0"
    androidTestImplementation "androidx.test.ext:junit:1.1.2"
    androidTestImplementation "androidx.test:rules:1.3.0"

    implementation "androidx.navigation:navigation-fragment-ktx:2.3.4"
    implementation "androidx.navigation:navigation-ui-ktx:2.3.4"
    androidTestImplementation "androidx.navigation:navigation-testing:2.3.4"
    debugImplementation "androidx.fragment:fragment-testing:1.3.1"

    implementation "androidx.navigation:navigation-compose:1.0.0-alpha09"

    // other dependencies unrelated to issue skipped for clarity
}

因为我没有直接声明 androidx.fragment:fragment,所以它作为传递依赖项出现,所以我想知道它是否可以解析为小于 8 的 1.3.0-alpha,因此不包含修复.我添加了依赖约束以确保 1.3.1 得到解决

constraints {
    implementation('androidx.fragment:fragment:1.3.1') {
        because 'avoid bug'
    }
    implementation('androidx.fragment:fragment-ktx:1.3.1') {
        because 'avoid bug'
    }
}

但它没有帮助,所以不是这样

我的代码还有什么问题(很可能 gradle 依赖项)?

通过强制片段进入 CREATED 状态,您正在测试它在 detached 时的行为方式,这在设计上确实破坏了它的视图层次结构。

返回 RESUMED(重新附加片段)时,将重新创建视图并恢复其状态。注意:视图未使用 savedInstanceState 恢复,片段实际上在内部保存已保存的视图状态。

EditText 确实保存了它的状态,这就是它不会失败但 TextViews 和 Buttons 没有保存任何东西的原因。

您可以通过将 android:saveEnabled="true" 添加到其 XML 来强制 TextView 保存其文本,但为了可见性,您需要将状态存储在片段字段中(甚至 save/restore 它通过savedInstanceState) 并在 onViewCreated.

中使用它