본문 바로가기
Android/Android Core

[Android/Kotlin] activityResultLauncher로 비동기 데이터 교환 및 파일 업로드 결과 처리하기

by quessr 2024. 11. 1.

 

안드로이드에서는 화면 간 데이터 교환을 위해 기존에 startActivityForResult를 사용했지만, activityResultLauncher API가 등장하면서 더 직관적이고 안전하게 데이터를 주고받을 수 있게 되었습니다.

이번 글에서는 카메라 액티비티에서 촬영한 파일을 서버에 업로드하고, 그 결과를 원래 화면에 전달하여 토스트로 표시하는 예시를 통해 activityResultLauncher의 사용법을 알아보겠습니다.

1. activityResultLauncher를 onCreate에서 정의해야 하는 이유

activityResultLauncher는 액티비티의 생명주기를 따르는 안전한 비동기 콜백입니다. onCreate에서 한 번만 정의하여야 안정적으로 동작하며, 다른 메서드에서 정의할 경우 여러 개의 activityResultLauncher 인스턴스가 불필요하게 생성되어 예상치 못한 동작이 발생할 수 있습니다. onCreate에서 정의하여 액티비티 생명주기와 함께 안전하게 관리하는 것이 좋습니다.

 

2. activityResultLauncher를 활용한 파일 업로드 결과 처리 예시

이번 예시에서는 신고하기 액티비티에서 카메라 액티비티를 실행하고, 촬영한 파일을 서버에 업로드한 후 성공 여부를 원래 액티비티로 전달하여 토스트로 알림을 표시하는 과정입니다. activityResultLauncher를 통해 서버 업로드 결과 데이터를 주고받아 비동기적으로 관리할 수 있습니다.

 

신고하기 액티비티

신고하기 액티비티는 activityResultLauncher를 onCreate에서 정의하여, 카메라 액티비티의 실행 결과를 비동기적으로 수신합니다. sendEmergencyCreate() 메서드가 비상 신고 API를 호출해 서버로부터 이벤트 ID를 받고, 이 ID를 tryMediaCapture()로 전달하여 카메라 액티비티를 실행합니다.

 

class ReportActivity : AppCompatActivity() {

    private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>

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

        // activityResultLauncher 정의 및 등록
        activityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            if (it.resultCode == RESULT_OK) {
                Toast.makeText(this, "사진이 등록되었습니다.", Toast.LENGTH_SHORT).show()
            } else if (it.resultCode == RESULT_CANCELED) {
                val errorMessage = it.data?.getStringExtra("onFailure")
                Toast.makeText(this, errorMessage ?: "사진 등록 실패", Toast.LENGTH_SHORT).show()
            }
        }
    }

    // 비상 신고 API 호출
    private fun sendEmergencyCreate() {
        ApiSender.reportEmergency(this) { eventId ->
            Logger.d("Received event ID in ReportActivity: $eventId")
            tryMediaCapture(eventId) // 응답받은 eventId로 카메라 액티비티 실행
        }
    }

    private fun tryMediaCapture(eventId: Long) {
        val captureFormat = UserSettingsManager.getEmergencyFormat(this)
        if (captureFormat != UserSettingsManager.EmergencyFormatType.NONE) {
            val intent = Intent(this, MediaCaptureActivity::class.java).apply {
                putExtra("eventId", eventId)
                putExtra("mediaFormat", captureFormat.value)
            }
            activityResultLauncher.launch(intent) // activityResultLauncher를 통해 카메라 액티비티 시작
        }

        (application as GaboApplication).isEmergency = true
        uiEmergency()
    }
}

 

카메라 액티비티

카메라 액티비티는 takePhoto()와 captureVideo() 메서드를 통해 촬영한 파일을 서버에 업로드합니다. 업로드 성공 시 RESULT_OK와 성공 메시지를 반환하고, 실패 시 RESULT_CANCELED와 실패 메시지를 반환하여 신고하기 액티비티가 이를 받아서 토스트로 알림을 표시할 수 있게 합니다.

class MediaCaptureActivity : AppCompatActivity() {

    private suspend fun uploadFiles(eventId: Long, videoFile: File?, imageFiles: List<File>?) {
        fileUploadClient.uploadFiles(
            context = this,
            eventId = eventId,
            videoFile = videoFile,
            imageFiles = imageFiles,
            onSuccess = {
                Logger.d("파일 업로드 성공 $eventId")
                val intent = Intent().apply {
                    putExtra("onSuccess", "파일 업로드 성공 $eventId")
                }
                setResult(RESULT_OK, intent)
                if (!isFinishing) finish()
            },
            onFailure = { errorMessage ->
                Logger.d("파일 업로드 실패: $errorMessage, $eventId")
                val intent = Intent().apply {
                    putExtra("onFailure", "파일 업로드 실패: $errorMessage")
                }
                setResult(RESULT_CANCELED, intent)
                if (!isFinishing) finish()
            })
    }

    private fun takePhoto() {
        val photoFile = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "photo.jpg")
        imageCapture.takePicture(
            ImageCapture.OutputFileOptions.Builder(photoFile).build(),
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                    CoroutineScope(Dispatchers.IO).launch {
                        uploadFiles(eventId = mediaEventId, imageFiles = listOf(photoFile), videoFile = null)
                    }
                }

                override fun onError(exception: ImageCaptureException) {
                    Logger.e("Photo capture failed", exception)
                }
            }
        )
    }

    private fun captureVideo() {
        val videoFile = File(getExternalFilesDir(Environment.DIRECTORY_MOVIES), "video.mp4")
        val outputOptions = FileOutputOptions.Builder(videoFile).build()
        videoCapture.output.prepareRecording(this, outputOptions).apply {
            if (ActivityCompat.checkSelfPermission(this@MediaCaptureActivity, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
                withAudioEnabled()
            }
        }.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
            if (recordEvent is VideoRecordEvent.Finalize && !recordEvent.hasError()) {
                CoroutineScope(Dispatchers.IO).launch {
                    uploadFiles(eventId = mediaEventId, videoFile = videoFile, imageFiles = null)
                }
            }
        }
    }
}

 

위 예시에서 takePhoto()와 captureVideo() 메서드는 촬영 후 uploadFiles() 메서드를 호출하여 서버에 파일을 업로드합니다. uploadFiles()는 서버 응답에 따라 성공 시 RESULT_OK와 성공 메시지를 반환하고, 실패 시 RESULT_CANCELED와 실패 메시지를 반환하여 신고하기 액티비티가 이를 통해 알림을 표시할 수 있게 합니다.

반응형