I just tried to load an image from my resources with val context = ambient(ContextAmbient) but when I try to run the project I get an error during the generation of the code.
java.lang.IllegalStateException: Backend Internal error: Exception during code generation
#Composable
fun MovieImage(image: Int) {
val context = ambient(ContextAmbient)
Container(modifier = Modifier.None, width = 24.dp, height = 24.dp) {
DrawImage(image = imageFromResource( context.resources, image))
}
}
I ran in to the same problem when upgrading from 0.1.0-dev03 to 0.1.0-dev05.
It was solved by adding composeOptions{ kotlinCompilerExtensionVersion "0.1.0-dev05" } to my build.gradle like this:
android {
// ... other gradle properties
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion "0.1.0-dev05"
}
}
Related
I have a project with several flavors. Each of these flavors has its own configuration which is available as a json file in the assets folder in the respective project structure.
In the theme definition I read the JSON using Gson and cast it into a corresponding model.
My problem is now the following:
At runtime of the app this all works wonderfully but in the composable preview in Android Studio it unfortunately only works for a single flavor. As soon as I switch to another flavor in the build variant, the json-asset of the old variant continues to load. Since the configuration also contains assets that are only available in the respective flavors, this leads to a crash of the preview.
I debugged the preview handling by throwing some exceptions during the casting and it seems, like if there'S something cached and not reset after build-variant change. A restart of Android Studio didn't also help so I don't quite know what to do about it.
Has anyone noticed a similar behavior and/or found a solution for it?
Here is some code to explain::
My theme definition:
object AppTheme {
val colors: AppColors
#Composable
#ReadOnlyComposable
get() = LocalAppColors.current
val typography: AppTypography
#Composable
#ReadOnlyComposable
get() = LocalAppTypography.current
val configuration: ConfigurationC
#Composable
#ReadOnlyComposable
get() = LocalAppConfiguration.current
}
private val LocalAppColors = staticCompositionLocalOf {
lightAppColors
}
private val LocalAppTypography = staticCompositionLocalOf {
appTypography
}
private val LocalAppConfiguration = staticCompositionLocalOf {
ConfigurationC()
}
#Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: #Composable () -> Unit,
) {
val colors = if (darkTheme) darkAppColors else lightAppColors
CompositionLocalProvider(
LocalAppConfiguration provides ConfigurationC.init(LocalContext.current),
LocalAppColors provides colors,
LocalAppTypography provides typography,
) {
MaterialTheme(
colors = colors.materialColors,
typography = typography.materialTypography,
content = content,
)
}
}
A simple Preview:
#Composable
#Preview(name = "light", showBackground = true)
#Preview(name = "dark", showBackground = true, uiMode = UI_MODE_NIGHT_YES)
fun EnabledPreview() {
AppTheme {
Button.MyCustomButton(
modifier = Modifier,
title = "Custom Button",
font = AppTheme.configuration.font.h1
color = AppTheme.configuration.colors.text1
enabled = enabled,
onClick = {}
)
}
}
I'm loading an image using Coil for Compose like below.
#Composable
fun SvgImageSample() {
val painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.decoderFactory(SvgDecoder.Factory())
.data("https://someserver.com/SVG_image.svg")
.size(Size.ORIGINAL)
.build()
)
Image(
painter = painter,
modifier = Modifier.size(100.dp).testTag("myImg"),
contentDescription = null
)
}
The image is loaded properly. Now, I would like to write a test to check if the image was loaded. Is there any assertion out-of-the-box for that?
Something like this:
class MyTest {
#get:Rule
val composeTestRule = createComposeRule()
#Test
fun checkIfTheImageLoads() {
composeTestRule.setContent {
MyAppThemeTheme {
SvgImageSample()
}
}
composeTestRule.onNodeWithTag("myImg")
.assertCoilImageIsLoaded() // <- this is what I want
}
}
I found what I was looking for... Please let me know if anyone has a better solution.
This is what I did:
Add this dependency in your build.gradle.
implementation "androidx.test.espresso.idling:idling-concurrent:3.5.0-alpha07"
This is necessary to use the IdlingThreadPoolExecutor class.
Declare the an IdlingThreadPool object like below:
object IdlingThreadPool: IdlingThreadPoolExecutor(
"coroutinesDispatchersThreadPool",
Runtime.getRuntime().availableProcessors(),
Runtime.getRuntime().availableProcessors(),
0L,
TimeUnit.MILLISECONDS,
LinkedBlockingQueue(),
Executors.defaultThreadFactory()
)
I get this hint from this issue in the Coil github page.
Use the object declared above in the ImageRequest object.
#Composable
fun SvgImageSample() {
val painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.dispatcher(IdlingThreadPool.asCoroutineDispatcher()) // << here
.decoderFactory(SvgDecoder.Factory())
.data("https://someserver.com/SVG_image.svg")
.size(Size.ORIGINAL)
.build()
)
Image(
painter = painter,
modifier = Modifier
.size(100.dp)
.semantics {
testTag = "myImg"
coilAsyncPainter = painter
},
contentDescription = null
)
}
Notice the IdlingThreadPool object was used in the dispatcher function. The other detail is coilAsyncPainter property which is receiving the painter object. It will be necessary during the test to check if the image was loaded.
Declare the coilAsyncPainter semantic property.
val CoilAsyncPainter = SemanticsPropertyKey<AsyncImagePainter>("CoilAsyncPainter")
var SemanticsPropertyReceiver.coilAsyncPainter by CoilAsyncPainter
This is what you need to do in the application code.
In the test code, declare a new SemanticNodeInteration.
fun SemanticsNodeInteraction.isAsyncPainterComplete(): SemanticsNodeInteraction {
assert(
SemanticsMatcher("Async Image is Success") { semanticsNode ->
val painter = semanticsNode.config.getOrElseNullable(CoilAsyncPainter) { null }
painter?.state is AsyncImagePainter.State.Success
}
)
return this;
}
So here, basically the painter object is obtained from the semantic property and then is checked if the current state is Success.
Finally, here it is the test.
class MyTest {
#get:Rule
val composeTestRule = createComposeRule()
#Test
fun async_image_was_displayed() {
composeTestRule.setContent {
MyAppThemeTheme {
SvgImageSample()
}
}
composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("myImg")
.isAsyncPainterComplete()
}
}
Another way would be to implement an EventListener and check the right events are emitted. Will save you using testTags and semantic properties in the app code.
https://coil-kt.github.io/coil/api/coil-base/coil-base/coil/-event-listener/index.html
A quick hacky attempt, but you could wrap this in a Composable helper that does this for any block passed in.
#Test
fun imageLoader() {
var success = 0
var errors = 0
composeTestRule.setContent {
Coil.setImageLoader(
ImageLoader.Builder(context)
.eventListener(object : EventListener {
override fun onSuccess(
request: ImageRequest,
result: SuccessResult
) {
success++
}
override fun onError(
request: ImageRequest,
result: ErrorResult
) {
errors++
}
})
.build()
)
MyAppThemeTheme {
SvgImageSample()
}
}
Thread.sleep(500)
assertThat(errors).isEqualTo(0)
assertThat(success).isEqualTo(1)
}
First, I have to say that the approach suggested by #nglauber worked. However, I cringed at that level of complexity for a simple test, so I tried a straight forward test and that works as well and I will keep so.
First, I loaded the image simply with AsyncImage
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(template.previewUrl)
.crossfade(true)
.build(),
placeholder = painterResource(template.thumbNailResId),
contentDescription = stringResource(R.string.template_description),
contentScale = ContentScale.Fit,
)
Then in the test, I simply checked for the node with content description is displayed like so
#Test
fun intialImageDisplayedTest() {
val template = TemplateRepository.getTemplate()[0]
composeTestRule.setContent {
val selectedIndex = remember{ mutableStateOf(-1) }
TemplateItem(
selectedId = selectedIndex,
template = template,
onPreviewButtonClicked = {}
)
}
composeTestRule.onNodeWithTag("template_${template.templateId}").assertIsDisplayed()
composeTestRule.onNodeWithContentDescription(getImageDescriptionText()).assertIsDisplayed()
}
private fun getImageDescriptionText(): String {
return composeTestRule.activity.resources.getString(R.string.template_description)
}
Again keeping it simple. I also added a matcher with a test tag. No Idling resource needed.
I'm trying to write the test for my composes. So I have a test class put in AndroidTest just like this:
#HiltAndroidTest
#UninstallModules(AuthenticationModule::class, AppModule::class)
class AuthenticationScreenTest {
#get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
#get:Rule(order = 1)
val composeRule = createAndroidComposeRule<MainActivity>()
#Inject
lateinit var setting: Setting
#Before
fun setup() {
hiltRule.inject()
composeRule.setContent {
val navController = rememberNavController()
RefectoryTheme {
NavHost(
navController = navController,
startDestination = AuthenticationNavGraph.AuthenticationScreen.route
) {
composable(AuthenticationNavGraph.AuthenticationScreen.route) {
AuthenticationScreen(navController = navController, setting = setting)
}
}
}
}
}
#Test
fun checkLoadingButtonExpantion() {
composeRule.onNodeWithTag(testTag = AUTHENTICATION_SCREEN_LOGIN_BUTTON)
.assertIsDisplayed()
}
}
but I keep getting the error:
androidx.compose.ui.test.junit4.android.ComposeNotIdleException: Idling resource timed out:
possibly due to compose being busy.
IdlingResourceRegistry has the following idling resources registered:
- [busy] androidx.compose.ui.test.junit4.ComposeIdlingResource#a005df5
All registered idling resources: Compose-Espresso link
The android emulator is launched, test compiles successfully, but it seems it can't find the object.
I also have added a test tag to the modifier of the object:
LoadingButton(
buttonText = stringResource(id = R.string.login),
isExpanded = state.isLoginExpanded,
modifier = Modifier
.padding(MaterialTheme.spacing.medium)
.align(Alignment.CenterHorizontally)
.testTag(AUTHENTICATION_SCREEN_LOGIN_BUTTON)
) {
viewModel.onEvent(AuthenticationEvent.Login)
}
But after 28 seconds, I got the error as mentioned above.
What am I missing?
I just realized what the problem was.
I am using Lottie on my screen, and the animation is infinity repeating.
So I don't know why but it seems that it doesn't allow testing to go through.
The tests ran without any problem when I commented the Lottie section.
Trying to play around with Jetpack Compose and got stuck in the first activiity itself.
My App crashes on launch with below exception:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: io.chanse.locals.cerve.dev, PID: 19105
java.lang.NoSuchMethodError: No static method ChanseTheme(ZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V in class Lio/chanse/locals/commonui/theme/ChanseThemeKt; or its super classes (declaration of 'io.chanse.locals.commonui.theme.ChanseThemeKt' appears in /data/app/io.chanse.locals.cerve.dev-E-UJEroB15OQtmBe0WnvGw==/base.apk!classes5.dex)
at io.chanse.locals.cerve.location.LocationActivity.Screen(LocationActivity.kt:45)
at io.chanse.locals.cerve.location.LocationActivity$onCreate$1.invoke(LocationActivity.kt:39)
at io.chanse.locals.cerve.location.LocationActivity$onCreate$1.invoke(LocationActivity.kt:38)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:121)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:46)
at androidx.compose.ui.platform.ComposeView.Content(ComposeView.android.kt:333)
at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:179)
at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:178)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:121)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:46)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:193)
at androidx.compose.ui.platform.CompositionLocalsKt.ProvideCommonCompositionLocals(CompositionLocals.kt:148)
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:114)
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:113)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:121)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:46)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:193)
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt.ProvideAndroidCompositionLocals(AndroidCompositionLocals.android.kt:106)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$3.invoke(Wrapper.android.kt:162)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$3.invoke(Wrapper.android.kt:161)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:121)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:46)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:193)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:161)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:144)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:121)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.kt:46)
at androidx.compose.runtime.ComposerKt.invokeComposable(Composer.kt:3418)
at androidx.compose.runtime.ComposerImpl.composeContent$runtime_release(Composer.kt:2600)
at androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:348)
at androidx.compose.runtime.Recomposer.composeInitial$runtime_release(Recomposer.kt:693)
at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:304)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:144)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:135)
at androidx.compose.ui.platform.AndroidComposeView.setOnViewTreeOwnersAvailable(AndroidComposeView.android.kt:603)
at androidx.compose.ui.platform.WrappedComposition.setContent(Wrapper.android.kt:135)
E/AndroidRuntime: at androidx.compose.ui.platform.WrappedComposition.onStateChanged(Wrapper.android.kt:187)
at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354)
at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:196)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:142)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:135)
at androidx.compose.ui.platform.AndroidComposeView.onAttachedToWindow(AndroidComposeView.android.kt:667)
at android.view.View.dispatchAttachedToWindow(View.java:21304)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:4239)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:4246)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:4246)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:4246)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:4246)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2571)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2225)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:9126)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:999)
at android.view.Choreographer.doCallbacks(Choreographer.java:797)
at android.view.Choreographer.doFrame(Choreographer.java:732)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:984)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:237)
at android.app.ActivityThread.main(ActivityThread.java:8173)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:496)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1100)
Here is my Activity code (:app module):
class LocationActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Screen()
}
}
#Composable
fun Screen() {
ChanseTheme {
Column(
verticalArrangement = Arrangement.Center
) {
PinCodeField()
}
}
}
#Composable
fun PinCodeField() {
var text by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = text,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
onValueChange = { text = it },
label = { Text(stringResource(R.string.ask_pincode)) },
leadingIcon = { Icon(Icons.Filled.LocationOn, null) },
trailingIcon = { Icon(Icons.Filled.ArrowForward, null) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
}
Here is my Theme present in ChanseTheme.kt (:commonui module)
#Composable
fun ChanseTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: #Composable () -> Unit
) {
MaterialTheme(
colors = if (darkTheme) DarkColors else LightColors,
) {
content()
}
}
And my colors present in ChanseColors.kt (:commonui module)
val DarkColors = darkColors(
primary = Purple200,
primaryVariant = Purple600,
secondary = Green200,
secondaryVariant = Green500,
background = Black900,
surface = Black800,
error = Red200,
onPrimary = Black900,
onSecondary = Black900,
onBackground = White50,
onSurface = White50,
onError = Black900
)
val LightColors = lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Green200,
secondaryVariant = Green500,
background = White50,
surface = White50,
error = Red600,
onPrimary = White50,
onSecondary = Black900,
onBackground = Black900,
onSurface = Black900,
onError = White50
)
FYI: Here is the dependecies: (:buildSrc module)
object Activity {
const val activityCompose = "androidx.activity:activity-compose:1.3.0-alpha04"
}
object Compose {
const val version = "1.0.0-beta02"
const val runtime = "androidx.compose.runtime:runtime:$version"
const val runtimeLivedata = "androidx.compose.runtime:runtime-livedata:$version"
const val material = "androidx.compose.material:material:$version"
const val icon = "androidx.compose.material:material-icons-core:$version"
const val iconX = "androidx.compose.material:material-icons-extended:$version"
const val foundation = "androidx.compose.foundation:foundation:$version"
const val layout = "androidx.compose.foundation:foundation-layout:$version"
const val ui = "androidx.compose.ui:ui:$version"
const val tooling = "androidx.compose.ui:ui-tooling:$version"
const val animation = "androidx.compose.animation:animation:$version"
const val uiTest = "androidx.compose.ui:ui-test-junit4:$version"
}
object Lifecycle {
const val viewModelCompose = "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha03"
}
The above dependencies are present in :commonui module as api and the entire module is used as dependency in :app module.
What could go wrong here? Any help would be appreciated.
This issue was caused because your :commonui doesn't configure build.gradle file in an appropriate for jetpack compose way:
android {
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerVersion = kotlin_version
kotlinCompilerExtensionVersion = compose_version
}
kotlinOptions {
jvmTarget = '1.8'
useIR = true
}
}
Try to configure :commonui and all traversal modules in this way and it should work.
Traversal modules:
for example you have the next module graph -> :commonui-> A ->
module_which_has_a_problem In this situation, you have to configure
module A in the same way
I believe this is a bug. I get the same error when I run Previews on a real device sometimes. So I suggest :
Making sure you're running the Activity and not the Preview (check the selected run configuration next to the run button)
Try to run on the emulator
Compose tooling is quite unstable at the moment, in my experience. Be prepared to do things like invalidate cache, alternate compose versions to force some kind of reset, etc.
I fixed it with below changes:
Moved my ChanseTheme.kt and ChanseColors.kt files from :commonui module to :app module and it started working.
But I am not sure, why it doesn't work if those two files were present in my :commonui module.
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerVersion = kotlin_version
kotlinCompilerExtensionVersion = compose_version
}
These two blocks are needed in each modules using compose, even when you are importing the compose dependencies in a common module.
It seems that this issue is because you're using the multiplatform plugin. If your :commonui module was pure android ("kotlin-android" instead of "org.jetbrains.kotlin.multiplatform") it would run without crashes.
In order to fix this and keep using multiplatform, simply add this plugin on your module: id("org.jetbrains.compose").version("1.1.0").
You may also need to add this as a repository: maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
I have my custom lint, that checks XML files for hex colors usage.
It works perfectly when I run
./gradlew myapp:lint
But it does not hightlight problem places. Even more - Analyze -> Inspect Code does not give me result and returns only
No suspicious code found.
Source Code:
class XmlColorsDetector : ResourceXmlDetector() {
companion object {
val ISSUE: Issue = Issue.create(
id = "CustomColorsXml",
briefDescription = "Custom colors detector",
explanation = "Use only theme colors, in other case our themes behaviour will not work properly",
category = Category.CORRECTNESS,
priority = 6,
severity = Severity.ERROR,
implementation = Implementation(
XmlColorsDetector::class.java,
Scope.RESOURCE_FILE_SCOPE
)
)
}
override fun getApplicableAttributes(): Collection<String>? {
return listOf("color", "textColor", "background")
}
override fun getApplicableElements(): Collection<String>? {
return listOf("color")
}
override fun visitAttribute(context: XmlContext, attribute: Attr) {
if (attribute.textContent.startsWith("#") && !context.file.path.containsExcludes()) {
println("uri" + context.file.path)
context.report(ISSUE, context.getLocation(attribute), "You can not use hardcoded colors")
}
}
private fun String.containsExcludes(): Boolean {
val excludes = listOf("/color", "/drawable")
excludes.forEach {
if (this.contains(it))
return true
}
return false
}
}
I tried LintFix but it nothing changed.
How can I achieve a proper hightlight in Android Studio?