I had an instrumented test that worked, however, then was added additional param to the object which required to call the #Composable function, so the entire test method should be marked as #Composable, so I added this annotation to the function, however, it starts to throw the error:
org.junit.runners.model.InvalidTestClassError: Invalid test class 'entpay.awl.ui.component.showpage.ScreenKtTest':
1. Method testDetails should have no parameters
So, the method looks like this
#Test
#Composable. <------ Composable annotation
fun testDetails() {
....
val window: WindowSize = rememberWindowSize() <------ This method is Composable
composeTestRule.setContent {
Details(
config = ...,
windowSize = window
)
}
}
I am a new in instrumental testing, however, I assume that I can't use #Composable methods in testing directly
How to make it work?
UPD
I also tried to put the window line in setContent, result the same
....
composeTestRule.setContent {
val window: WindowSize = rememberWindowSize()
Details(
config = ...,
windowSize = window
)
}
Related
I can have
val composeFunction = remember { mutableStateOf ({}) }
I can have
val composeFF = #Composable { Text("ABC") }
val composeFunction = remember { mutableStateOf (composeFF) }
Why can't I have
val composeFunction = remember { mutableStateOf (#Composable { Text("ABC") }) }
It errors out state
Internal Error occurred while analyzing this expression
(Please use the "
" icon in the bottom-right corner to report this error):
jar:file:/Applications/Android%20Studio.app/Contents/lib/platform-impl.jar!/general/error.svg
Type inference failed. Expected type mismatch: inferred type is #Composable () -> Unit but () -> Unit was expected
Have you tried specifying the type?
val composeFunction = remember { mutableStateOf<#Composable () -> Unit> (#Composable { Text("ABC") }) }
Looks like the compiler cannot infer an ordinary function to something that is supplied with a #Composable annotation
Update:
It turns out #Composable annotation actually modifies the function type, similar to what suspend modifier does.
Based on this arcticle,
An important thing to note is that Compose is not an annotation
processor. Compose works with the aid of a Kotlin compiler plugin in
the type checking and code generation phases of Kotlin: there is no
annotation processor needed in order to use compose. This annotation
more closely resembles a language keyword. A good analogy is Kotlin’s
suspend keyword.
furthermore,
The important point here is that when you annotate a function type
with #Composable you’re changing its type: the same function type
without the annotation is not compatible with the annotated type.
Also, suspend functions require a calling context, meaning that you
can only call suspend functions inside of another suspend function.
I'm trying to make Kotlin's invoke operator a #Composable, everything works fine, until I add a parameter to it, which should have a default value. See the code below:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent{
Button()
}
}
}
object Button{
#Composable
operator fun invoke(text: String = "SomeText"){
println(text) // prints: null
}
}
When the operator is not annotated as #Composable the output is SomeText, as it should be.
Is this some error in Jetpack Compose, or am I missing something?
The behavior is the same on the latest stable Compose v 1.1.1 and on 1.2.0-beta01. Kotlin 1.6.21
Based on the info provided in the comments, I decided to answer:
I'll maybe think of something better, but off the top of my head, this is what you can do for now
enum class ButtonType {
Primary,
Secondary,
Tertiary
}
Return the correct type of Button
#Composable
fun MasterButton(type: ButtonType) {
when(type) {
primary -> PrimaryButton()
secondary -> SecondaryButton()
else -> TertiaryButton() // Must provide an 'else' branch
}
}
This will do the job for you.
CORRECT APPROACH I:
I just got the correct one the moment I started typing the first approach.
#Composable
fun ( #Composable () -> Unit ).Primary(...) {
PrimaryButton()
}
Make copies for every other button.
STRONG NOTICE: This is a RIDICULOUS way of "cleaning" up the code. Nobody should ever use anything remotely resembling this ever, but since that is just what the question is about, this is how you go about doing it. Know that this will attach an extension function called Primary(...) to every single #Composable function, and that cannot change. You can't apply it to select Composable(s) only, since this is basically just an extension function that I have applied on a general labmda, since 'extension functions for extension functions' are not something that exist as of now.
I am going to take this as your question (even though it is in the comments) and try to answer the way I achieve this.
What I'm trying to achieve is a way to clean up the namespace, so that
not all Composables are available as a top-level function. The general
idea is to group all flavors of let's say Buttons (Primary, Secondary,
Tertiary) to be Composables declared as a function of object Button.
But I would like to be able to use also this Button object as a
default Button (let it be Primary) in a Compose way, so just by using
it as it would be a function, thus invoke() operator. I would have
Button.Primary(), Button.Secondary() and Button() which would be an
"alias" for Button.Primary().
My implementation is quite simple,
Expose only one top-level Composable function to have a cleaner namespace.
Pass an argument that denotes the type of the required Composable, using a sealed class.
Button Type
sealed class MyIconButtonType(
open val typeName: String,
) {
data class Default(
override val typeName: String = "default",
) : MyIconButtonType(
typeName = typeName,
)
data class BorderIconButton(
override val typeName: String = "border",
// The variant specific attributes can be added here
val borderWidth: Int,
) : MyIconButtonType(
typeName = typeName,
)
}
Button (The only composable exposed to other files)
#Composable
fun MyTestIconButton(
onClickLabel: String,
modifier: Modifier = Modifier,
data: MyIconButtonType = MyIconButtonType.Default(),
onClick: () -> Unit,
content: #Composable () -> Unit,
) {
when (data) {
is MyIconButtonType.Default -> {
// This composable should be private
MyTestIconDefaultButton(
// parameter as required
)
}
is MyIconButtonType.BorderIconButton -> {
// This composable should be private
MyTestIconDefaultButton(
// parameter as required, also make sure to pass variant specific attributes here
)
}
}
}
Usage
// For default impl
MyTestIconButton(
// default parameters
) {
}
// For specific variants
MyTestIconButton(
// default parameters
data = MyIconButtonType.BorderIconButton(
borderWidth = 10,
),
) {
}
Note:
Data class requires at least one attribute. Use object if no attributes like the typeName are required.
Like this,
sealed class MyIconButtonType {
object Default : MyIconButtonType()
data class BorderIconButton(
val borderWidth: Int,
) : MyIconButtonType()
}
Kotlin concepts that are used for reference,
Sealed classes, data classes and objects
when statement
Visibility modifiers
In Kotlin, function is a first-class citizen. We can store a function in a variable as below
val functionVariable: () -> Unit = ::myFunction
fun myFunction() { }
However, for #Composable function, how can I do so?
If I did the below, it will cry foul i.e. org.jetbrains.kotlin.diagnostics.SimpleDiagnostic#e93b05f8 (error: could not render message)
val functionVariable: () -> Unit = ::myFunction
#Composable
fun myFunction() { }
Is there a way to store composable function as a variable?
Composable function reference is not yet supported (and that's what the error message actually is). Besides, #Composable annotation is part of the function signature because it adds some parameters to the function. So you need to use val functionVariable: #Composable () -> Unit = { myFunction() }.
You can declare a composable function variable, like:
var functionVariable: #Composable () -> Unit = {}
Later you can reassign it (essentially redefining it) like:
functionVariable = { myFunction1() }
or
functionVariable = { myFunction2() }
But if you have functionVariable already planted in your larger Composable, the reassignment likely won't trigger recomposation. What I do is declare a state variable like:
var functionAssigned by mutableStateOf(0)
And in you larger Composable where you plant functionVariable, do:
if (functionAssigned >= 0) functionVariable()
And whenever you want to reassign the variable, do:
functionVariable = { myFunction2() }
functionAssigned++
That will trigger needed recomposation.
I am creating a simple junit test to test a function in my view model but the first assertion fails as the function I call returns null. When I debug the function I call has null parameters which is weird cause I pass them in.
I have spent time debugging and searching for why I am having that issue but I have found nothing that fixes my issue or tells me what the issue is.
#RunWith(MockitoJUnitRunner::class)
class CurrencyUnitTest {
#Rule
#JvmField
val rule = InstantTaskExecutorRule()
#Mock
val currencyViewModel : CurrencyViewModel = mock(CurrencyViewModel::class.java)
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
val rates: HashMap<String, Double> =
hashMapOf(
"USD" to 1.323234,
"GBP" to 2.392394,
"AUD" to 0.328429,
"KWR" to 893.4833
)
val currencyRates = MutableLiveData<Resource<CurrencyRatesData?>>()
val resource = Resource<CurrencyRatesData?>(Status.SUCCESS, CurrencyRatesData("CAD", rates, 0))
currencyRates.value = resource
`when`(currencyViewModel.currencyRatesData).thenReturn(currencyRates)
val baseCurrency = MutableLiveData<String>()
baseCurrency.value = "CAD"
`when`(currencyViewModel.baseCurrency).thenReturn(baseCurrency)
}
#Test
fun calculateValueTest() {
// this fails
assertEquals("0.36", currencyViewModel.calculateValue("AUD", "1.11"))
}
}
Mocked classes will not really be called. If you want to test your currencyViewModel.calculateValue() method, create a real object of that class and mock possible constructor arguments.
To add to what Ben has said: the class you want to test has to be a real object, not a mock. A mock "does nothing" per default, and only does what you do it to tell you, so to test it does not make any sense.
What you mock is the dependencies of the class you test, i.e. the objects you pass to its' constructor.
In short: if you want to test CurrencyViewModel, create an object of it instead of mocking it.
I have a custom class:
class MyClass {
var name = ""
fun changeName(newName: String) {
name = newName
}
}
and my testing class:
#Test
fun testVerifyMock() {
val instance: MyClass = mock()
instance.changeName("newname")
Assert.assertEquals("newname", instance.name)
}
I'm faily new to Unit Tests and I'm kinda stuck, can someone please point me to why I get this error:
java.lang.AssertionError:
Expected :newname
Actual :null
Basically the call instance.changeName("newname") doesn't seem to be changing the name since it's always null
Mockito mocks just ignore what you pass to their methods unless you explicitly tell them what to do. In the case of changeName, the parameter is just ignored and therefore the name will remain null. I don't see why you would use a mock here anyway, so just change to:
val instance = MyClass()
...
Here's a post on "when to use mock".