Compose Test is Flaky - android

My some view that starts from an activity shows an alert dialog when pressing the back button using the BackHandler.
#Composable
fun PostEditContent(
title: String,
uiState: BasePostEditUiState
) {
var enabledBackHandler by remember { mutableStateOf(true) }
var enabledAlertDialog by remember { mutableStateOf(false) }
val snackBarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
val navigateToBack = { onBackPressedDispatcher?.onBackPressed() }
val forceNavigateToBack = {
coroutineScope.launch {
enabledBackHandler = false
awaitFrame()
navigateToBack()
}
}
if (enabledAlertDialog) {
PostEditAlertDialog(
onDismissRequest = { enabledAlertDialog = false },
onOkClick = { forceNavigateToBack() }
)
}
BackHandler(enabledBackHandler) {
enabledAlertDialog = true
}
...
It's working fine. But in the testing, it sometimes failed because the awaitFrame() is not working properly.
#Suppress("TestFunctionName")
#Composable
private fun PostEditScreenWithHomeBackStack(uiState: PostEditUiState) {
val navController = rememberNavController()
NavHost(navController, startDestination = "home") {
composable(route = "home") {
Text(HOME_STACK_TEXT)
}
composable(route = "edit") {
PostEditScreen(uiState = uiState)
}
}
SideEffect {
navController.navigate("edit")
}
}
...
#Test
fun navigateToUp_WhenSuccessSaving() {
composeTestRule.apply {
setContent {
PostEditScreenWithHomeBackStack(emptyUiState.copy(title = "a", content = "a"))
}
// The save button calls the `forceNavigateToBack()` that in the upper code block
onNodeWithContentDescription("save").performClick()
onNodeWithText(HOME_STACK_TEXT).assertIsDisplayed()
}
}
Is there a better way to show the alert dialog or fix the flaky test? Thanks.

Related

GET request in retrofit in android is not working

I am trying to learn compose and retrofit and for that I am developing a very easy app, fetching jokes from a public API and showing them in a lazy list. But it is not working and I am not able to see any jokes. I am new to Kotlin and Jetpack compose. Please help me debug this.
I have a joke class
data class Joke(
val id: Int,.
val punchline: String,
val setup: String,
val type: String
)
This is the API I am GETing from:
https://official-joke-api.appspot.com/jokes/:id
This is the response:
{"type":"general","setup":"What did the fish say when it hit the wall?","punchline":"Dam.","id":1}
This is the retrofit api service:
const val BASE_URL = "https://official-joke-api.appspot.com/"
interface JokeRepository {
#GET("jokes/{id}")
suspend fun getJoke(#Path("id") id: String ) : Joke
companion object {
var apiService: JokeRepository? = null
fun getInstance(): JokeRepository {
if (apiService == null) {
apiService = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build().create(JokeRepository::class.java)
}
return apiService!!
}
}
}
This is the Jokes view model:
class JokeViewModel : ViewModel() {
private val _jokeList = mutableListOf<Joke>()
var errorMessage by mutableStateOf("")
val jokeList: List<Joke> get() = _jokeList
fun getJokeList() {
viewModelScope.launch {
val apiService = JokeRepository.getInstance()
try {
_jokeList.clear()
// for(i in 1..100) {
// var jokeWithId = apiService.getJoke(i.toString())
// _jokeList.add(jokeWithId)
// Log.d("DEBUGGG", jokeWithId.setup)
// }
var joke = apiService.getJoke("1")
_jokeList.add(joke)
}
catch (e: Exception) {
errorMessage = e.message.toString()
}
}
}
}
This is the Main Activity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val jokeViewModel = JokeViewModel()
super.onCreate(savedInstanceState)
setContent {
HasyamTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
JokeView(jvm = jokeViewModel)
}
}
}
}
}
This is the Joke Component and view
#Composable
fun JokeView(jvm: JokeViewModel) {
LaunchedEffect(Unit, block = {
jvm.getJokeList()
})
Text(text = jvm.errorMessage)
LazyColumn() {
items(jvm.jokeList) {
joke -> JokeComponent(joke)
}
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun JokeComponent(joke: Joke) {
var opened by remember { mutableStateOf(false)}
Column(
modifier = Modifier.padding(15.dp)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { },
elevation = CardDefaults.cardElevation(
defaultElevation = 5.dp
),
onClick = { opened = !opened}
) {
Text(modifier = Modifier.padding(15.dp), text = joke.setup)
}
if (opened) {
Text(modifier = Modifier.padding(15.dp), text = joke.punchline)
}
}
}
Thank you so much
The issue here is that you are not using stateFlow. The screen is not recomposed so your LazyColumn is not recreated with the updated values.
ViewModel
class JokeViewModel : ViewModel() {
var errorMessage by mutableStateOf("")
private val _jokes = MutableStateFlow(emptyList<Joke>())
val jokes = _jokes.asStateFlow()
fun getJokeList() {
viewModelScope.launch {
val apiService = JokeRepository.getInstance()
try {
var jokes = apiService.getJoke("1")
_jokes.update { jokes }
} catch (e: Exception) {
errorMessage = e.message.toString()
}
}
}
}
Joke View
#Composable
fun JokeView(jvm: JokeViewModel) {
val jokes by jvm.jokes.collectAsState()
LaunchedEffect(Unit, block = {
jvm.getJokeList()
})
Text(text = jvm.errorMessage)
LazyColumn {
items(jokes) {
joke -> JokeComponent(joke)
}
}
}
You should read the following documentation about states : https://developer.android.com/jetpack/compose/state

Bottom Sheet not being shown if hiding keyboard

If my keyboard is opened, then on clicking of button if i try to hide my keyboard and change sheetState to show() then my keyboard hides but my sheet is not visible.
Mimicing opening of sheetState on result from api call.
Note - Sheet is shown for first time only
setContent {
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val key = LocalSoftwareKeyboardController.current
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {
repeat(20) {
Text(text = "$it")
}
}
) {
Column {
var otpResponse by remember { mutableStateOf<Boolean?>(null) }
if (otpResponse == false) {
LaunchedEffect(key1 = Unit, block = {
delay(180)
otpResponse = true
})
}
if (otpResponse == true) {
LaunchedEffect(key1 = Unit, block = {
sheetState.show()
})
}
Column {
var string by remember { mutableStateOf("") }
TextField(value = string, onValueChange = { string = it })
Button(onClick = {
key?.hide()
otpResponse = false
}) {
Text(text = "TEST")
}
}
}
}
}
I have modified your code have look which is working without delay
#OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class)
#Preview(showBackground = true)
#Composable
fun DefaultPreview(){
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val key = LocalSoftwareKeyboardController.current
val scope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {
repeat(20) {
Text(text = "$it")
}
}
) {
Column {
Column {
var string by remember { mutableStateOf("") }
TextField(value = string, onValueChange = { string = it })
Button(onClick = {
key?.hide()
scope.launch{
sheetState.show()
}
}) {
Text(text = "TEST")
}
}
}
}
}
thanks for my colleague to fix this

Why won't my Card Swipe right in this Test?

I am testing an app in myTest() and would like to simulate a swipeRight gesture on MyCard() composable.
When I use performTouchInput { swipeRight() } nothing happens. The card stays in the same place.
How can I simulate the swipe right gesture on the card? What am I missing?
Code
class MyTest {
#get:Rule
val composeRule = createComposeRule()
#Before
fun setUp() {
composeRule.setContent {
MyCard()
}
}
#Test
fun myTest() = runTest {
composeRule.onNodeWithTag("DraggableCard")
.performTouchInput { swipeRight() }
}
}
#SuppressLint("UnusedTransitionTargetStateParameter")
#Composable
fun MyCard() {
var swipeState by remember { mutableStateOf(false) }
val transitionState = remember { MutableTransitionState(swipeState).apply { targetState = !swipeState } }
val transition = updateTransition(transitionState, "cardTransition")
val offsetTransition by transition.animateFloat(
label = "cardOffsetTransition",
transitionSpec = { tween(durationMillis = 300) },
targetValueByState = { if (swipeState) 75f else 0f },)
Card(
modifier = Modifier
.testTag("DraggableCard")
.offset { IntOffset(offsetTransition.roundToInt(), 0) }
.pointerInput(Unit) {
detectHorizontalDragGestures { _, dragAmount ->
when {
dragAmount >= 6 -> { swipeState = true }
dragAmount < -6 -> { swipeState = false }
}
}
},
content = { Text(text = "Hello") }
)
}

trying to save value of my edit text field after changing to one activity to 2nd activity and then return to 1st activity in jetpack compose

I am trying to save my edit text value after changing from one activity to another
ex: just like in a register pager (first you have to fill all data in pages and in the end submit.
I am having trouble here is if i want to edit my first page data so i moved from 2nd page to 1st page but my 1st page doesn't show any data
I tried using save state and restore state of navigation but it wont help
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { ScreenNavigation() }
}
}
#Composable
fun Page1(navController: NavHostController) {
var test = remember { mutableStateOf("") }
Column() {
TextField(value = test.value, onValueChange = { test.value = it })
Button(
onClick = {
navController.navigate(ScreenRoute.Page2.name) {
popUpTo(ScreenRoute.Page2.name) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
) {}
Text(text = "Page1")
TextField(
value = test.value,
onValueChange = { test.value = it },
)
}
}
#Composable
fun Page2(navController: NavHostController) {
var test = remember { mutableStateOf("") }
Column() {
TextField(value = test.value, onValueChange = { test.value = it })
Button(
onClick = {
navController.navigate(ScreenRoute.Page1.name) {
popUpTo(ScreenRoute.Page1.name) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
) {}
Text(text = "Page2")
}
}
// code for navigation
#Composable
fun ScreenNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = ScreenRoute.Page1.name) {
composable(ScreenRoute.Page1.name) { Page1(navController) }
composable(ScreenRoute.Page2.name) { Page2(navController) }
}
}
// code for screen route
enum class ScreenRoute {
Page1,
Page2,
}
Have both variables outside the functions and above class like this:
var test1 = mutableStateOf("")
var test2 = mutableStateOf("")
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { ScreenNavigation() }
}
}
#Composable
fun Page1(navController: NavHostController) {
Column() {
TextField(value = test1.value, onValueChange = { test1.value = it })
Button(
onClick = {
navController.navigate(ScreenRoute.Page2.name) {
popUpTo(ScreenRoute.Page2.name) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
) {}
Text(text = "Page1")
TextField(
value = test1.value,
onValueChange = { test1.value = it },
)
}
}
#Composable
fun Page2(navController: NavHostController) {
Column() {
TextField(value = test2.value, onValueChange = { test2.value = it })
Button(
onClick = {
navController.navigate(ScreenRoute.Page1.name) {
popUpTo(ScreenRoute.Page1.name) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
) {}
Text(text = "Page2")
}
}
// code for navigation
#Composable
fun ScreenNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = ScreenRoute.Page1.name) {
composable(ScreenRoute.Page1.name) { Page1(navController) }
composable(ScreenRoute.Page2.name) { Page2(navController) }
}
}
// code for screen route
enum class ScreenRoute {
Page1,
Page2,
}
Have both variables outside the functions
Use rememberSaveable
remembersaveable save the state of value
val numberOfFamilyMember = rememberSaveable {
mutableStateOf("")
}
Try using rememberSaveable like this
#Composable
fun Page1(navController: NavHostController) {
var test by rememberSaveable { mutableStateOf("") }
Column() {
TextField(value = test.value, onValueChange = { test.value = it })
Button(
onClick = {
navController.navigate(ScreenRoute.Page2.name) {
popUpTo(ScreenRoute.Page2.name) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
) {}
Text(text = "Page1")
TextField(
value = test.value,
onValueChange = { test.value = it },
)
}
}
Using variables outside Composable seems to be a side effect.
rememberSaveable should survive the activity change according to docs

How to make ModalBottomSheetLayout to use the newest state for sheetContent?

I tried to show a timestamp (or any strings that can be updated in general) inside a sheetContent of the ModalBottomSheetLayout that is shown by clicking on a floating action button. However, that timestamp is only updated once: the first time that the sheet is shown. If I close the sheet (through onSave in the code) and open it again, the timestamp stays the same instead of showing the newest timestamp. I think I maybe missing "remember" or "mutableState" somehow but I am not able to get it to work.
class MainActivity : ComponentActivity() {
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AskQuestionTheme {
val modalBottomSheetState =
rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
Scaffold(
floatingActionButton = {
FloatingActionButton(
onClick = { scope.launch { modalBottomSheetState.show() } }) {
Icon(
Icons.Rounded.Add,
contentDescription = "add"
)
}
}) {
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {
var t = Date().toString()
MySheet(
str = t,
onSave = {
scope.launch {
modalBottomSheetState.hide()
}
},
onCancel = { scope.launch { modalBottomSheetState.hide() } })
}
) {
Text("hello")
}
}
}
}
}
}
#Composable
fun MySheet(str: String, onSave: () -> Unit, onCancel: () -> Unit) {
Column {
Box(modifier = Modifier.fillMaxWidth()) {
Icon(
Icons.Rounded.Close,
contentDescription = "edit",
modifier = Modifier.align(Alignment.CenterStart).clickable { onCancel() })
Button(onClick = onSave, modifier = Modifier.align(Alignment.CenterEnd)) {
Text("save")
}
}
Text(str)
Spacer(Modifier.height(100.dp))
}
}
There are two ways for me. Up to you which select)
First variant. Added a new property var t by remember { mutableStateOf(Date().toString()) } and change its before showing ModalBottomSheetLayout. It is an update code
t = Date().toString().
class MainActivity : ComponentActivity() {
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AskQuestionTheme {
val modalBottomSheetState =
rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
var t by remember { mutableStateOf(Date().toString()) }
Scaffold(
floatingActionButton = {
FloatingActionButton(
onClick = {
scope.launch {
t = Date().toString()
modalBottomSheetState.show()
}
}) {
Icon(
Icons.Rounded.Add,
contentDescription = "add"
)
}
}) {
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {
MySheet(
str = t,
onSave = {
scope.launch {
modalBottomSheetState.hide()
}
},
onCancel = { scope.launch { modalBottomSheetState.hide() } })
}
) {
Text("hello")
}
}
}
}
}
}
Second variant. Now you can see LaunchedEffect. Code inside this block is triggered when this value modalBottomSheetState.direction changes. Inside this block we update a date.
class MainActivity : ComponentActivity() {
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AskQuestionTheme {
val modalBottomSheetState =
rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
Scaffold(
floatingActionButton = {
FloatingActionButton(
onClick = { scope.launch { modalBottomSheetState.show() } }) {
Icon(
Icons.Rounded.Add,
contentDescription = "add"
)
}
}) {
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {
var t by remember { mutableStateOf(Date().toString()) }
LaunchedEffect(modalBottomSheetState.direction) {
if (modalBottomSheetState.direction == -1f) {
t = Date().toString()
}
}
MySheet(
str = t,
onSave = {
scope.launch {
modalBottomSheetState.hide()
}
},
onCancel = { scope.launch { modalBottomSheetState.hide() } })
}
) {
Text("hello")
}
}
}
}
}
}

Categories

Resources