通过使用FragmentScenario停止和恢复来测试androidx.fragment生命周期,onCreateView()调用了两次,但该错误已在1.3.1中修复
我正在使用androidx为我的应用程序编写插入指令的测试。fragment:fragment testing。其中一个测试用例是检查当片段停止并恢复时,所有底层逻辑是否正常运行,以模拟应用程序被最小化(home按钮)并再次返回。这些测试利用了通过使用FragmentScenario停止和恢复来测试androidx.fragment生命周期,onCreateView()调用了两次,但该错误已在1.3.1中修复,android,android-fragments,fragment-lifecycle,instrumented-test,android-fragmentscenario,Android,Android Fragments,Fragment Lifecycle,Instrumented Test,Android Fragmentscenario,我正在使用androidx为我的应用程序编写插入指令的测试。fragment:fragment testing。其中一个测试用例是检查当片段停止并恢复时,所有底层逻辑是否正常运行,以模拟应用程序被最小化(home按钮)并再次返回。这些测试利用了碎片场景。moveToState()。首先,我使用androidx.fragment:fragment testing:1.2.5编写了我的测试,它们都通过了。但是当我将androidx.fragment:fragment testing更新为1.3.1时
碎片场景。moveToState()
。首先,我使用androidx.fragment:fragment testing:1.2.5编写了我的测试,它们都通过了。但是当我将androidx.fragment:fragment testing
更新为1.3.1
时,前面提到的测试开始失败
我检查了错误,结果是在生命周期更改期间再次调用了Fragment.onCreateView()
,即使它不应该调用(在返回到CREATED
和resume
的情况下),导致视图“重置”到布局中声明的初始状态。我查了一下,发现了一个bug,描述中提到“onCreateView()生命周期方法被调用两次”(在中也提到)。问题是它已经在片段1.3.0-alpha08中修复,所以它不应该出现在1.3.1中。这意味着我的项目配置一定有问题
下面是一个重现问题的示例代码。它表明视图在生命周期更改恢复->创建->恢复时既不保留文本也不保持可见性。手动测试不会重现此问题,它只影响仪表化测试
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
}
}
因为我没有直接声明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依赖项)还有什么问题吗?通过强制片段进入
已创建的
状态,您正在测试它的行为,而该状态的设计确实破坏了它的视图层次结构
当移回恢复时
(片段重新附着)视图被重新创建,其状态被恢复。注意:不使用savedInstanceState
还原视图,片段实际上在内部保存保存的视图状态
EditText会保存其状态,这就是为什么它不会失败,但TextView和按钮不会保存任何内容
您可以通过将
android:saveEnabled=“true”
添加到文本视图的XML中来强制文本视图保存文本,但为了便于查看,您需要将状态存储在片段字段中(甚至通过savedInstanceState
保存/恢复状态)并在视图创建的onViewCreated
中使用它,方法是将片段强制进入CREATED
状态,以测试它在设计时破坏其视图层次结构时的行为
当移回恢复时
(片段重新附着)视图被重新创建,其状态被恢复。注意:不使用savedInstanceState
还原视图,片段实际上在内部保存保存的视图状态
EditText会保存其状态,这就是为什么它不会失败,但TextView和按钮不会保存任何内容
您可以通过将
android:saveEnabled=“true”
添加到文本视图的XML中来强制文本视图保存文本,但为了便于查看,您需要将状态存储在片段字段中(甚至通过savedInstanceState
保存/恢复状态)并在创建的视图中使用它,通常需要手动保存并恢复到视图中;这并不是视图自动为您做的事情。请注意,将片段移动到已创建的绝对应该调用片段上的onDestroyView()
,然后将其移动回已恢复的则应该调用onCreateView()
。如果您还没有遇到这种情况,那么这表明旧版本的片段存在问题,而不是新版本。我不希望视图在一般情况下保留其文本和可见性,我刚刚使用了文本和可视性这两种易于检查的测试条件来指示片段是在我期望它不是CREATED状态时被重新创建的,表示CREATED不仅在“onCreate调用”之后到达,而且在“onStop调用”之前到达。因此,我不理解的是,状态不是生命周期中的一个点,而是一个范围和碎片场景。moveToState()移动到该范围的最近边界,因此在Resume->CREATED的情况下,它将在顶部调用,而不再调用。我知道这是不正确的,moveToState()应该总是移动到给定状态的“最早”点?顺便问一下,使用FragmentScenario测试停止和恢复片段而不重新创建视图的正确方法是什么?我尝试过继续->开始->继续,但它显然甚至没有调用桌面TextView
的text
,而visibility
通常需要手动保存并恢复到视图中;这并不是视图自动为您做的事情。请注意,将片段移动到已创建的绝对应该调用片段上的onDestroyView()
,然后将其移动回已恢复的则应该调用onCreateView()
。如果您还没有遇到这种情况,那么这表明旧版本的片段存在问题,而不是新版本。我不希望视图在一般情况下保留其文本和可见性,我刚刚使用了文本和可视性这两种易于检查的测试条件来指示片段是在我期望它不是CREATED状态时被重新创建的,表示CREATED不仅在“onCreate调用”之后到达,而且在“onStop调用”之前到达。因此,我的理解是,状态不是生命周期中的一个点,而是
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
}
constraints {
implementation('androidx.fragment:fragment:1.3.1') {
because 'avoid bug'
}
implementation('androidx.fragment:fragment-ktx:1.3.1') {
because 'avoid bug'
}
}