When developing Android applications using Kotlin in Android Studio, many developers encounter a frustrating issue: the app works perfectly fine on the emulator but fails or behaves unexpectedly on a real device. This is a common problem that can occur due to multiple reasons, ranging from API compatibility, permissions, hardware limitations, or even differences in device configuration. In this article, we will explore the most common causes, real-world examples, and provide practical code fixes to ensure your Kotlin app runs smoothly on both emulator and physical Android devices.

1. Common Causes of Emulator vs. Real Device Issues
- Missing Permissions: The emulator often grants permissions automatically, but on a real device, users must explicitly allow them.
- Different API Levels: Your emulator might be running the latest Android version, while your physical device uses an older API.
- Hardware Dependencies: Features like camera, GPS, Bluetooth, and sensors behave differently on real devices.
- ProGuard or R8 Issues: Code obfuscation during release builds may break certain classes or methods.
- File Path Differences: The emulator may have different file structures compared to physical devices.
2. Example Case: Permission Issue
One of the most common issues is related to runtime permissions. Suppose you are building a simple Kotlin app that accesses the camera. On the emulator, it works without problems, but on a real device, the app crashes because the camera permission is not granted properly.
class MainActivity : AppCompatActivity() { private val CAMERA_REQUEST_CODE = 100 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Check and request permission if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), CAMERA_REQUEST_CODE) } else { openCamera() } } private fun openCamera() { val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) startActivityForResult(cameraIntent, CAMERA_REQUEST_CODE) } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == CAMERA_REQUEST_CODE && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { openCamera() } else { Toast.makeText(this, "Camera permission denied", Toast.LENGTH_SHORT).show() } } }
On the emulator, permissions may be granted by default, but on a real device, users must allow it manually. This code ensures the app checks and requests permission before accessing the camera.
3. Example Case: API Level Compatibility
Another issue happens when you use APIs not supported by your physical device. For example, suppose you are using the BiometricPrompt
API which requires Android 9 (API 28) or higher. If your real device runs Android 7, the app will crash.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val biometricPrompt = BiometricPrompt.Builder(this) .setTitle("Login") .setSubtitle("Authenticate with biometrics") .setNegativeButton("Cancel", mainExecutor, { _, _ -> }) .build() biometricPrompt.authenticate(CancellationSignal(), mainExecutor, object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationSucceeded( result: BiometricPrompt.AuthenticationResult? ) { super.onAuthenticationSucceeded(result) Toast.makeText(applicationContext, "Success!", Toast.LENGTH_SHORT).show() } }) } else { Toast.makeText(this, "Biometric not supported on this device", Toast.LENGTH_LONG).show() }
This code ensures backward compatibility by checking the API version before calling features only available in newer Android versions.
4. Debugging Techniques
- Use Logcat: Always check Logcat output when running on a real device to capture specific error messages.
- Check Permissions in Settings: Ensure permissions are manually granted on the physical device.
- Enable Developer Options: Sometimes USB debugging restrictions affect app behavior.
- Test on Multiple Devices: Don’t rely only on one emulator or device for testing.
5. ProGuard / R8 Issues
If your app works in debug mode but crashes on the release build, ProGuard (or R8) may be removing required classes. Add rules to your proguard-rules.pro
file:
# Keep all classes extending AppCompatActivity -keep class * extends androidx.appcompat.app.AppCompatActivity { *; } # Keep all model classes (example) -keep class com.example.myapp.models.** { *; } # Keep Retrofit/Gson models -keep class com.google.gson.** { *; } -keep class retrofit2.** { *; }
This ensures important classes are not stripped during minification.
6. Final Thoughts
The difference between emulator and real device performance is a challenge every Android developer faces. By carefully handling permissions, ensuring API compatibility, writing defensive code, and checking ProGuard rules, you can make sure your Kotlin app works on both emulator and physical devices without issues. Always remember to test on multiple real devices to catch edge cases that the emulator may not reveal.