android / architecture-samples

A collection of samples to discuss and showcase different architectural tools and patterns for Android apps.
Apache License 2.0
44.5k stars 11.65k forks source link

HiltTestActivity fails with error "You need to use a Theme.AppCompat theme (or descendant) with this activity." #894

Closed sarimmehdi closed 1 year ago

sarimmehdi commented 1 year ago

I am following the code from here:

// inside debug folder
@SuppressLint("CustomSplashScreen")
@AndroidEntryPoint
class SplashScreenActivityTest: AppCompatActivity()

Then the AndroidManifest.xml for the debug folder:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application>
        <activity android:name=".SplashScreenActivityTest"
            android:exported="false"/>
    </application>

</manifest>

This is what my instrumentation test looks like:

@HiltAndroidTest
class SplashScreenFragmentTest {

    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    @Before
    fun setup() {
        hiltRule.inject()
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    @Test
    fun splashScreenExitsToLoginScreenAfter3Seconds() {
        launchFragmentInHiltContainer<SplashScreenFragment> {

        }
    }
}

When I run my Instrumentation Test, I get the following error:

java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
at androidx.appcompat.app.AppCompatDelegateImpl.createSubDecor(AppCompatDelegateImpl.java:856)
at androidx.appcompat.app.AppCompatDelegateImpl.ensureSubDecor(AppCompatDelegateImpl.java:819)
at androidx.appcompat.app.AppCompatDelegateImpl.onPostCreate(AppCompatDelegateImpl.java:536)
at androidx.appcompat.app.AppCompatActivity.onPostCreate(AppCompatActivity.java:151)
at android.app.Instrumentation.callActivityOnPostCreate(Instrumentation.java:1383)
at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3891)
at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:221)
at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:201)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:173)
at android.app.servertransaction.TransactionExehttps://github.com/android/architecture-samples/blob/views-hilt/app/src/debug/java/com/example/android/architecture/blueprints/todoapp/HiltTestActivity.ktcutor.execute(TransactionExecutor.java:97)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2328)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:246)
at android.app.ActivityThread.main(ActivityThread.java:8645)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)

I am able to get rid of the error by applying a theme inside the AndroidManifest.xml file of debug:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application android:theme="@style/someThemeThatInheritsFromThemeAppCompat">
        <activity android:name=".SplashScreenActivityTest"
            android:exported="false"/>
    </application>

</manifest>

But then the prod version of my app (which also uses a custom theme derived from Theme.AppCompat inside the manifest for the app module), fails to run and complains with the following error (:feature_splash:splash_presentation is the module where I created the debug folder with the test activity similar to what is shown here):

Manifest merger failed : Attribute application@theme value=(@style/myCustomTheme) from AndroidManifest.xml:13:9-46
    is also present at [:feature_splash:splash_presentation] AndroidManifest.xml:10:18-59 value=(@style/mdCockpitBaseTheme).
    Suggestion: add 'tools:replace="android:theme"' to <application> element at AndroidManifest.xml:5:5-28:19 to override.

Is there a way to make the instrumentation test run without adding that extra theme in the manifest for debug? This is what my launchFragmentInHiltContainer looks like btw:

@ExperimentalCoroutinesApi
inline fun <reified T : Fragment> launchFragmentInHiltContainer(
    fragmentArgs: Bundle? = null,
    themeResId: Int = androidx.fragment.testing.R.style.FragmentScenarioEmptyFragmentActivityTheme,
    fragmentFactory: FragmentFactory? = null,
    crossinline action: T.() -> Unit = {}
) {
    val mainActivityIntent = Intent.makeMainActivity(
        ComponentName(
            ApplicationProvider.getApplicationContext(),
            SplashScreenActivityTest::class.java
        )
    ).putExtra(
        "androidx.fragment.app.testing.FragmentScenario.EmptyFragmentActivity.THEME_EXTRAS_BUNDLE_KEY",
        themeResId
    )

    ActivityScenario.launch<SplashScreenActivityTest>(mainActivityIntent).onActivity { activity ->
        fragmentFactory?.let {
            activity.supportFragmentManager.fragmentFactory = it
        }
        val fragment = activity.supportFragmentManager.fragmentFactory.instantiate(
            Preconditions.checkNotNull(T::class.java.classLoader),
            T::class.java.name
        )
        fragment.arguments = fragmentArgs

        activity.supportFragmentManager.beginTransaction()
            .add(android.R.id.content, fragment, "")
            .commitNow()

        (fragment as T).action()
    }
}
sarimmehdi commented 1 year ago

Ok, so I solved the issue by replacing my HiltTestActivity with this one (courtesy of this):

@SuppressLint("CustomSplashScreen")
@AndroidEntryPoint
class SplashScreenActivityTest: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setTheme(
            intent.getIntExtra(
                "androidx.fragment.app.testing.FragmentScenario.EmptyFragmentActivity.THEME_EXTRAS_BUNDLE_KEY",
                androidx.fragment.testing.R.style.FragmentScenarioEmptyFragmentActivityTheme
            )
        )
    }
}

Then, inside launchFragmentInHiltContainer, I provided my app's custom theme which inherits from Theme.AppCompat:

@ExperimentalCoroutinesApi
inline fun <reified T : Fragment> launchFragmentInHiltContainer(
    fragmentArgs: Bundle? = null,
    themeResId: Int = R.style.myCustomeTheme,
    fragmentFactory: FragmentFactory? = null,
    crossinline action: T.() -> Unit = {}
) {
    val mainActivityIntent = Intent.makeMainActivity(
        ComponentName(
            ApplicationProvider.getApplicationContext(),
            SplashScreenActivityTest::class.java
        )
    ).putExtra(
        "androidx.fragment.app.testing.FragmentScenario.EmptyFragmentActivity.THEME_EXTRAS_BUNDLE_KEY",
        themeResId
    )

    ActivityScenario.launch<SplashScreenActivityTest>(mainActivityIntent).onActivity { activity ->
        fragmentFactory?.let {
            activity.supportFragmentManager.fragmentFactory = it
        }
        val fragment = activity.supportFragmentManager.fragmentFactory.instantiate(
            Preconditions.checkNotNull(T::class.java.classLoader),
            T::class.java.name
        )
        fragment.arguments = fragmentArgs

        activity.supportFragmentManager.beginTransaction()
            .add(android.R.id.content, fragment, "")
            .commitNow()

        (fragment as T).action()
    }
}