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

Posted

技术标签:

【中文标题】通过使用 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 【发布时间】:2021-06-12 05:41:15 【问题描述】:

我正在使用 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),导致视图'重置'到布局中声明的初始状态。我查了一下,发现了一个错误,描述中提到“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 的可见性。手动测试不会重现此问题,它只会影响仪器测试。

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_
    android:layout_
    android:orientation="vertical"
    tools:context=".fragmenttesting.LifecycleBugFragment">

    <TextView
        android:id="@+id/textView"
        android:layout_
        android:layout_
        android:text="default text" />

    <EditText
        android:id="@+id/editText"
        android:layout_
        android:layout_
        android:inputType="text"
        />

    <Button
        android:id="@+id/button"
        android:layout_
        android:layout_
        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 依赖项)?

【问题讨论】:

TextViewtextvisibility 通常是您需要手动保存和恢复到视图的内容;这不是 Views 自动为您做的事情。 请注意,将您的 Fragment 移动到 CREATED 绝对应该在您的 Fragment 上调用 onDestroyView() 并将其移回 RESUMED 然后应该调用 onCreateView()。如果您还没有发生这种情况,那么这表明旧版本的 Fragments 存在问题,而不是新版本。 我没想到视图在一般情况下会保留它们的文本和可见性,我只是使用文本和可见性作为易于检查的测试条件来表明 Fragment 在我预计它不会重新创建时 对于 CREATED 状态,developer.android.com/reference/androidx/lifecycle/… 表示不仅“在 onCreate 调用之后”而且“在 onStop 调用之前”达到了 CREATED。因此,我的不理解是状态不是生命周期中的一个点,而是一个范围,并且 FragmentScenario.moveToState() 移动到该范围的最近边界,因此在 RESUMED->CREATED 的情况下,它将调用 onStop 仅此而已。我知道这是不正确的,并且 moveToState() 应该总是移动到给定状态的“最早”点? 顺便说一句,在不使用 FragmentScenario 查看重新创建的情况下测试 Fragment 被停止和恢复的正确方法是什么?我尝试过 RESUMED->STARTED->RESUMED,但显然它甚至没有调用 onStop 【参考方案1】:

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

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

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

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

【讨论】:

以上是关于通过使用 FragmentScenario 停止和恢复来测试 androidx.fragment 生命周期,onCreateView() 调用了两次,但此错误已在 1.3.1 中修复的主要内容,如果未能解决你的问题,请参考以下文章

通过 C# 代码在远程机器上启动和停止 IIS [关闭]

Centos7.3_x86_64通过systemctl控制tomcat8.0.46启动和停止

Win7怎么使用命令行启动和停止Windows服务程序

停止 onclick 传播“通过”其他元素

使用 Keras 和 sklearn GridSearchCV 交叉验证提前停止

Grails hibernate/Searchable 通过给出以下异常来停止服务器启动