This crash often begins with an innocent-looking library method that accepts Context, then quietly does (Activity) context several calls later. The caller passes getBaseContext(), applicationContext, a Service, or a themed wrapper; the cast finally exposes the lie in the API. The durable fix is to make the required capability explicit.
What the exception proves
java.lang.ClassCastException:
android.app.ContextImpl cannot be cast to android.app.Activity
at com.example.library.Prompt.show(Prompt.kt:42)
at com.example.MainActivity.onCreate(MainActivity.kt:18)Read from the first application frame
ContextImplis the runtime object the code attempted to cast.The first frame in your/library package identifies the cast site, here
Prompt.kt:42.The caller frame shows how that value entered the API.
The exception does not mean every Context should be replaced with
this; it means one path assumed an Activity without proving it.Preserve the complete stack, device/API, code version, and runtime type before editing.
Context types are chosen by lifetime and capability
Activity context: tied to one Activity; owns a Window, theme, lifecycle, and activity-result/UI operations.
Application context: tied broadly to the process; useful for long-lived non-UI work and process-scoped dependencies.
Service context: belongs to a Service and is not an Activity.
ContextThemeWrapper: wraps another Context with a theme; it might ultimately wrap an Activity, but that is not guaranteed.
View context: selected when the View is created; often themed and sometimes Activity-backed.
Fragment is not a Context; it can expose its current host context or FragmentActivity while attached.
The original failing pattern
public final class BrokenPrompt {
public static void show(Context context) {
Activity activity = (Activity) context; // Unsafe contract.
new AlertDialog.Builder(activity)
.setMessage("Continue?")
.show();
}
}
// ContextImpl is not an Activity.
BrokenPrompt.show(getBaseContext());The method signature hides the real requirement
The compiler permits any Context because the parameter type promises any Context is acceptable.
The body immediately violates that promise by requiring an Activity.
getBaseContext()returns the wrapped base Context; it is not an API for recovering the current Activity.An AlertDialog needs a valid UI/window owner and appropriate theme.
Changing only the caller leaves future callers free to trigger the same crash.
Fix 1: require Activity in the API
object Prompt {
fun show(activity: Activity) {
if (activity.isFinishing || activity.isDestroyed) return
AlertDialog.Builder(activity)
.setMessage("Continue?")
.setPositiveButton("Continue", null)
.show()
}
}The type system now protects callers
A Service or Application cannot be passed accidentally because it is not an Activity.
The Window and theme come from the Activity that owns the visible UI.
Lifecycle checks avoid opening a dialog while the owner is finishing/destroyed, though state/navigation ownership may need stronger lifecycle design.
The method does not store the Activity beyond the synchronous UI operation.
For reusable AndroidX code, consider accepting
FragmentActivityorComponentActivityonly when that narrower feature set is genuinely required.
Call from an Activity correctly
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Prompt.show(this)
}
}Here this really is an Activity
Inside an Activity member function, unqualified
thisrefers to the Activity instance.AppCompatActivityis a FragmentActivity/ComponentActivity and ultimately an Activity Context.The call is lifecycle-scoped to the visible owner.
Do not pass
applicationContext,baseContext, or a singleton-cached Context to an Activity-only API.
Watch this inside listeners
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Prompt.show(MainActivity.this);
}
});Nested scopes change what this means
Inside this anonymous Java listener, plain
thismeans theView.OnClickListener.MainActivity.thisexplicitly selects the enclosing Activity.In Kotlin,
this@MainActivityis the labeled equivalent when another receiver shadowsthis.Prefer lambdas and explicit parameters where they make ownership easier to read.
From a Fragment, use the attached host deliberately
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<Button>(R.id.confirm).setOnClickListener {
Prompt.show(requireActivity())
}
}Fragment attachment is part of the contract
requireActivity()returns the FragmentActivity currently hosting the Fragment.It throws
IllegalStateExceptionwhen the Fragment is not associated with an Activity.Call from a valid attached/view lifecycle callback—not a delayed callback after detachment.
Use
requireContext()only for APIs that need Context but not specifically Activity.Prefer Fragment-owned dialogs/navigation APIs when they better preserve state and lifecycle.
Fix 2: if UI is unnecessary, accept Context honestly
class AnalyticsClient(context: Context) {
private val appContext = context.applicationContext
fun record(name: String) {
appContext.getSharedPreferences("analytics", Context.MODE_PRIVATE)
.edit()
.putLong(name, System.currentTimeMillis())
.apply()
}
}Application context fits process-lifetime work
The constructor accepts any Context because the implementation needs app resources/storage, not a Window.
It immediately stores
applicationContextto avoid retaining an Activity.Long-lived repositories, databases, preferences, and many system services commonly fit this pattern.
Application context is not universally “best”; visual services, themed inflation, dialogs, and activity-result APIs need a UI-aware owner.
Long-lived registrations still require explicit unregister/unbind cleanup even with application context.
Launching an Activity from non-Activity Context
fun openDetails(context: Context, itemId: String) {
val intent = Intent(context, DetailActivity::class.java).apply {
putExtra("item_id", itemId)
if (context !is Activity) {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
}
context.startActivity(intent)
}Launching does not require a fake cast
Context.startActivityworks outside an Activity when the Intent includesFLAG_ACTIVITY_NEW_TASK.The flag starts/resolves the Activity in a task because there is no caller Activity task to reuse directly.
This changes task/back-stack behavior; background activity launch restrictions still apply.
If you need a result, transition, lifecycle owner, or existing navigation controller, require an appropriate Activity/Fragment API instead.
Validate the target and catch/handle
ActivityNotFoundExceptionfor implicit intents.
View and Compose contexts may be wrapped
A library receiving view.context or Compose LocalContext.current may see a ContextThemeWrapper. It often wraps an Activity in ordinary app UI, but previews, dialogs, services, tests, and custom context setups break that assumption. Prefer passing Activity explicitly. At an integration boundary where only a Context is available, unwrap defensively and return null.
fun Context.findActivity(): Activity? {
var current: Context = this
while (current is ContextWrapper) {
if (current is Activity) return current
val base = current.baseContext
if (base === current) return null
current = base
}
return current as? Activity
}Unwrapping is a boundary adapter, not an API design
The safe cast returns null instead of crashing for Application, Service, preview, or non-Activity window contexts.
The loop follows ContextWrapper base contexts and stops on a self-reference.
A themed wrapper can preserve the Activity below it, but there is no guarantee one exists.
Callers must handle null by disabling UI, deferring, or using an explicit owner.
Do not hide this search deep inside every library method; make UI ownership visible at the public boundary.
Compose usage with a safe boundary
@Composable
fun ConfirmButton() {
val context = LocalContext.current
Button(onClick = {
val activity = context.findActivity() ?: return@Button
Prompt.show(activity)
}) {
Text("Confirm")
}
}LocalContext is typed as Context for a reason
Compose does not promise
LocalContext.current as Activityis always safe.The adapter tolerates previews and nonstandard hosts without ClassCastException.
For navigation, permission, or result flows, hoist events to an Activity/Fragment or use lifecycle-aware Compose APIs.
Avoid capturing an obsolete Activity in a long-lived coroutine/effect after configuration change.
Design library APIs around capabilities
class SecureSdk private constructor(
private val appContext: Context
) {
companion object {
fun create(context: Context) =
SecureSdk(context.applicationContext)
}
fun preload() {
// Process-lifetime work only.
}
fun launchVerification(activity: ComponentActivity) {
// UI/lifecycle/result work owned by this Activity.
}
}Split process and UI dependencies
Initialization stores only application context.
UI entry points explicitly require a lifecycle-capable Activity.
ComponentActivityis appropriate only if AndroidX lifecycle/activity-result features are used; otherwise accept Activity.The SDK must not retain the Activity after the UI flow finishes.
A callback, Activity Result contract, Fragment, or intent-based public Activity can further decouple library internals.
Do not solve the crash by leaking Activity
Never put an Activity in a static field, singleton, Application object, process-wide service, or long-lived repository.
A retained Activity keeps its Window/View tree and resources alive after configuration change or navigation.
A WeakReference can become null at any time and does not make an operation lifecycle-correct.
Cancel asynchronous UI work when the owner stops/destroys, or return an event/state to the current UI.
Use lifecycle-aware coroutines, ViewModels for non-View state, and explicit UI callbacks/results.
A Context decision table
Dialog, Window, activity result, permissions tied to UI, transition → Activity/ComponentActivity/Fragment.
Inflate themed visible UI → the correct visual/themed Context.
Database, DataStore/preferences, repository, WorkManager initialization → application context.
Toast → Context is sufficient; application context is usually appropriate for long-lived callers.
Start Activity without an Activity owner → Context plus
FLAG_ACTIVITY_NEW_TASK, accepting task semantics.LayoutInflater/WindowManager for a non-Activity window → a proper window context on supported APIs, not an Activity cast.
Debug the runtime type before changing code
Log.d(
"ContextDebug",
"type=${context::class.java.name}, " +
"app=${context.applicationContext::class.java.name}"
)Log types, not sensitive objects
The runtime class quickly distinguishes Activity, Application, Service, and wrapper cases.
Do not log Context.toString(), Intent extras, tokens, account data, or user content in production.
Remove or gate temporary diagnostic logging before release.
Combine the type with the stack’s cast site and lifecycle state; class name alone does not define the correct owner.
Common mistaken fixes
Replace everything with
applicationContext→ avoids some leaks but breaks UI/theme/window operations.Replace everything with
this→ meaning changes in listeners, receivers, adapters, Compose receivers, and nested classes.Cast with
as Activityor(Activity)→ postpones type checking until runtime.Use
as? Activity!!→ turns a safe-null check into another crash.Store the last Activity globally → creates lifecycle leaks and races.
Catch ClassCastException → hides the broken contract and leaves the operation without a valid UI owner.
Testing matrix
Activity caller succeeds and uses the visible owner.
Fragment caller succeeds only while attached and handles delayed callbacks after detachment.
Application and Service callers never reach Activity-only APIs.
Themed View/Compose contexts unwrap or fail gracefully at the explicit boundary.
Configuration change does not retain the old Activity.
Background launch uses intended task flags and respects platform restrictions.
Dialogs/actions do not run after finish/destroy or saved-state boundaries.
Library initialization accepts application context without changing UI behavior.
Fast diagnosis flow
Open the complete stack trace and locate the first app/library cast.
Log/inspect the runtime Context type at the caller.
Classify the operation as UI/window/lifecycle work or process/non-UI work.
Change the callee signature to Activity/Fragment/ComponentActivity if UI ownership is required.
Otherwise remove the cast and retain application context only when lifecycle scope requires it.
Test Activity, Fragment, Service/Application, wrappers, configuration changes, and delayed callbacks.
Add a regression test and document the public API’s ownership/lifetime contract.
Code review questions
Does the parameter type describe the narrowest capability the method actually uses?
Could this object outlive an Activity or configuration instance?
What happens when the caller is a Service, test, preview, detached Fragment, or themed wrapper?
Is UI work owned by the current lifecycle and cancelled when that owner disappears?
Primary references
Android’s `ContextWrapper` reference explains base context, application context lifetime, visual contexts, and wrapper subclasses.
The official `Context.startActivity`) contract requires
FLAG_ACTIVITY_NEW_TASKoutside an Activity Context.AndroidX `Fragment.requireActivity()`) documents the attached-host guarantee and failure mode.
Use the Activity, ComponentActivity, Fragment lifecycle, Activity Result, dialogs, and background-launch documentation for the exact capability the API needs.
Comments and corrections