英文:
Updating TextField throws ComposeNotIdleException
问题
调用AndroidComposeTestRule.performTextInput
时,针对具有初始状态的TextField会引发ComposeNotIdleException
异常。复制问题的代码如下:
// 生产代码
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = MyViewModel()
setContent {
MyAppTheme {
ScreenContent(viewModel)
}
}
}
}
@Composable
fun ScreenContent(viewModel: MyViewModel) {
TextField(
modifier = Modifier.semantics { contentDescription = "TextField" },
value = viewModel.textFieldState.value.text,
onValueChange = { viewModel.updateTextState(it) },
)
}
class MyViewModel : ViewModel() {
private val _textFieldState = mutableStateOf(TextFieldState())
val textFieldState = _textFieldState
init {
viewModelScope.launch {
_textFieldState.value = _textFieldState.value.copy(text = "initialText")
}
}
fun updateTextState(newText: String) {
_textFieldState.value = _textFieldState.value.copy(text = newText)
}
}
data class TextFieldState(
val text: String = "",
val showError: Boolean = false,
)
测试代码如下:
// 测试代码
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@get:Rule
val composeRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun testIdlingTextField() {
composeRule.setContent { ScreenContent(MyViewModel()) }
composeRule.onNodeWithContentDescription("TextField").performTextInput("newText")
composeRule.onNodeWithText("newText").assertIsDisplayed()
}
}
当我只使用简单的字符串而不是TextFieldState
时,上述测试通过。如果有人能够解释为什么会发生这种情况,我将不胜感激。
英文:
When calling AndroidComposeTestRule.performTextInput
on a TextField with initial state, ComposeNotIdleException
is thrown. Code to replicate the issue:
Production code:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = MyViewModel()
setContent {
MyAppTheme {
ScreenContent(viewModel)
}
}
}
}
@Composable
fun ScreenContent(viewModel: MyViewModel) {
TextField(
modifier = Modifier.semantics { contentDescription = "TextField" },
value = viewModel.textFieldState.value.text,
onValueChange = { viewModel.updateTextState(it) },
)
}
class MyViewModel : ViewModel() {
private val _textFieldState = mutableStateOf(TextFieldState())
val textFieldState = _textFieldState
init {
viewModelScope.launch {
_textFieldState.value = _textFieldState.value.copy(text = "initialText")
}
}
fun updateTextState(newText: String) {
_textFieldState.value = _textFieldState.value.copy(text = newText)
}
}
data class TextFieldState(
val text: String = "",
val showError: Boolean = false,
)
Test code:
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@get:Rule
val composeRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun testIdlingTextField() {
composeRule.setContent { ScreenContent(MyViewModel()) }
composeRule.onNodeWithContentDescription("TextField").performTextInput("newText")
composeRule.onNodeWithText("newText").assertIsDisplayed()
}
}
The above test passes when I'm just using a simple String instead of TextFieldState
. I would really appreciate it if someone could give me an explanation of why this is happening.
答案1
得分: 1
在进一步调查问题后,发现问题是视图模型的初始化。只需在composeRule
的setContent
之外初始化视图模型:
@Test
fun testIdlingTextField() {
val viewModel: MyViewModel = MyViewModel()
composeRule.setContent { ScreenContent(viewModel) }
composeRule.onNodeWithContentDescription("TextField").performTextInput("newText")
composeRule.onNodeWithText("newText").assertIsDisplayed()
}
Explanation: 根据官方文档,Compose通过检查哪些可组合读取了该状态来跟踪状态更改。当视图模型在setContent
可组合内部创建时,它读取了MyViewModel
的init
块内的_textFieldState
。因此,当performTextInput
更改_textFieldState
时,setContent
将被重新组合,并且会再次创建一个新的视图模型,导致无限重新组合。
PS: 解决这个问题的其他方法包括:
- 在
MyViewModel
的init
块内避免读取状态:_textFieldState.value = TextFieldState(text = "initialText")
- 将视图模型初始化保留在
setContent
内部,并使用remember
可组合来在重新组合之间保持相同的视图模型。
英文:
After investigating the problem further, it turned out that the issue was the initialization of the view model. Just initialize the view model outside composeRule's setContent
:
@Test
fun testIdlingTextField() {
val viewModel: MyViewModel = MyViewModel()
composeRule.setContent { ScreenContent(viewModel) }
composeRule.onNodeWithContentDescription("TextField").performTextInput("newText")
composeRule.onNodeWithText("newText").assertIsDisplayed()
}
Explanation: according to the official doc, Compose keeps track of state changes by checking which composables read that state. When the view model is created inside the setContent
composable, it reads _textFieldState
(inside the init
block of MyViewModel
). So when performTextInput
changes the _textFieldState
, the setContent
gets recomposed and a new view model gets created again, causing the infinite recomposition.
PS: Other solutions to solve this problem are:
- inside
init
block of MyViewModel, avoid reading the state:_textFieldState.value = TextFieldState(text = "initialText")
- keep the view model initialization inside
setContent
and use theremember
composable to have the same view model across recompositions
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论