androidkotlinandroid-jetpack-composeandroid-testing

Setting requestedOrientation results in crash


I am trying to test an Android app built with Kotlin and Jetpack compose for configuration changes like switching to LANDSCAPE/PORTRAIT mode. When I set the orientation to Landscape/Portrait, I see crash. Below is the simple AndroidTest code. Could someone please help me in fixing this?

package com.example.sandbox
import android.content.pm.ActivityInfo
import androidx.compose.ui.test.ComposeTimeoutException
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.core.app.ActivityScenario
import org.junit.Rule
import org.junit.Test

class MainActivityTest {
    @get:Rule
    val rule = createAndroidComposeRule<MainActivity>()
    private lateinit var scenario: ActivityScenario<MainActivity>

    @Test
    fun sandboxTest() {
        scenario = ActivityScenario.launch(MainActivity::class.java)
        rule.waitForIdle()
        rule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
    }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            SandBoxTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!"
    )
}

Crash:

java.lang.NullPointerException: Cannot run onActivity since Activity has been destroyed already
at androidx.test.internal.util.Checks.checkNotNull(Checks.java:50)
at androidx.test.core.app.ActivityScenario.lambda$onActivity$2$androidx-test-core-app-ActivityScenario(ActivityScenario.java:792)
at androidx.test.core.app.ActivityScenario$$ExternalSyntheticLambda2.run(D8$$SyntheticClass:0)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:487)
at java.util.concurrent.FutureTask.run(FutureTask.java:264)
at android.app.Instrumentation$SyncRunnable.run(Instrumentation.java:2497)
at android.os.Handler.handleCallback(Handler.java:984)
at android.os.Handler.dispatchMessage(Handler.java:104)
at android.os.Looper.loopOnce(Looper.java:238)
at android.os.Looper.loop(Looper.java:357)
at android.app.ActivityThread.main(ActivityThread.java:8149)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:957)



Solution

  • The issue you're facing is likely due to the timing of the orientation change. When you set the requested orientation, the activity gets recreated, which can cause the crash you're seeing. Here are a few steps to help you fix this:

    Use the ActivityScenario directly in the test rule: Instead of manually creating and managing the ActivityScenario, use it within the Compose test rule. This ensures proper synchronization and lifecycle management.

    Wait for the activity to be idle after changing orientation: Ensure that the test waits for the activity to become idle after changing the orientation.

    Here's how you can update your test code:

    class MainActivityTest {
        @get:Rule
        val rule = createAndroidComposeRule<MainActivity>()
    
        @Test
        fun sandboxTest() {
            // Launch the activity
            val scenario = ActivityScenario.launch(MainActivity::class.java)
    
            // Wait for the activity to be idle
            rule.waitForIdle()
    
            // Change the orientation to landscape
            scenario.onActivity { activity ->
                activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
            }
    
            // Wait for the activity to be idle again
            rule.waitForIdle()
        }
    }
    
    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
    
    @Composable
    fun Greeting(name: String, modifier: Modifier = Modifier) {
        Text(
            text = "Hello $name!"
        )
    }
    

    In this updated code:

    This should help prevent the NullPointerException caused by the activity being destroyed while the test is still running.