[안드로이드] 컴포넌트 - Content Provider

참조
깡쌤의 안드로이드 프로그래밍
안드로이드 developer - Content Providers
틀린 부분을 댓글로 남겨주시면 수정하겠습니다..!!

Content Provider 구조

컨텐츠 프로바이더는 앱 간의 데이터 공유를 목적으로 사용되는 컴포넌트입니다.


컨텐츠 프로바이더의 구조는 위와 같습니다.

A앱의 데이터를 사용하기 위해서 B앱이 직접 데이터에 접근하는 것이 아니라 A앱에서 만들어준 컨텐츠 프로바이더의 함수를 이용하여 데이터를 이용하는 구조입니다. 결국 컨텐츠 프로바이더를 매개로 해서 데이터와 데이터를 이용하는 외부 앱이 직접 연결되지 않게 하려는 의도입니다. 외부 앱에서 데이터를 이용한다고 하더라도 컨텐츠 프로바이더의 함수를 이용하는 구조이므로 컨텐츠 프로바이더를 제공하는 앱에서 외부 앱에게 제공하고싶은 데이터를 제어할 수 있습니다.


Content Provider 작성

Content Provider 상속

컨텐츠 프로바이더는 ContentProvider를 상속받아 작성하며 아래의 모든 함수를 오버라이드해야합니다. 콘텐츠 프로바이더의 생명주기 함수는 onCreate() 하나만 있고 최초에 한 번만 호출됩니다. 그리고 나머지 query(), insert(), update(), delete() 함수는 외부 앱에서 필요할 때 호출되는 구조입니다.

// ContentProvider 클래스를 상속받아 작성
class ExampleContentProvider : ContentProvider() {

    // 데이터를 삭제하는 메서드
    // 리턴값으로는 삭제된 행(데이터)의 개수
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
        TODO("Implement this to handle requests to delete one or more rows")
    }
    
    // content URI에 응답하는 MIME type을 반환하는 메서드
    override fun getType(uri: Uri): String? {
        TODO(
            "Implement this to handle requests for the MIME type of the data" +
                    "at the given URI"
        )
    }

    // 새로운 데이터를 넣는 메서드
    // 리턴값은 새롭게 생긴 행(데이터)의 URI
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        TODO("Implement this to handle requests to insert a new row.")
    }

    // Provider을 초기화하는 메서드
    // ContentResolver 객체가 ContentProvider 객체에 접근하기전까지 만들어지지 않습니다
    override fun onCreate(): Boolean {
        TODO("Implement this to initialize your content provider on startup.")
    }
    
    // 데이터를 획득하는 메서드
    // 리턴되는 데이터는 Cursor 객체
    override fun query(
        uri: Uri, projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ): Cursor? {
        TODO("Implement this to handle query requests from clients.")
    }

    // 존재하는 데이터를 수정하는 메서드
    // 리턴값은 수정된 행(데이터)의 개수
    override fun update(
        uri: Uri, values: ContentValues?, selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        TODO("Implement this to handle requests to update one or more rows.")
    }
}
  • query()
    프로바이더로부터 데이터를 반환합니다. 반환되는 데이터는 Cursor 객체이고 매개변수는 아래와 같습니다.

    • uri : Uri는 프로바이더안에 있는 테이블 이름에 해당하는 테이블로 매핑
    • projection : 각 행에 포함될 column(속성)을 정의
    • selection : where절에 해당하는 구문
    • selectionArgs : where절에 들어갈 데이터(?안에 들어갈 데이터)
    • srodOrder : 정렬할 순서
  • insert()
    프로바이더로 새로운 데이터를 넣습니다. 반환되는 데이터는 새롭게 들어간 행의 URI입니다. 매개변수는 아래와 같습니다.(한번 나온 매개변수는 정의하지 않겠습니다)

    • ContentValues : 속성의 이름과 값의 쌍으로 이루어진 데이터
  • update()
    프로바이더로부터 존재하는 데이터를 업데이트 합니다. 반환되는 값은 업데이트된 데이터(행)의 개수입니다. 매개변수는 위에 나온 uri, contentValues와 selection, selectionArgs입니다.

  • delete()
    프로바이더로부터 데이터를 삭제합니다. 반환되는 값은 삭제된 삭제된 데이터(행)의 개수입니다. 매개변수는 위에 나온 uri, selection, selectionArgs입니다.

  • getType()
    content의 URI에 응답하는 MIME type을 반환합니다.

  • onCreate()
    프로바이더를 초기화합니다. ContentResolver가 이 객체에 접근하기 전까지는 Content Proider가 만들어지지 않습니다.


AndroidManifest.xml에 등록

ContentProvider 또한 컴포넌트이므로 AndroidManifest.xml에 등록해서 사용합니다.
주의할 점은 ContentProvider를 등록할 때 android:name 속성이외에 android:authorities라는 속성을 꼭 정의해야 합니다. 이 속성은 개발자 임의의 문자열이지만 유일해야 합니다. 즉 authorities의 값은 스마트폰 전체에서 콘텐츠 프로바이더를 구분하기 위한 식별자입니다.

<provider
    android:name=".ExampleContentProvider"
    android:authorities="kr.co.lee.contentproviderexample.Provider"
    android:enabled="true"
    android:exported="true"></provider>

Content Provider 이용

컨텐츠 프로바이더는 다른 컴포넌트와는 다르게 인텐트를 사용하여 실행하지 않습니다. ContentResolver를 이용해 획득하여 사용합니다. ContentResolver는 컨텐츠 프로바이더로 생성된 객체들을 담고 있는 관리자 역할의 클래스입니다.

ContentResolver에는 모든 앱의 컨텐츠 프로바이더가 등록되어 있습니다. 이때 식별자로 Uri 객체를 사용하면 됩니다. 컨텐츠 프로바이더를 식별하기 위해 사용되는 URI는 규칙이 있습니다.

content://user_dictionary/words

  • content://
    URL의 sceme은 content로 항상 존재하고 이를 통해 컨텐츠 URI로 식별합니다.

  • user_dictionary
    이 단어는 AndroidManifest.xml에 컨텐츠 프로바이더를 선언할 때 속성으로 준 authorities 값입니다.

  • words
    이 단어는 테이블의 path에 해당합니다.

참조
UriUri.Builder는 문자열로 부터 형태가 잡힌 URI를 만들기 위한 편리한 메서드들을 제공합니다.
ContentUris 클래스는 Uri에 id를 더하는 것과 같은 편리한 메서드들을 제공합니다.

ContentResolver를 이용해서 적절한 Uri 객체로 컨텐츠 프로바이더를 식별할 수 있다면 이후 컨텐츠 프로바이더를 이용해 데이터를 획득하거나 추가허는 등의 작업은 DBMS 프로그램과 유사합니다. 컨텐츠 프로바이더의 query(), insert(), update(), delete() 함수를 적절히 호출하면 됩니다.


query 예시

컨텐츠 프로바이더를 통해 다른 앱의 데이터를 받기 위해 query()를 사용하는 예제입니다.
사용하는 메서드는 ContentResolver.query()이고 반환값은 Cursor 객체이고 매개변수는 다음과 같습니다.

  • Uri uri : 찾으려는 ContentProvider의 URI로 AndroidManifest.xml에 선언한 authorities값을 선언합니다.

  • String[] projection : 반환받으려는 속성들의 리스트입니다. null값을 넣으면 모든 속성이 포함된 행(데이터)를 획득합니다.

  • String[] selection : where절을 선언하는 곳으로 조건을 명시합니다. null값을 넣으면 모든 행(데이터)를 획득하고, 만약 where절을 선언할 것이면 조건=?와 같이 명시해야 합니다.

  • String[] selectionArgs : where절의 ?에 들어갈 값으로 ?는 이 값으로 변경됩니다. where절을 선언하지 않았다면 null값을 넣으면 됩니다.

  • String sortOrder : 반환되는 행(데이터)를 정렬하는 조건입니다. null값을 넣으면 기본적인 정렬을 사용합니다.

위의 매개변수에 값을 넣은 후 ContentResolver.query()를 사용해 반환되는 Cursor 객체를 사용하면 행(데이터)가 반환됩니다. 그 객체를 사용하여 데이터를 사용하면 됩니다.

/*
 * where절의 ?에 들어갈 데이터
 */
private lateinit var selectionArgs: Array<String>

// UI에서 값 받아오기
searchString = searchWord.text.toString()

// Remember to insert code here to check for invalid or malicious input.

// 만약 searchString 값이 있다면 where절에 값 셋팅 아니면 where절 없이 모든 행 불러오기
selectionArgs = searchString?.takeIf { it.isNotEmpty() }?.let {
    selectionClause = "${UserDictionary.Words.WORD} = ?"
    arrayOf(it)
} ?: run {
    selectionClause = null
    emptyArray<String>()
}

// contentResolver.query()를 사용하여 행을 받아오기
// 반환되는 데이터는 Cursor 객체
mCursor = contentResolver.query(
UserDictionary.Words.CONTENT_URI,  // 받아오려는 데이터의 위치(URI)
projection,                       // 반환되는 행의 column
selectionClause,                  // 조건을 명시하는 곳으로 where절에 해당
selectionArgs,                    // 조건을 명시하는 where절에 ?에 해당하는 값에 해당
sortOrder                         // 정렬조건
)

// cursor 객체의 데이터 갯수를 확인하여 분기
when (mCursor?.count) {
    null -> {
        // null이면 에러헤 해당하는 것으로 에러를 처리할 코드를 작성
        
    }
    0 -> {
        // query가 성공적이지 못한 경우를 처리할 코드
    }
    else -> {
        // 데이터가 있는 경우를 처리할 코드
        mCursor?.apply {
            // word라는 column에 해당하는 인덱스값 얻기
            val index: Int = getColumnIndex(UserDictionary.Words.WORD)

            // moveToNext() 메서드를 사용하여데이터가 있으면 True를 획득 없으면 False를 획득
            while (moveToNext()) {
                // 인덱스를 사용하여 해당 인덱스에 해당하는 데이터 얻기
                newWord = getString(index)

            }
        }
    }
}

Cursor 객체의 moveToNext() 메서드는 실행되면 다음 행으로 이동하며 반환값으로 true 혹은 false를 반환합니다. 성공적으로 이동하면 true를 반환하고 데이터가 마지막이였다면 그 다음 호출시에 false를 반환합니다. 이를 while문으로 처리하여 행(데이터)를 이동하며 getString()을 사용하여 해당하는 인덱스(column의 인덱스)의 데이터를 획득합니다.


insert 예시

컨텐츠 프로바이더를 이용하여 데이터를 넣는 예시입니다.
사용하는 메서드는 ContentResolver.insert()이고 반환되는 값은 새롭게 생성된 행의 URL입니다. 매개변수는 다음과 같습니다.

  • Uri url: 데이터를 넣으려는 테이블의 URL에 해당합니다. query()메서드의 매개변수와 같습니다.

  • ContentValues values : 새롭게 삽입되는 행의 값들입니다. ContentValues는 key, value 형식으로 되어있고 key는 column의 이름을 넣으면 됩니다.

// insert의 결과를 받을 Uri 객체
lateinit var newUri: Uri

...

// 새롭게 삽입될 값들을 포함하는 ContentValues 객체 선언
val newValues = ContentValues().apply {
    // key, value 형식으로 데이터를 셋팅
    put(UserDictionary.Words.APP_ID, "example.user")
    put(UserDictionary.Words.LOCALE, "en_US")
    put(UserDictionary.Words.WORD, "insert")
    put(UserDictionary.Words.FREQUENCY, "100")

}

newUri = contentResolver.insert(
UserDictionary.Words.CONTENT_URI,   // URI
newValues                          // 삽입할 데이터
)

update 예시

컨텐츠 프로바이더를 이용하여 데이터를 수정하는 예시입니다.
사용하는 메서드는 ContentResolver.update()이고 매개변수는 다음과 같습니다.

  • Uri uri : 위에 나온 Uri와 같습니다.

  • ContentValues values : 기존의 값을 새로운 값으로 변경하기 위한 ContentValues 객체입니다.

  • String where : 변경하기 위한 행에 필터를 적용하는 것으로 where절에 해당합니다.

  • String[] selectionArgs : where절의 ?에 들어갈 값의 배열입니다.

// 업데이트되는 값을 포함하는 객체를 선언(ContentValues 클래스 사용)
val updateValues = ContentValues().apply {
    /*
     * 업데이트 할 데이터의 key와 value를 설정한다
     */
    putNull(UserDictionary.Words.LOCALE)
}

// 업데이트하려는 행(데이터)의 where절에 해당
val selectionClause: String = UserDictionary.Words.LOCALE + "LIKE ?"
// 업데이트하려는 where절의 ?에 해당하는 값
val selectionArgs: Array<String> = arrayOf("en_%")

// Defines a variable to contain the number of updated rows
// 업데이트되는 행(데이터)의 개수(반환값)
var rowsUpdated: Int = 0

...

rowsUpdated = contentResolver.update(
UserDictionary.Words.CONTENT_URI,   // URI
updateValues,                      // 업데이트하려는 column
selectionClause,                   // 업데이트하려는 행의 필터(where절)
selectionArgs                      // where절에 들어갈 ?의 값
)

delete 예시

컨텐츠 프로바이더를 이용하여 데이터를 삭제하는 예시입니다. 사용하는 메서드는 ContentResolver.delete()이고 반환값은 삭제되는 행(데이터)의 개수입니다. 매개변수는 다음과 같습니다.

  • Uri url : 삭제하려는 행의 URL

  • String where : 삭제하려는 행의 where절에 해당

  • String[] selection : where절의 ?에 해당하는 값

// 삭제하려는 행의 조건(where 절)
val selectionClause = "${UserDictionary.Words.LOCALE} LIKE ?"
// where절의 ?에 해당하는 값
val selectionArgs: Array<String> = arrayOf("user")

// 삭제되는 행의 개수
var rowsDeleted: Int = 0

...


rowsDeleted = contentResolver.delete(
UserDictionary.Words.CONTENT_URI,   // URI
selectionClause,                   // 삭제하려는 행을 명시하는 조건
selectionArgs                      // 조건의 ?에 해당하는 값
)

깃허브 예제 코드

좋은 웹페이지 즐겨찾기