갤러리에서 사진을 가져오려면 먼저 Content ProviderContent Provider를 알아야 한다.

- 콘텐츠 프로바이더 (Content Provider)
다른 앱의 데이터를 제공받아 사용하려면 Content Provider를 구현해야한다. 다른 앱 또는 안드로이드 OS에 이미 구현되어 이쓴 ContentProvider로부터 데이터를 제공받아 사용한다.

- 콘텐츠 리졸버 (Content Resolver)
Content Provider로부터 데이터를 가져오는 도구가 Content Resolver이다. 안드로이드에 있는 연락처 갤러리, 음악 같은 기본 데이터를 이용하는 용도로 많이 사용된다.

갤러리에서 사진 가져오기

기존의 안드로이드에서 갤러리에서 이미지를 가져오려면 registerForActivityResult를 이용했던 기억이 있다.
Compose에서는 rememberLauncherForActivityResult를 이용해서 갤러리로부터 사진을 가져온다.

// 갤러리에서 사진 가져오기
val takePhotoFromAlbumLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        if (result.resultCode == Activity.RESULT_OK) {
            result.data?.data?.let { uri ->
                getImageUrl(uri)
            }
        } else if (result.resultCode != Activity.RESULT_CANCELED) {
            // ..
        }
    }

갤러리에서 사진을 가져오는 건 GetContent()로도 가능하지만, 이는 MIME 타입을 하나밖에 지정하지 못한다. 이미지 중에서 jpeg, png, bmp, webp 를 가져와야 한다면 StartActivityForResult()로 하고 인텐트를 따로 지정해줘야 한다.

val takePhotoFromAlbumIntent =
    Intent(Intent.ACTION_GET_CONTENT, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply {
        type = "image/*"
        action = Intent.ACTION_GET_CONTENT
        putExtra(
            Intent.EXTRA_MIME_TYPES,
            arrayOf("image/jpeg", "image/png", "image/bmp", "image/webp")
        )
        putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
    }

Multipart로 캐스팅

갤러리로부터 이미지를 가져와서 백엔드와 api 통신을 할 경우 이미지를 Multipart로 캐스팅해줘야 한다. 갤러리로부터 이미지를 가져오는 경우 Uri 타입으로 이미지가 반환되고 Uri를 Multipart로 캐스팅한다.


contentResolver.query(this, null, null, null, null)  // cursor 반환

contentResolver.query(uri, ...)는 해당 uri에 있는 데이터에 순차적으로 접근하기 위한 코드이다. 여기서는 Intent에서 설정했던 MediaStore.Images.Media.EXTERNAL_CONTENT_URI에 있는 이미지에 접근하게 된다.


val displayName = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))

OpenableColumns.DISPLAY_NAME 은 파일이나 다른 컨텐츠 프로바이더에서 열 수 있는 컨텐츠의 Column 이름 중 하나이다. 이것을 사용하면 파일의 이름을 쉽게 얻을 수 있다.


override fun writeTo(sink: BufferedSink) {
    sink.writeAll(contentResolver.openInputStream(this@asMultipart)?.source()!!)
}

이 함수의 목적은 RequestBody의 내용을 sink에 쓰는 것입니다. 이때 sink는 HTTP 요청의 바디를 보내기 위한 스트림입니다.


  • MultipartBody로 바꾸는 전체 코드

    fun Uri.asMultipart(name: String, contentResolver: ContentResolver): MultipartBody.Part? {
      return contentResolver.query(this, null, null, null, null)?.use {
          if (it.moveToNext()) {
              val displayName = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME));
              Log.d(TAG, "displayName: $displayName")
              val requestBody = object : RequestBody() {
                  override fun contentType(): MediaType? {
                      return contentResolver.getType(this@asMultipart)?.toMediaType()
                  }
    
                  override fun writeTo(sink: BufferedSink) {
                      sink.writeAll(contentResolver.openInputStream(this@asMultipart)?.source()!!)
                  }
              }
              MultipartBody.Part.createFormData(name, displayName, requestBody)
          } else {
              null
          }
      }
    }
    

API 통신

Uri.asMultipart()로 부터 반환된 MultipartBody를 통해 백엔드와 api 통신을 한다.

suspend fun getImageUrl(uri: Uri, context: Context) {
    uri.asMultipart("file", context.contentResolver)?.let { multipartBody ->
        postImageUseCase(multipartBody)
    }?.let { multipart ->
        when (val apiState = multipart.first()) {
            is ApiState.Success<*> -> {
                Log.d(TAG, "getImageUrl success: ${apiState.value as ImageUrl}")
                userInfo = userInfo.copy(imageUrl = (apiState.value).imageUrl)
            }

            is ApiState.Error -> {
                Log.d(TAG, "getImageUrl Error: ${apiState.errMsg}")
            }

            ApiState.Loading -> TODO()
        }
    }
}

Reference

Content Resolver
Compose, 갤러리에서 사진 가져오기
안드로이드 공식 홈페이지 사진 선택 도구
kotlin에서 use 사용