matching arguments not matching arguments[1] and arguements[2] matching are swapped by mockk - kotlin

I want to test that on click, user is being navigated to the correct screen.
I can see the correct button is clicked after I do performClick() in my tests but it is saying the call happened with different arguments. I can't figure out what is causing the error.
#Destination
#Composable
fun UserScreen(
navigator: DestinationsNavigator,
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(8.dp)
) {
Button( onClick = {
navigator.navigate(DataCreateDestination(createType = "user"))
}){
Text(
text = "Create User"
)
}
Button( onClick = {
navigator.navigate(DataCreateDestination(createType = "group"))
}) {
Text(
text = "Create Group"
)
}
}
}
the test now
#RunWith(AndroidJUnit4::class)
#HiltAndroidTest
class UserScreenTest {
#get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
private val mockNavigation = mockk< DestinationsNavigator>(relaxed = true)
private fun hasText(#StringRes strResId: Int) = hasText(rule.activity.getString(strResId))
#Before
fun init(){
rule.waitForIdle()
}
#Test
fun testNavigateToCreateUserScreen() {
coEvery {
mockNavigation.navigate(DataCreateDestination(createType="user"))
} returns Unit
rule.setContent {
UserScreen(navigator = mockNavigation)
}
rule.onNode(hasText("Create User")).assertIsDisplayed().assertHasClickAction().performClick()
coVerify (exactly = 1){ mockNavigation.navigate(DataCreateDestination(createType="user"))}
}
}
the test fails with the following message:
java.lang.AssertionError: Verification failed: call 1 of 1: DestinationsNavigator(#2).navigate(eq(DataCreateDestination$invoke$2#c570f86), eq(false), eq(lambda {}))). Only one matching call to DestinationsNavigator(#2)/navigate(Direction, Boolean, Function1) happened, but arguments are not matching:
[0]: argument: DataCreateDestination$invoke$2#aebacc4, matcher: eq(DataCreateDestination$invoke$2#c570f86), result: -
[1]: argument: false, matcher: eq(false), result: +
[2]: argument: lambda {}, matcher: eq(lambda {}), result: +

Related

How to override SnackbarData in Jetpack Compose?

I need a custom parameter in the snackbar, but showSnackbar only allows passing three parameters (message, actionLabel and duration). I need to pass an Enum to decide which status to show.
#Composable
fun BaseScreen(
viewModel: BaseViewModel?,
content: #Composable () -> Unit
) {
val scaffoldState: ScaffoldState = rememberScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope: CoroutineScope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
snackbarHost = {
SnackbarHost(
hostState = snackbarHostState,
snackbar = { snackbarData ->
CustomSnackbar(
message = snackbarData.message,
// I can't get custom parameter
status = snackbarData.status
)
}
)
},
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
viewModel?.showSnackbar = { message ->
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = message,
// I can't pass custom parameter
status = SnackbarStatusEnum.DANGER
)
}
}
content()
}
}
}
You can use something different.
Use a variable(state) to set the status and use a condition inside the Snackbar to change backgroundColor and actions.
Something like:
Scaffold(
scaffoldState = scaffoldState,
snackbarHost = {
SnackbarHost(it) { data ->
// custom snackbar with the custom colors
Snackbar(
backgroundColor = if (status) Color.Red else Color.Yellow,
//contentColor = ...,
snackbarData = data
)
}
},
)

My response data return " kotlinx.coroutines.flow.SafeFlow#1493a74"

I am trying to do dictionary app using kotlin language. I built the project with mvvm and clean architecture. I have been trying to pull vocabulary information from the internet using jsoap. I am using flow for data. I couldnt find where the issiue is. Normally, the words should appear on the screen or I should be able to see the data when I println on the console.But I can't see it on the screen or on the console, probably because the data coming from the internet is as follows.
kotlinx.coroutines.flow.SafeFlow#1493a74
I am sharing my codes below
ExtractedData
data class ExtractedData(
var id :Int = 0,
var word:String = "",
var meaning :String = ""
)
I created ExtractedData class to represent vocabulary or word data from internet
WordInfoRepositoryImpl
class WordInfoRepositoryImpl #Inject constructor(
private val api:DictionaryApi
) : WordInfoRepository {
//get words with meanings on the internet using jsoap
override fun getEventsList(): Flow<Resource<MutableList<ExtractedData>>> = flow {
emit(Resource.Loading())
val listData = mutableListOf<ExtractedData>()
try {
val url = "https://ielts.com.au/australia/prepare/article-100-new-english-words-and-phrases-updated-2020"
val doc = withContext(Dispatchers.IO){
Jsoup.connect(url).get()//-->Here it gives the following warning even though I have it in withContext `Inappropriate blocking method call`
}
val table = doc.select("table")
val rows = table.select("tr")
val eventsSize = rows.size
for (i in 1 until eventsSize) {
val row = rows[i]
val cols = row.select("td")
val word = cols[0].text()
val meaning = cols[1].text()
listData.add(ExtractedData(i,word,meaning))
}
}
catch (e: IOException) {
emit(Resource.Error("IO Exception"))
}
catch (e : HttpException) {
emit(Resource.Error("HTTP EXCEPTION"))
}
emit(Resource.Success(listData))
}
}
getEventsList is in my WordInfoRepositoryImpl class in my data layer here I am pulling data from internet using jsoap
WordInfoRepository
interface WordInfoRepository {
fun getEventsList(): Flow<Resource<MutableList<ExtractedData>>>
}
this is the interface that I reference wordInforepositoryImpl in the data layer in my interface domain layer
GetWordsAndMeaningsOnTheInternetUseCase
class GetWordsAndMeaningsOnTheInternetUseCase#Inject constructor(
private val repository: WordInfoRepository
){
operator fun invoke() : Flow<Resource<MutableList<ExtractedData>>> {
return repository.getEventsList()
}
}
GetWordsAndMeaningsOnTheInternetUseCase is my usecase in my domain layer
ViewModel
#HiltViewModel
class MostUsedWordScreenViewModel #Inject constructor(
private val getWordsAndMeaningsOnTheInternetUseCase: GetWordsAndMeaningsOnTheInternetUseCase
) : ViewModel() {
private var searchJob: Job? = null
private val _state = mutableStateOf(MostUsedWordState())
val state: State<MostUsedWordState> = _state
init {
fetchData()
}
private fun fetchData() {
searchJob?.cancel()
searchJob = viewModelScope.launch(IO) {
getWordsAndMeaningsOnTheInternetUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Error -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Loading -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = true
)
}
}
}
}
}
}
MostUsedWordScreen
#Composable
fun MostUsedWordScreen(viewModel: MostUsedWordScreenViewModel = hiltViewModel()) {
val state = viewModel.state.value
println("state --- >>> "+state.mostWordUsedItems)
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(state.mostWordUsedItems.size) { i ->
val wordInfo = state.mostWordUsedItems[i]
if(i > 0) {
Spacer(modifier = Modifier.height(8.dp))
}
MostUsedWordItem(word = wordInfo)
if(i < state.mostWordUsedItems.size - 1) {
Divider()
}
}
}
}
#Composable
fun MostUsedWordItem(word : ExtractedData ) {
// println("this is MostUsedWordItem")
Column(modifier = Modifier
.padding(5.dp)
.fillMaxWidth()) {
Text(text = word.word,
modifier = Modifier.padding(3.dp),
textAlign = TextAlign.Center,
fontSize = 18.sp,
)
}
}
It is included in the MostUsedWordScreenViewModel and MostUsedWordScreen presententation layer
Where I println("state --- >>> "+state.mostWordUsedItems) in MostUsedWordScreen, the state console shows as empty like this System.out: state --- >>> []
I tried to explain as detailed as I can, I hope you can understand.
A Flow doesn't do anything until you call a terminal operator on it. You called onEach, which is not a terminal operator. You should use collect. Or you can avoid the nesting inside a launch block by using onEach and launchIn, which does the same thing as launching a coroutine and calling collect() on the flow. You don't need to specify Dispatchers.IO here because nothing in your Flow is blocking. You correctly wrapped the blocking call in withContext(Dispatchers.IO), and the warning is a false positive. That's a well-known bug in their compiler inspection.
searchJob = getWordsAndMeaningsOnTheInternetUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Error -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Loading -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = true
)
}
}
}.launchIn(viewModelScope)
By the way, you need to move your emit(Success...) inside your try block. The way it is now, when there is an error, the error will immediately get replaced by a Success with empty list.
Side note, I recommend avoiding passing MutableLists around between classes. You have no need for them and it's a code smell. Sharing mutable state between classes is error-prone. I don't think there is any justification for using a Flow<MutableList> instead of a Flow<List>.
You rarely even need a MutableList locally in a function. For example, you could have done in your try block:
val listData = List(eventsSize - 1) {
val row = rows[it + 1]
val cols = row.select("td")
val word = cols[0].text()
val meaning = cols[1].text()
ExtractedData(i,word,meaning)
}
emit(Resource.Success(listData))

Wait for result from Coroutine and then use it in Composable function

I am creating a video scraper, and it has the following function which scrapes the video source from a URL that has been given as the parameter:
fun scrapeVideoSrcFromUrl(url: String): String? {
val document = Jsoup.connect(url).get()
for (element in document.getElementsByTag("script")) {
if (element.attr("type") == "application/ld+json") {
val content = element.data()
val array = JsonParser.parseString(content).asJsonArray
val embedUrl = Gson().fromJson(array.get(0).asJsonObject.get("embedUrl"), String::class.java)
var embedId = ""
for (char in embedUrl.dropLast(1).reversed()) {
if (char != '/') {
embedId += char
} else {
break
}
}
val doc = Jsoup.connect("$RUMBLE_API_URL${embedId.reversed()}").ignoreContentType(true).get()
val jsonData = doc.getElementsByTag("body").first()?.text()
val mp4 = JsonParser.parseString(jsonData).asJsonObject.get("u").asJsonObject.get("mp4").asJsonObject.get("url").toString()
return mp4.replace("\"", "")
}
}
return null
}
I want to show this in a dialog for a certain link using ExoPlayer, so I did the following:
#Composable
fun VideoPlayer(videoSrc: String) {
val context = LocalContext.current
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(
MediaItem.fromUri(
videoSrc
)
)
prepare()
playWhenReady = true
}
}
Box(modifier = Modifier.fillMaxSize()) {
DisposableEffect(key1 = Unit) {
onDispose {
exoPlayer.release()
}
}
AndroidView(
factory = {
StyledPlayerView(context).apply {
player = exoPlayer
layoutParams =
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
}
)
}
}
Then, in the main Composable:
if (openDialog) {
AlertDialog(
onDismissRequest = {
openDialog = false
},
title = {
Column {
Text(
text = viewModel.currentRumbleSearchResult?.title ?: ""
)
Spacer(
Modifier.height(8.dp)
)
Text(
text = "By ${viewModel.currentRumbleSearchResult?.channel?.name ?: ""}",
style = MaterialTheme.typography.titleSmall
)
}
},
text = {
VideoPlayer(RumbleScraper.create().scrapeVideoSrcFromUrl("https://rumble.com/v1m9oki-our-first-automatic-afk-farms-locals-minecraft-server-smp-ep3-live-stream.html")!!)
},
confirmButton = {
TextButton(
onClick = {
openDialog = false
}
) {
Text("Exit")
}
}
)
}
After running that code I keep getting NetworkOnMainThread exceptions, and I tried many things to fix it but nothing worked.
So I am unsure what to do as to how I can go around fixing this. I was wondering how I would go around waiting in the background for a result and then show it in the Compose function when it returns the value?
You can do something like this:
var videoSrc by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
videoSrc = RumbleScraper.create().scrapeVideoSrcFromUrl("")
}
}
text = { VideoPlayer(videoSrc) }
You can also call the scrapeVideoSrcFromUrl inside your viewModel and update some state that you will use in UI.
If you want to run it in response to some event like item click, you will be better of with something like this:
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
withContext(Dispatchers.IO) { ... }
}
}
)

android modern development: how can I understand user did not change any data in a screen?

I built a fragment, using jetpack compose for adding views on screen and state and view model classes for saving state.
first time when I navigate to this fragment, fetch API is called and If I have any value on this API, I filled text fields with them. It is possible that the API is empty and do not return any value.
Users can enter any data on this text fields or can ignore them because filling the text fields is optional and when he/she clicks on submit button; the data is saved in server by calling save API and shows a successful message that data is saved.
my question is that when user navigates to this fragment and does not enter or change any value,
when clicks on submit button, I do not want to show that toast.
how can I handle it? thanks.
First you will need to create a data class to hold the values for your screen as a mutable state with a default value.
data class SampleState(
val isLoading: Boolean = true,
val someText1: String = "",
val someText2: String = ""
)
It is also necessary to create a sealed class with the possible screen actions, such as changing someText1 and someText2 and also the click of a button.
sealed class SampleAction {
object TestButton : SampleAction()
data class ChangeSomeText1(val text1: String) : SampleAction()
data class ChangeSomeText2(val text2: String) : SampleAction()
}
And finally, a sealed class to contain the possible one-time-event that can be triggered through some action, like after clicking on the button with the fields properly filled in.
sealed class SampleChannel {
object ValidFields : SampleChannel()
}
The view model will be responsible for managing all the logic:
#HiltViewModel
class SampleViewModel #Inject constructor() : ViewModel() {
var state by mutableStateOf(value = SampleState())
private set
private val _channel = Channel<SampleChannel>()
val channel = _channel.receiveAsFlow()
init {
loadInitialFakeData()
}
fun onAction(action: SampleAction) {
when (action) {
is SampleAction.TestButton -> testButtonClicked()
is SampleAction.ChangeSomeText1 -> state = state.copy(someText1 = action.text1)
is SampleAction.ChangeSomeText2 -> state = state.copy(someText2 = action.text2)
}
}
private fun loadInitialFakeData() = viewModelScope.launch {
delay(timeMillis = 1000)
if (Random.nextBoolean()) state = state.copy(
someText1 = "fake text 1 from api",
someText2 = "fake text 2 from api"
)
state = state.copy(isLoading = false)
}
private fun testButtonClicked() {
val hasBlankField = listOf(
state.someText1,
state.someText2
).any { string ->
string.isBlank()
}
if (hasBlankField) return
viewModelScope.launch {
_channel.send(SampleChannel.ValidFields)
}
}
}
In this case there is a loadInitialFakeData function that is executed as soon as the view model is instantiated, which simulates a delay only for testing purposes and according to a random boolean assigns or not values in the state class.
There is also a function testButtonClicked that will do the logic if any of the fields is empty, if any of them is empty nothing happens (I left it that way so as not to extend the code too much) and if it is not empty we notify the channel with the ValidFields.
The only public function in the view model is the onAction, which is responsible for receiving actions through the composable screen.
Now on the composable screen we can do it like this:
#Composable
fun SampleScreen(
viewModel: SampleViewModel = hiltViewModel()
) {
val state = viewModel.state
val scaffoldState = rememberScaffoldState()
LaunchedEffect(key1 = Unit) {
viewModel.channel.collect { channel ->
when (channel) {
SampleChannel.ValidFields -> {
scaffoldState.snackbarHostState.showSnackbar(
message = "all right!"
)
}
}
}
}
Scaffold(
modifier = Modifier.fillMaxSize(),
scaffoldState = scaffoldState
) {
Column(modifier = Modifier.fillMaxSize()) {
ComposeLoading(isLoading = state.isLoading)
ComposeForm(
changeSomeText1 = {
viewModel.onAction(SampleAction.ChangeSomeText1(it))
},
changeSomeText2 = {
viewModel.onAction(SampleAction.ChangeSomeText2(it))
},
testButtonClick = {
viewModel.onAction(SampleAction.TestButton)
},
state = state
)
}
}
}
#Composable
private fun ComposeLoading(
isLoading: Boolean
) = AnimatedVisibility(
modifier = Modifier.fillMaxWidth(),
visible = isLoading,
enter = expandVertically(animationSpec = tween()),
exit = shrinkVertically(animationSpec = tween())
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
Spacer(modifier = Modifier.height(height = 16.dp))
Text(
text = "loading...",
color = MaterialTheme.colors.onBackground,
style = MaterialTheme.typography.body2
)
Spacer(modifier = Modifier.height(height = 16.dp))
}
}
#Composable
private fun ComposeForm(
changeSomeText1: (String) -> Unit,
changeSomeText2: (String) -> Unit,
testButtonClick: () -> Unit,
state: SampleState
) = Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
ComposeSimpleTextField(
value = state.someText1,
onValueChange = {
changeSomeText1(it)
},
label = "text field 1"
)
Spacer(modifier = Modifier.height(height = 16.dp))
ComposeSimpleTextField(
value = state.someText2,
onValueChange = {
changeSomeText2(it)
},
label = "text field 2"
)
Spacer(modifier = Modifier.height(height = 16.dp))
Button(
modifier = Modifier.align(alignment = Alignment.End),
onClick = testButtonClick
) {
Text(text = "Test Button")
}
}
#Composable
private fun ComposeSimpleTextField(
value: String,
onValueChange: (String) -> Unit,
label: String
) = TextField(
modifier = Modifier.fillMaxWidth(),
value = value,
onValueChange = { onValueChange(it) },
label = {
Text(text = label)
}
)
The main thing here is the LaunchedEffect to collect all possible events from the SampleChannel (on this example there is only one) and the calls to screen events when the text of some field is changed or when the button is clicked... The rest is just a compose boilerplate that doesn't matter much for the main logic.
Note: I used Hilt for dependency injection in this example, so I had access to #HiltViewModel and hiltViewModel()
implementation 'com.google.dagger:hilt-android:2.42'
kapt 'com.google.dagger:hilt-android-compiler:2.42'
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'

Click listener for entire Snakbar layout

I have a snackbar and I showing information to users. If user click Snackbar, I want to open an other fragment.
private fun showLastUnsuccessfulLoginMessage(message: String) {
val snackbar =
Snackbar.make(view!!, message , Snackbar.LENGTH_INDEFINITE)
snackbar.view.setSafeOnClickListener {
Log.wtf("clicked","setSafeOnClickListenerClicked")
}
snackbar.view.z=200f
snackbar.view.translationZ=200f
snackbar.view.setBackgroundColor(ContextCompat.getColor(activity!!,R.color.colorWhite))
val snackbarTextView: TextView = snackbar.view.findViewById(R.id.snackbar_text)
snackbarTextView.setTextColor(ContextCompat.getColor(activity!!,R.color.colorBlack))
snackbarTextView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_warning_red, 0, 0, 0)
snackbarTextView.compoundDrawablePadding = resources.getDimensionPixelOffset(R.dimen.space_s)
val params =
snackbar.view.layoutParams
(params as FrameLayout.LayoutParams).gravity = Gravity.TOP
snackbar.view.layoutParams = params
snackbar.view.visibility = View.INVISIBLE
snackbar.addCallback(object : Snackbar.Callback() {
override fun onShown(snackbar: Snackbar?) {
super.onShown(snackbar)
animateViewFromTop(snackbar!!.view)
}
})
snackbar.show()
isShowedUnsuccessfulLoginMessage = true
object : CountDownTimer(8000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
animateViewToTop(snackbar.view)
}
}.start()
}
With this code, Snackbar is showing and when I click the snackbar, nothing happened.
snackbar.view.setSafeOnClickListener {
Log.wtf("clicked","setSafeOnClickListenerClicked")
}
This is not working fine.