Developing Myself Everyday
article thumbnail

 

이 글에서는 Android의 4대 컴포넌트인 ContentProvider에 대해 더 자세하게 알아보고자 합니다.


 

 

 

ContentProvider란?


ContentProvider는 중앙 저장소로의 데이터 엑세스를 관리합니다. ContentProvider를 사용해 다른 애플리케이션과 데이터를 공유하고 관리할 수 있습니다.

 

ContentProvider의 주요 목적은 데이터에 대한 추상화 계층을 제공하고, 다른 애플리케이션에서 데이터에 접근하는 방법을 표준화하는 것입니다.

 

Content Provider를 사용하는 상황은 다음과 같습니다.

  • 다른 애플리케이션에서 ContentProvider에 엑세스하기위한 코드를 구현
  • 나의 애플리케이션에서 ContentProvider를 새롭게 생성하여 다른 애플리케이션과 데이터 공유

 

 

 

Content Provider는 외부 애플리케이션의 데이터를 관계형 데이터베이스와 유사한 형식의 테이블에 표시합니다. 그래서 우리가 외부 애플리케이션에 대한 정보를 얻으려면 ContentProvider에 접근하면 됩니다.

 

 

 

ContentProvider로 데이터 요청


ContentProvider 내의 데이터에 접근하고자 하는 경우, 애플리케이션의 `Context`에 있는 `ContentResolver` 객체를 사용하여 클라이언트로서 Provider와 통신합니다. 

 

`ContentResolver` 객체는 ContentProvider 인터페이스를 구현한 클래스의 인스턴스인 Provider 객체와 통신합니다.

 

Provider 객체는 데이터 요청을 받고 결과를 반환합니다.

 

 

UI에서 기타 저장소에 접근하고자 할 경우에는 `CursorLoader`를 사용해 백그라운드에서 비동기식 쿼리를 실행합니다. 그럼 `CursorLoader`는 `ContentResolver`를 사용해 `ContentProvider`를 가져옵니다. 

 

 

 

query()

위에서 ContentProvider는 관계형 데이터베이스와 유사한 형식의 테이블에 정보를 저장한다고 했습니다. 행은 제공자가 수집하는 특정 데이터 유형의 인스턴스를 나타내고, 행의 각 열은 인스턴스에 대해 수집된 개별 데이터를 나타냅니다. 이를 가져오기 위해서는 query()를 사용해야 합니다.

 

query()의 주요 매개변수는 다음과 같습니다:

  • Uri: 데이터를 어떤 프로바이더에서 가져올지를 지정하는 URI입니다.
  • 프로젝션(Projection): 원하는 열들을 선택하여 어떤 데이터를 가져올지 지정합니다.
  • 선택(Selection): 데이터를 어떻게 필터링할지를 지정합니다.
  • 선택 매개변수(Selection Arguments): 선택 조건에 전달되는 매개변수입니다.
  • 정렬(Order): 결과 데이터의 정렬 순서를 지정합니다.
// Queries the UserDictionary and returns results
cursor = contentResolver.query(
        UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
        projection,                        // The columns to return for each row
        selectionClause,                   // Selection criteria
        selectionArgs.toTypedArray(),      // Selection criteria
        sortOrder                          // The sort order for the returned rows
)

 

 

 

ContentProvider 실습


그럼 실제로 ContentProvider를 사용해서 어제부터 촬영된 이미지를 갤러리에서 가져와 보는 코드를 작성해 보면서 더 자세하게 알아보도록 하겠습니다.

 

 

ContentResolver.query()

원하는 이미지를 가져오기 위해서는 ContentResolver에서 query를 잘 작성해야 합니다.

 

가장 먼저 쿼리할 데이터의 위치를 식별하도록 정의해야 합니다. 갤러리 이미지는 외부 저장소에 저장되기에 `MediaStore.Images.Media.EXTERNAL_CONTENT_URI` 을 Uri로 전달합니다.

contentResolver.query(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
)

 

 

 

Projection

검색한 결과로 반환할 열을 지정하는 문자열 배열이 바로 projection입니다. 이미지의 ID와 이름을 가져오기 위해 다음과 같이 설정합니다.

val projection = arrayOf(
    MediaStore.Images.Media._ID,
    MediaStore.Images.Media.DISPLAY_NAME
)

 

 

 

Selection

다음은 필터링 조건을 지정해야 합니다. Selection은 문자열로 SQL의 WHERE과 비슷한 역할을 합니다. 우린 사진이 촬용된 시간을 기준으로 이미지를 가져올 것이기 떄문에 DATE_TAKEN을 조건으로 넣습니다.

val selection = "${MediaStore.Images.Media.DATE_TAKEN} >= ?"

 

 

 

Selection Arguments

Selection 문자열 내의 `?` 다음에 들어갈 값이 Selection Arguments입니다. 이렇게 함으로써 SQL Injection을 방지할 수 있습니다. 

 

우리는 어제부터 찍은 사진을 원하기 때문에 Calendar로 어제 날짜를 구하고 해당 날짜보다 이후에 찍힌 이미지를 가져오기 위해 아래와 같이 코드를 설정합니다.

val millisYesterday = Calendar.getInstance().apply {
    add(Calendar.DAY_OF_YEAR, -1)
}.timeInMillis

val selectionArgs = arrayOf(millisYesterday.toString())

 

 

 

SortOrder

마지막으로 검색돈 결과를 정렬하는 방법을 지정해야 합니다. 정렬 순서는 열 이름과 정렬 방향을 결정합니다.

 

가장 최근에 찍힌 사진으로 정렬해 보겠습니다.

val sortOrder = "${MediaStore.Images.Media.DATE_TAKEN} DESC"

 

이렇게 하면 우리가 원하는 조건에 맞는 이미지를 가져오게 됩니다.

 

 

 

Cursor

ContentResolver 객체는 Provider 객체에게 데이터를 요청하고 Provider 객체는 결과를 반환합니다. 

 

바로 이 객체가 Cursor 객체입니다. Cursor 객체는 자신이 포함한 행과 열에 랜덤 읽기 엑세스를 제공합니다.

 

Cursor 메서드를 사용하여 결과에서 행을 반복하고 각 열의 데이터 유형을 결정하고 열에서 데이터를 가져오며 결과의 다른 속성을 검사할 수 있습니다.

 

Cursor 객체를 안전하게 사용하기 위해 `use`를 사용합니다. `use`를 사용하면 Cursor를 사용해 데이터를 추출하고 사용한 다음에 Cursor를 안전하기 close()하고 자원 누수를 방지할 수 있습니다.

contentResolver.query(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
)?.use { cursor ->
    
}

 

 

 

데이터 처리

우리가 원하는 값은 테이블의 행에 있는 ID와 NAME열에 있는 정보입니다. 아래의 RDBMS 테이블을 자세히 보면 첫 번째 행에는 해당 열의 데이터 유형 및 제약 조건이 있다는 것을 알 수 있습니다.

 

 

Cursor는 처음에 테이블의 첫번째 행을 가리킵니다. 그렇기에 우리는 첫번째 Cursor에서 우리가 원하는 정보가 있는 열 번호를 얻을 수 있다는 것을 알 수 있습니다. 그 방법은 아래와 같습니다.

val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID)
val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)

 

 

그리고 Cursor가 가리키는 Image를 저장할 list를 생성합니다. 이를 위해 Image 데이터 클래스를 정의하고 images List를 생성합니다.

data class Image(
    val id: Long,
    val name: String,
    val uri: Uri
)

val images = mutableListOf<Image>()

 

 

이젠 Cursor를 계속해서 움직이면서 이미지를 List에 넣어야 합니다. 이를 위해서 `moveToNext()`를 통해 Cursor를 움직입니다. `moveToNext()`는 더 이상 Cursor를 움직일 행이 없다면 false를 반환하기에 while문에 넣어 모든 행을 이동할 수 있게 합니다.

 

이전에 얻었던 열 번호를 통해 id와 name을 얻은 다음 그리고 `ContentUris.withAppendedId()`를 사용하여 이미지의 URI를 생성합니다. 이젠 이것들을 데이터 클래스에 담아 list에 추가합니다.

while (cursor.moveToNext()) {
    val id = cursor.getLong(idColumn)
    val name = cursor.getString(nameColumn)
    val uri = ContentUris.withAppendedId(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        id
    )

    images.add(Image(id, name, uri))
}

 

 

전체 use 블록은 아래와 같습니다.

contentResolver.query(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
)?.use { cursor ->
    val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID)
    val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)

    val images = mutableListOf<Image>()
    while (cursor.moveToNext()) {
        val id = cursor.getLong(idColumn)
        val name = cursor.getString(nameColumn)
        val uri = ContentUris.withAppendedId(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            id
        )

        images.add(Image(id, name, uri))
    }
    viewModel.updateImages(images)
}

 

전체 use 블록에서 `viewModel.updateImages(images)`가 보입니다. 이는 상태 관리를 통해 UI에 Images를 바로 반영하기 위한 코드입니다.

 

 

 

상태 관리를 위한 ViewModel

ViewModel은 이미지 데이터를 관리하고 UI와 데이터 간의 효율적인 통신을 지원하는 데 사용됩니다.

 

여기서는 간단하게 mutableStateOf로 images를 두고 업데이트를 하는 메서드를 두겠습니다.

class ImageViewModel: ViewModel() {
    var images by mutableStateOf(emptyList<Image>())
        private set

    fun updateImages(images: List<Image>) {
        this.images = images
    }
}

 

 

 

UI 구성

해당 예제에서는 Compose를 사용했기에 이미지의 List를 LazyColumn을 사용했습니다. Compose에 대한 설명은 생략합니다.

LazyColumn(
    modifier = Modifier.fillMaxSize()
) {
    items(viewModel.images) { image ->
        Column(
            modifier = Modifier.fillMaxWidth(),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            AsyncImage(
                model = image.uri,
                contentDescription = null
            )
            Text(text = image.name)
        }
    }
}

AsyncImage는 이미지를 비동기적으로 로드하고 표시하는 기능을 수행합니다. 이를 사용하기 위해서는  implementation("io.coil-kt:coil-compose:2.4.0") 을 gradle에 추가해야 합니다.

 

 

 

권한 설정

마지막으로 이미지에 접근할 수 있게 권한을 설정합니다. 

 

아래의 Permission을 Manifest에 추가하고

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

 

아래의 requestPermissions를 onCreate() 안에 추가하면 해당 액티비티가 create될 때 권한을 요청합니다.

ActivityCompat.requestPermissions(
    this,
    arrayOf(Manifest.permission.READ_MEDIA_IMAGES),
    0
)

 

 

이제 직접 실행해서 결과를 확인해볼 수 있습니다.

 

 

 

 

Reference

 

Kotlin extensions use를 알아보고, 사용법을 알아보자. |

I’m an Android Developer.

thdev.tech

 

[Kotlin] 자바의 try-with-resource 구문과 코틀린의 use 함수 · Challengist

[Kotlin] 자바의 try-with-resource 구문과 코틀린의 use 함수 01 Nov 2019 | Kotlin 자바의 try with resources문 자원 입출력을 수행할 때 메모리 누수를 방지하기 위해 아래와 같이 try-finally 구문을 사용할 수 있

shinjekim.github.io

 

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!