通过使用 FragmentScenario 停止和恢复来测试 androidx.fragment 生命周期,onCreateView() 调用两次,但此错误已在 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 并返回到 {{1 }}),导致视图“重置”到布局中声明的初始状态。我查了一下,发现了一个错误,描述中提到了“onCreateView() 生命周期方法调用了两次”https://issuetracker.google.com/issues/143915710(它也在 https://medium.com/androiddevelopers/fragments-rebuilding-the-internals-61913f8bf48e 中提到)。问题是它已经在 Fragment 1.3.0-alpha08 中修复了,所以它不应该在 1.3.1 中发生。这意味着我的项目配置一定有问题。

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

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 } ,所以它是一个传递依赖,所以我想知道它是否会被解析为小于 8 的 1.3.0-alpha,因此不包含修复。我添加了依赖约束以确保 1.3.1 得到解决

androidx.fragment:fragment

但它没有帮助,所以事实并非如此

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

解决方法

通过强制 Fragment 进入 CREATED 状态,您正在测试它在 detached 设计上确实会破坏其视图层次结构时的行为。

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

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

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

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...