Screen Recorder app in Android using Jetpack Compose

Screen Recorder app in Android using Jetpack Compose
Have you ever wanted to record your phone screen, maybe to create a tutorial video, share your gaming skills, or explain an app feature to your friend? That’s exactly what a Screen Recorder App does!

But wait... what's Jetpack Compose doing here?

➡️ Jetpack Compose is a modern way of designing Android UIs using Kotlin. Instead of writing lots of XML files, you can build your app’s UI using simple and powerful Kotlin code.
So, a Screen Recorder App using Jetpack Compose means:
✅ The UI (User Interface) is built with Jetpack Compose.
✅ The screen recording functionality is handled by Android’s MediaProjection API + MediaRecorder API.

🎥 What is a Screen Recorder App in Jetpack Compose?

Imagine this: You’re playing a game, crushing your high score, and you wish your friends could see it too. Or maybe you’ve figured out how to fix a bug on your phone and want to show someone else step by step.
That’s where a Screen Recorder App comes in — it lets you capture everything happening on your screen and save it as a video.

Now, what about Jetpack Compose? That’s simply the modern way of designing the app’s user interface (UI) in Android. Instead of writing boring old XML layouts, we use clean Kotlin code that’s easy to read and faster to build.

So basically:
➡️ Jetpack Compose = Beautiful, fast Android UIs.
➡️ Screen Recorder App = Captures your screen and saves it as a video.
Put them together, and you get a modern screen recording app with an awesome UI.

🔧  How Does a Screen Recorder App Work (in Simple Words)?

Here’s the simple breakdown of what happens behind the scenes:

✅ Step 1: Ask Permission

The first thing your app does is politely ask Android, “Hey, can I record what’s on the screen?” Android shows a pop-up where the user clicks Allow.

✅ Step 2: Start Capturing the Screen

Behind the scenes, Android gives your app a special tool called MediaProjection. This tool quietly watches what’s happening on your screen.

✅ Step 3: Record the Screen

Another tool called MediaRecorder takes those screen images and stitches them into a video file. Kind of like how a camera takes pictures and creates a video.

✅ Step 4: Save the Video

Once you stop recording, the app saves the video on your phone — usually in a folder like /Movies/ScreenRecords.

✅ Step 5: Done! 🎉

Now you have a screen recording you can watch, share on WhatsApp, upload to YouTube, or keep for later.

🤔 Why Should You Learn to Build This App?

✔️ Learn How Android Really Works

Recording a screen is not a simple button click — you’ll learn about system permissions, media tools, and storage management.
✔️ Practice Modern Android Development
Jetpack Compose is the future of Android UI. Learning to build cool, useful apps in Compose will help you stay ahead of the curve.

✔️ Build a Real-World Project
This is not a "Hello World" app. It’s something you could actually use on your phone, or share on your GitHub as a portfolio project.

✔️ Open Doors for Future Apps
Once you learn how screen capture works, you can able to build:
  • Live streaming apps
  • Remote helpdesk apps
  • Educational tutorial apps

✅ 1. Alternative Screen Capture Approaches

ApproachAudio?PerformanceComplexityUse case
MediaProjection + MediaRecorder⚙️ Good🟢 EasyRecommended for most use cases
MediaProjection + MediaCodec + Surface⚡ Best🔴 AdvancedMore control over encoding, smaller file sizes
FFmpeg (via JNI / external libs)⚙️ OK🔴 ComplexCustom encoding, stream to network
ReplayKit (iOS only) --iOS alternative
Third-party libraries (e.g. ScreenStream, Scrcpy base)🚫/✅⚙️ Medium🟡MediumSpecial cases like streaming

🚀 What’s in the App?

You’ll typically build:

  • A Start/Stop Recording Button
  • A Timer showing how long the recording is running
  • A Save Location, so users can find their recordings
  • A Simple, Clean Compose UI

Step-by-step guide to building a Screen Recorder app in Android using Jetpack Compose and the MediaProjection API + MediaRecorder

Folder Structure

✅ Dependency

👉No extra dependency is needed for the UI above if you are already using Jetpack Compose Material 3, which is included in modern Compose projects.

✅ AndroidManifest.xml

<!-- Required for MediaProjection and foreground service -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />

<!-- Record screen with microphone audio -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />

<!-- Android 13+ Notification Permission -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<!-- Optional: for devices <= Android 12 to save files -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<service
android:name=".ScreenRecorderService"
android:foregroundServiceType="mediaProjection"
android:exported="false" />
Screen Recorder Service Manifest File

Android Manifest Permission

✅ MainActivity.kt

package com.example.screencapture

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import com.example.screencapture.ui.theme.ScreenCaptureTheme

class MainActivity : ComponentActivity() {
private val viewModel: ScreenRecorderViewModel by viewModels {
ScreenRecorderViewModelFactory(application)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()

setContent {
ScreenCaptureTheme {
ScreenRecorderApp(viewModel)
}
}
}
}

✅ ScreenRecorderViewModel.kt

package com.example.screencapture

import android.app.Activity
import android.app.Application
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class ScreenRecorderViewModel(
private val app: Application
) : AndroidViewModel(app) {

companion object {
private const val TAG = "ScreenRecorderViewModel"
}

// UI State
private val _isRecording = MutableStateFlow(false)
val isRecording: StateFlow<Boolean> = _isRecording

private val _includeAudio = MutableStateFlow(true)
val includeAudio: StateFlow<Boolean> = _includeAudio

private val _errorMessage = MutableStateFlow<String?>(null)
val errorMessage: StateFlow<String?> = _errorMessage

// MediaProjection permission data
private var mediaProjectionIntent: Intent? = null
private var resultCode: Int = Activity.RESULT_CANCELED

/**
* Called from UI on Start/Stop button click
*/
fun toggleRecording(onRequestProjection: () -> Unit) {
if (_isRecording.value) {
stopRecording()
} else {
onRequestProjection()
}
}

/**
* Called from Activity after MediaProjection permission is granted
*/
fun onMediaProjectionPermissionGranted(resultCode: Int, data: Intent) {
this.resultCode = resultCode
this.mediaProjectionIntent = data
startScreenRecordingService()
}

/**
* Start the Foreground Screen Recorder Service
*/
private fun startScreenRecordingService() {
if (mediaProjectionIntent == null || resultCode != Activity.RESULT_OK) {
_errorMessage.value = "Screen capture permission not granted"
Log.e(TAG, "Missing MediaProjection permission")
return
}

val serviceIntent = Intent(app, ScreenRecorderService::class.java).apply {
action = ScreenRecorderService.ACTION_START
putExtra(ScreenRecorderService.EXTRA_RESULT_CODE, resultCode)
putExtra(ScreenRecorderService.EXTRA_RESULT_DATA, mediaProjectionIntent)
putExtra(ScreenRecorderService.EXTRA_INCLUDE_AUDIO, includeAudio.value)
}

try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
app.startForegroundService(serviceIntent)
} else {
app.startService(serviceIntent)
}
_isRecording.value = true
_errorMessage.value = null
} catch (e: Exception) {
Log.e(TAG, "Failed to start recording: ${e.localizedMessage}")
_errorMessage.value = "Failed to start recording"
_isRecording.value = false
}
}

/**
* Stop the ScreenRecorderService
*/
fun stopRecording() {
viewModelScope.launch {
try {
val serviceIntent = Intent(app, ScreenRecorderService::class.java).apply {
action = ScreenRecorderService.ACTION_STOP
}
app.stopService(serviceIntent)
} catch (e: Exception) {
Log.e(TAG, "Error stopping service: ${e.localizedMessage}")
_errorMessage.value = "Error stopping recording"
} finally {
_isRecording.value = false
}
}
}

/**
* Toggle microphone recording
*/
fun toggleIncludeAudio() {
_includeAudio.value = !_includeAudio.value
}

/**
* Clear error after showing it in UI
*/
fun clearError() {
_errorMessage.value = null
}
}

✅ ScreenRecorderService.kt

package com.example.screencapture

import android.app.Activity
import android.content.Intent
import android.hardware.display.VirtualDisplay
import android.media.MediaRecorder
import android.media.projection.MediaProjection
import android.os.IBinder
import android.app.*
import android.content.Context
import android.hardware.display.DisplayManager
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Environment
import android.util.Log
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File

class ScreenRecorderService : Service() {

private var elapsedSeconds = 0L
private var timerJob: Job? = null


companion object {
const val ACTION_START = "ACTION_START"
const val ACTION_STOP = "ACTION_STOP"

const val EXTRA_RESULT_CODE = "RESULT_CODE"
const val EXTRA_RESULT_DATA = "RESULT_DATA"
const val EXTRA_INCLUDE_AUDIO = "INCLUDE_AUDIO"

private const val NOTIFICATION_CHANNEL_ID = "screen_recorder_channel"
private const val NOTIFICATION_ID = 1001
}

private var mediaProjection: MediaProjection? = null
private var mediaRecorder: MediaRecorder? = null
private var virtualDisplay: VirtualDisplay? = null

override fun onBind(intent: Intent?): IBinder? = null

override fun onCreate() {
super.onCreate()
createNotificationChannel()
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START -> startRecording(intent)
ACTION_STOP -> stopRecording()
else -> stopSelf()
}
return START_STICKY
}

private fun startRecording(intent: Intent) {
showNotification()
startTimer()

val resultCode = intent.getIntExtra(EXTRA_RESULT_CODE, Activity.RESULT_CANCELED)
val resultData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(EXTRA_RESULT_DATA, Intent::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(EXTRA_RESULT_DATA)
} ?: run {
stopSelf()
return
}

val includeAudio = intent.getBooleanExtra(EXTRA_INCLUDE_AUDIO, true)

val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, resultData)

initMediaRecorder(includeAudio)

val surface = mediaRecorder!!.surface
virtualDisplay = mediaProjection!!.createVirtualDisplay(
"ScreenRecorder",
720, 1280, resources.displayMetrics.densityDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
surface, null, null
)

mediaRecorder?.start()
}
private fun startTimer() {

timerJob = CoroutineScope(Dispatchers.Default).launch {
while (true) {
delay(1000)
elapsedSeconds++
updateNotification()
}
}
}
private fun updateNotification() {
val hours = elapsedSeconds / 3600
val minutes = (elapsedSeconds % 3600) / 60
val seconds = elapsedSeconds % 60

val timeText = "Recording: %02d:%02d:%02d".format(hours, minutes, seconds)

val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setContentTitle("Screen Recorder")
.setContentText(timeText)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setOngoing(true)
.build()

val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(NOTIFICATION_ID, notification)
}

private fun stopRecording() {
timerJob?.cancel()
timerJob = null
elapsedSeconds = 0L
try {
mediaRecorder?.apply {
stop()
reset()
release()
}
virtualDisplay?.release()
mediaProjection?.stop()
} catch (e: Exception) {
Log.e("ScreenRecorderService", "Stop error: ${e.localizedMessage}")
}

stopForeground(true)
stopSelf()
}

private fun initMediaRecorder(includeAudio: Boolean) {
val outputFile = getOutputFile()

mediaRecorder = MediaRecorder().apply {
setVideoSource(MediaRecorder.VideoSource.SURFACE)
if (includeAudio) {
setAudioSource(MediaRecorder.AudioSource.MIC)
}
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setOutputFile(outputFile.absolutePath)
setVideoEncoder(MediaRecorder.VideoEncoder.H264)
if (includeAudio) {
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
}
setVideoEncodingBitRate(5 * 1024 * 1024)
setVideoFrameRate(30)
setVideoSize(720, 1280)
prepare()
}
}

private fun getOutputFile(): File {
val dir = File(getExternalFilesDir(Environment.DIRECTORY_MOVIES), "ScreenRecords")
if (!dir.exists()) dir.mkdirs()
return File(dir, "ScreenRecord_${System.currentTimeMillis()}.mp4")
}

private fun showNotification() {
val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setContentTitle("Screen Recorder")
.setContentText("Recording in progress...")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setOngoing(true)
.build()

startForeground(NOTIFICATION_ID, notification)
elapsedSeconds = 0L
updateNotification() // Starts with 00:00:00
}

private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
"Screen Recorder Service",
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
}

override fun onDestroy() {
stopRecording()
super.onDestroy()
}
}

✅ ScreenRecorderUI.kt

package com.example.screencapture

import android.app.Activity
import android.content.Context
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp


// ScreenRecorderScreen.kt

@Composable
fun ScreenRecorderApp(viewModel: ScreenRecorderViewModel) {
val context = LocalContext.current
val isRecording by viewModel.isRecording.collectAsState()
val includeAudio by viewModel.includeAudio.collectAsState()
val errorMessage by viewModel.errorMessage.collectAsState()

val projectionManager = remember { context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager }

val mediaProjectionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
viewModel.onMediaProjectionPermissionGranted(result.resultCode, result.data!!)
} else {
Toast.makeText(context, "Screen capture permission denied", Toast.LENGTH_SHORT).show()
}
}

val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
val granted = permissions.values.all { it }
if (granted) {
mediaProjectionLauncher.launch(projectionManager.createScreenCaptureIntent())
} else {
Toast.makeText(context, "Permissions not granted", Toast.LENGTH_SHORT).show()
}
}

// Show error as Toast
LaunchedEffect(errorMessage) {
errorMessage?.let {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
viewModel.clearError()
}
}

Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
listOf(MaterialTheme.colorScheme.primaryContainer, MaterialTheme.colorScheme.background)
)
)
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
shape = RoundedCornerShape(24.dp),
elevation = CardDefaults.cardElevation(8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {

Text(
text = "🎥 Screen Recorder",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))

Row(verticalAlignment = Alignment.CenterVertically) {
Text("Include Audio", color = MaterialTheme.colorScheme.onSurfaceVariant)
Spacer(Modifier.width(8.dp))
Switch(
checked = includeAudio,
onCheckedChange = { viewModel.toggleIncludeAudio() }
)
}

Spacer(modifier = Modifier.height(32.dp))

Button(
onClick = {
if (isRecording) {
viewModel.stopRecording()
} else {
requestPermissions(context, viewModel.includeAudio.value, permissionLauncher)
}
},
colors = ButtonDefaults.buttonColors(containerColor = if (isRecording) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary)
) {
Text(
if (isRecording) " Stop Recording" else "▶ Start Recording",
color = MaterialTheme.colorScheme.onPrimary
)
}
}
}
}
}

/**
* Dynamically ask permissions (RECORD_AUDIO + POST_NOTIFICATIONS)
*/
private fun requestPermissions(
context: Context,
includeAudio: Boolean,
permissionLauncher: ManagedActivityResultLauncher<Array<String>, Map<String, Boolean>>
) {
val permissions = mutableListOf<String>()

if (includeAudio) {
permissions.add(android.Manifest.permission.RECORD_AUDIO)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissions.add(android.Manifest.permission.POST_NOTIFICATIONS)
}

if (permissions.isNotEmpty()) {
permissionLauncher.launch(permissions.toTypedArray())
} else {
val mediaProjectionManager = context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
(context as? Activity)?.startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), 1001)
}
}

✅ ScreenRecorderViewModelFactory

package com.example.screencapture

import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

@Suppress("UNCHECKED_CAST")
class ScreenRecorderViewModelFactory(private val app: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ScreenRecorderViewModel(app) as T
}
}
Screen Recorder app in Android using Jetpack Compose Screenshot 1Screen Recorder app in Android using Jetpack Compose Screenshot 2

🛠️ Final Thoughts

A Screen Recorder app may sound complex, but once you break it down, it’s a really fun project to build.
Plus, you’ll learn modern tools like:
  • Jetpack Compose UI
  • MediaProjection & MediaRecorder APIs
  • Permissions management
  • Coroutines & State management in Kotlin

Previous Post Next Post