更新TextField引发ComposeNotIdleException。

huangapple go评论53阅读模式
英文:

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 = &quot;TextField&quot; },
        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 = &quot;initialText&quot;)
        }
    }

    fun updateTextState(newText: String) {
        _textFieldState.value = _textFieldState.value.copy(text = newText)
    }
}

data class TextFieldState(
    val text: String = &quot;&quot;,
    val showError: Boolean = false,
)

Test code:

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @get:Rule
    val composeRule = createAndroidComposeRule&lt;ComponentActivity&gt;()

    @Test
    fun testIdlingTextField() {
        composeRule.setContent { ScreenContent(MyViewModel()) }
        composeRule.onNodeWithContentDescription(&quot;TextField&quot;).performTextInput(&quot;newText&quot;)
        composeRule.onNodeWithText(&quot;newText&quot;).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

在进一步调查问题后,发现问题是视图模型的初始化。只需在composeRulesetContent之外初始化视图模型:

@Test
fun testIdlingTextField() {
    val viewModel: MyViewModel = MyViewModel()
    composeRule.setContent { ScreenContent(viewModel) }
    composeRule.onNodeWithContentDescription("TextField").performTextInput("newText")
    composeRule.onNodeWithText("newText").assertIsDisplayed()
}

Explanation: 根据官方文档,Compose通过检查哪些可组合读取了该状态来跟踪状态更改。当视图模型在setContent可组合内部创建时,它读取了MyViewModelinit块内的_textFieldState。因此,当performTextInput更改_textFieldState时,setContent将被重新组合,并且会再次创建一个新的视图模型,导致无限重新组合。

PS: 解决这个问题的其他方法包括:

  • MyViewModelinit块内避免读取状态:_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(&quot;TextField&quot;).performTextInput(&quot;newText&quot;)
    composeRule.onNodeWithText(&quot;newText&quot;).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 = &quot;initialText&quot;)
  • keep the view model initialization inside setContent and use the remember composable to have the same view model across recompositions

huangapple
  • 本文由 发表于 2023年2月26日 19:30:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/75571664.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定