Android - ContentProvider 구현 및 예제

ContentProvider는 앱이 데이터를 다른 앱과 공유하는 것을 도와줍니다. 만약 내 앱에서 ContentProvider를 구현하여 제공한다면, 다른 앱들은 ContentResolver를 통해서 내 앱에 구현된 ContentProvider에 접근할 수 있습니다. ContentProvider는 데이터베이스와 유사하게 query, insert, update, delete 등의 API를 제공합니다. ContentProvider interaction

다른 앱들은 ContentProvider가 제공하는 API들을 이용하여 데이터를 읽거나 저장, 삭제할 수 있습니다. 물론 내부적으로 SQLite와 같은 데이터베이스를 사용하여 데이터를 관리해야 합니다. ContentProvider는 다른 앱과 데이터를 공유하기 위한 인터페이스라고 생각할 수 있습니다.

ContentProvider 선언 및 접근 제한

ContentProvider는 AndroidManifest.xml에 다음과 같이 정의할 수 있습니다. android:authorities 속성은 ContentProvider의 ID와 같습니다. 다른 앱에서 내 앱의 Provider를 찾을 때 authority를 알고 있어야 합니다.

<provider
    android:name=".SampleContentProvider"
    android:authorities="com.example.contentprovidersample.provider"
    android:exported="true"
    android:permission="com.example.contentprovidersample.provider.READ_WRITE"/>

android:permission 속성을 정의하지 않으면 내 앱만 ContentProvider에 접근할 수 있습니다. 이 속성에 퍼미션을 설정하면, 이 퍼미션을 갖고 있는 앱만 내 앱의 ContentProvider에 접근할 수 있습니다.

ContentProvider 접근 방법

ContentProvider는 데이터베이스와 유사하게 query, insert, update, delete 등의 API를 제공합니다. 앱은 Provider의 authority를 알고 있으면 ContentProvider를 통해 Provider에 접근할 수 있습니다.

다음은 App에서 Provider에게 데이터를 Query하는 코드입니다. 먼저 authority로 Uri를 만들고 이 Uri를 ContentResolver의 인자로 전달하여 query할 수 있습니다. API 사용 방법은 SQLite와 같은 데이터베이스와 유사합니다. projection, selection 등을 설정하여 원하는 데이터를 찾을 수 있습니다. 그리고 리턴되는 Cursor 객체를 통해 데이터를 읽을 수 있습니다.

companion object {
    const val TABLE_NAME = "cheeses"
    const val AUTHORITY = "com.example.contentprovidersample.provider"
    val URI_CHEESE: Uri = Uri.parse(
        "content://" + AUTHORITY + "/" + TABLE_NAME)
)

val cursor = contentResolver.query(
                SampleContentProvider.URI_CHEESE, // uri
                null, // projection
                null, // selection
                null, // selectionArgs
                Cheese.COLUMN_NAME // sortOrder
            )

Uri는 content://라는 scheme에 Provider의 authority와 table 이름을 조합하여 만듭니다.

// 1
val URI_CHEESE_DIR: Uri =
  Uri.parse("content://com.example.contentprovidersample.provider/cheeses")

// 2
val URI_CHEESE_ITEM: Uri =
  Uri.parse("content://com.example.contentprovidersample.provider/cheeses/5")

위의 Uri에서 // 1은 테이블 전체를 가리키는 Uri입니다. // 2는 마지막에 아이템의 ID를 의미하는 Integer가 있습니다. 이것은 ID 5의 값을 갖고 있는 아이템을 가리키는 Uri입니다.

Uri에 따라서 아이템에 대해서 어떤 작업을 수행할지 결정할 수 있습니다.

비동기적으로 접근

Activity나 Fragment에서 ContentProvider에 비동기적으로 접근하기 위해 LoaderManager, CursorLoader를 사용할 수 있습니다. LoaderManager는 Provider에서 데이터를 모두 가져오면 LoaderCallbacks를 통해서 Callback을 해 줍니다. 또한 추가, 삭제, 업데이트 등으로 Provider의 데이터 변경이 있을 때도 Callback을 해 줍니다.

CursorLoader, ContentResolver, ContentProvider

ContentProvider의 데이터 관리

ContentProvider는 내 앱의 데이터를 외부의 앱들과 연결해주는 인터페이스입니다. 실제 데이터를 저장하고 관리하지 않습니다. 따라서, ContentProvider는 내부적으로 데이터베이스를 사용하여 데이터를 관리해야 합니다. 데이터베이스는 SQLite, MongoDB 등을 사용할 수 있습니다. 또한 Android Architecture의 Room을 이용하여 구현할 수도 있습니다.

다음은 ContentProvider의 query() 메소드입니다. 내부적으로 SQLite를 사용하여 데이터를 관리하고 있습니다. (SampleDatabase는 SQLite를 추상화한 클래스입니다. 코드는 다음에 소개합니다.)

class SampleContentProvider : ContentProvider() {

  override fun query(
      uri: Uri,
      projection: Array<out String>?,
      selection: String?,
      selectionArgs: Array<out String>?,
      sortOrder: String?
  ): Cursor? {
        val queryBuilder = SQLiteQueryBuilder()
        queryBuilder.tables = Cheese.TABLE_NAME
        val db = SampleDatabase.getInstance(context!!)
        val cursor = queryBuilder.query(
            db, projection, selection, selectionArgs, null, null, sortOrder)

ContentProvider 샘플 및 튜토리얼

지금부터 튜토리얼 방식으로 ContentProvider를 구현하는 간단한 앱을 만들어보겠습니다.

이 글에서 구현하는 Sample은 GitHub - ContentProvider Sample에서 확인할 수 있습니다.

프로젝트 생성

Empty Activity를 선택합니다. Project - Empty Activity

Kotlin을 선택하여 프로젝트를 생성합니다. Project - kotlin

ContentProvider 클래스 구현

ContentProvider를 상속하는 Provider 클래스를 만듭니다. 기본적으로 다음 메소드를 오버라이드해야 합니다. 각각의 메소드는 SQLite를 이용하여 데이터를 처리합니다.

  • onCreate()
  • query()
  • insert()
  • update()
  • delete()
  • getTypte()

SampleContentProvider.kt 파일을 생성하고 다음과 같이 구현해 줍니다. 여기서 SampleDatabase라는 클래스는 SQLite를 추상화한 클래스입니다. 구현 내용은 다음에 소개합니다.

class SampleContentProvider : ContentProvider() {

    companion object {
        const val AUTHORITY = "com.example.contentprovidersample.provider"
        val URI_CHEESE: Uri = Uri.parse(
            "content://" + AUTHORITY + "/" + Cheese.TABLE_NAME)
        const val CODE_CHEESE_DIR = 1
        const val CODE_CHEESE_ITEM = 2
    }

    private var uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
    init {
        uriMatcher.addURI(AUTHORITY, Cheese.TABLE_NAME, CODE_CHEESE_DIR)
        uriMatcher.addURI(AUTHORITY, "${Cheese.TABLE_NAME}/#", CODE_CHEESE_ITEM)
    }

    override fun onCreate(): Boolean {
        return true
    }

    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
        val code: Int = uriMatcher.match(uri)
        return when (uriMatcher.match(uri)) {
            CODE_CHEESE_DIR, CODE_CHEESE_ITEM -> {
                val queryBuilder = SQLiteQueryBuilder()
                queryBuilder.tables = Cheese.TABLE_NAME
                val db = SampleDatabase.getInstance(context!!)
                val cursor = queryBuilder.query(
                    db, projection, selection, selectionArgs, null, null, sortOrder)
                cursor.setNotificationUri(context!!.contentResolver, uri)
                cursor
            }
            else -> {
                throw java.lang.IllegalArgumentException("Unknown URI: $uri")
            }
        }
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        return when (uriMatcher.match(uri)) {
            CODE_CHEESE_DIR -> {
                val id = SampleDatabase.getInstance(context!!)
                    .insert(Cheese.TABLE_NAME, null, values)
                val insertedUri = ContentUris.withAppendedId(uri, id)
                context!!.contentResolver.notifyChange(insertedUri, null)
                insertedUri
            }
            CODE_CHEESE_ITEM -> {
                throw java.lang.IllegalArgumentException("Invalid URI, cannot insert with ID: $uri")
            }
            else -> {
                throw java.lang.IllegalArgumentException("Unknown URI: $uri")
            }
        }
    }

    override fun update(uri: Uri, values: ContentValues?, selection: String?,
                        selectionArgs: Array<out String>?): Int {
        return when (uriMatcher.match(uri)) {
            CODE_CHEESE_DIR -> {
                throw java.lang.IllegalArgumentException("Invalid URI, cannot update without ID$uri")
            }
            CODE_CHEESE_ITEM -> {
                val id = ContentUris.parseId(uri)
                val count = SampleDatabase.getInstance(context!!)
                    .update(Cheese.TABLE_NAME, values, "${Cheese.COLUMN_ID} = ?",
                        arrayOf(id.toString()))
                context!!.contentResolver.notifyChange(uri, null)
                count
            }
            else -> {
                throw java.lang.IllegalArgumentException("Unknown URI: $uri")
            }
        }
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
        return when (uriMatcher.match(uri)) {
            CODE_CHEESE_DIR -> {
                throw java.lang.IllegalArgumentException("Invalid URI, cannot update without ID: $uri")
            }
            CODE_CHEESE_ITEM -> {
                val id = ContentUris.parseId(uri)
                val count = SampleDatabase.getInstance(context!!)
                    .delete(Cheese.TABLE_NAME,
                        "${Cheese.COLUMN_ID} = ?",
                        arrayOf(id.toString()))
                context!!.contentResolver.notifyChange(uri, null)
                count
            }
            else -> {
                throw java.lang.IllegalArgumentException("Unknown URI: $uri")
            }
        }
    }

    override fun getType(uri: Uri): String? {
        return when (uriMatcher.match(uri)) {
            CODE_CHEESE_DIR -> "vnd.android.cursor.dir/$AUTHORITY.$Cheese.TABLE_NAME"
            CODE_CHEESE_ITEM -> "vnd.android.cursor.item/$AUTHORITY.$Cheese.TABLE_NAME"
            else -> throw IllegalArgumentException("Unknown URI: $uri")
        }
    }
}

Constants는 Cheese.kt 파일을 만들고 여기에 정의하였습니다.

class Cheese {
    companion object {
        const val TABLE_NAME = "cheeses"
        const val COLUMN_ID = BaseColumns._ID
        const val COLUMN_NAME = "name"

        val CHEESES = arrayOf(
            "Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
            "Babybel", "Baguette Laonnaise", "Bakers", "Baladi", "Balaton", "Bandal", "Banon"
        )
    }
}

위의 코드에서 중요한 부분을 각각 설명하겠습니다.

UriMatcher

다음 코드는 UriMatcher를 정의하는 코드입니다. UriMatcher는 Uri가 일치하는지 아닌지를 판단해주는 Utility 클래스입니다. UriMatcher.addURI()에는 비교하고 싶은 Uri를 등록하는 메소드입니다. 가장 마지막 인자는 UriMatcher.match()의 결과로, 인자로 전달된 Uri와 일치할 때 리턴하는 Integer입니다. 만약 일치하는 Uri가 아니라면 -1을 리턴합니다.

const val TABLE_NAME = "cheeses"
const val CODE_CHEESE_DIR = 1
const val CODE_CHEESE_ITEM = 2
private var uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
init {
    uriMatcher.addURI(AUTHORITY, TABLE_NAME, CODE_CHEESE_DIR)
    uriMatcher.addURI(AUTHORITY, "${TABLE_NAME}/#", CODE_CHEESE_ITEM)
}

예를 들어 위에서 정의한 UriMatcher를 이용했을 때, 아래 코드의 Uri에 대한 match()CODE_CHEESE_DIR를 리턴합니다.

val uri = Uri.parse("content://com.example.contentprovidersample.provider/cheeses")
uriMatcher.match(uri)

다른 예로, 아래 코드의 Uri에 대한 match()CODE_CHEESE_ITEM를 리턴합니다.

val uri = Uri.parse("content://com.example.contentprovidersample.provider/cheeses/5")
uriMatcher.match(uri)

UriMatcher에 대한 자세한 내용은 Android Developer - Content uri patterns 페이지를 확인해주세요.

UriMatcher를 사용하는 이유

UriMatcher는 다음과 같이 사용될 수 있습니다. 아래 코드에서 Uri가 테이블 전체를 의미하는 ..../cheeses 형태로 들어온다면 insert를 수행하고, ..../cheeses/5처럼 특정 ID가 입력된 형태로 Uri가 인자로 전달되면 Exception을 발생시키도록 구현할 수 있습니다.

override fun insert(uri: Uri, values: ContentValues?): Uri? {
    return when (uriMatcher.match(uri)) {
        CODE_CHEESE_DIR -> {
            val id = SampleDatabase.getInstance(context!!)
                .insert(Cheese.TABLE_NAME, null, values)
            val insertedUri = ContentUris.withAppendedId(uri, id)
            context!!.contentResolver.notifyChange(insertedUri, null)
            insertedUri
        }
        CODE_CHEESE_ITEM -> {
            throw java.lang.IllegalArgumentException("Invalid URI, cannot insert with ID: $uri")
        }
        else -> {
            throw java.lang.IllegalArgumentException("Unknown URI: $uri")
        }
    }
}

Manifest에 ContentProvider 정의

ContentProvider를 구현했으면 App의 AndroidManifest에 정의해야 합니다.

외부 앱에서 이 Provider에 접근을 해야 한다면 퍼미션 속성을 정의해야 합니다. 퍼미션을 갖고 있는 앱만 접근할 수 있기 때문입니다.

<permission android:name="com.example.contentprovidersample.provider.READ_WRITE"/>

<application
  <provider
      android:name=".SampleContentProvider"
      android:authorities="com.example.contentprovidersample.provider"
      android:exported="true"
      android:permission="com.example.contentprovidersample.provider.READ_WRITE"/>
</application>

위의 Manifest에서 퍼미션을 정의하였는데요. 이 퍼미션의 ProtectionLevel은 normal입니다. 즉, 아무나 <uses-permission>으로 저 퍼미션을 사용한다고 정의하면 얻을 수 있습니다. Provider는 항상 열려있지 않지만, 저 Provider가 요구하는 퍼미션을 쉽게 알 수 있기 때문에 원한다면 퍼미션을 설정하여 접근할 수 있습니다. 만약 앱과 동일한 signature로 빌드된 앱만 권한을 주고 싶다면 ProtectionLevel을 signature로 설정하시면 됩니다.

데이터베이스(SQLite) 구현

이 Sample에서 데이터베이스는 SQLite를 사용하였습니다. 데이터베이스 구현은 SampleDatabase 클래스에 추상화하였습니다.

SampleDatabase.kt 파일을 생성하고 다음과 같이 구현합니다.

class SampleDatabase {
    companion object {
        private const val DATABASE_NAME = "cheese.db"
        private const val DATABASE_VERSION = 1
        private var sInstance: DatabaseHelper? = null

        @Synchronized
        fun getInstance(context: Context): SQLiteDatabase {
            if (sInstance == null) {
                sInstance = DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION)
            }
            return sInstance!!.writableDatabase
        }

        class DatabaseHelper(
            context: Context?,
            name: String?,
            factory: SQLiteDatabase.CursorFactory?,
            version: Int) : SQLiteOpenHelper(context, name, factory, version) {
            override fun onCreate(_db: SQLiteDatabase?) {
                _db?.execSQL("CREATE TABLE ${Cheese.TABLE_NAME}"
                        + " (${Cheese.COLUMN_ID} INTEGER PRIMARY KEY AUTOINCREMENT, "
                        + " ${Cheese.COLUMN_NAME} TEXT NOT NULL);")
            }

            override fun onUpgrade(_db: SQLiteDatabase?, _oldVersion: Int, _newVersion: Int) {
                _db?.execSQL("DROP TABLE IF EXISTS ${Cheese.TABLE_NAME}")
                onCreate(_db)
            }
        }
    }
}

코드를 보시면 SQLiteDatabaseSQLiteOpenHelper 등을 이용하여 구현하였습니다. Singleton으로 구현하였기 때문에 SampleDatabase.getInstance()으로 객체를 가져올 수 있습니다.

ContentProvider를 소개하는 글이기 때문에 SQLite에 대한 것은 자세히 설명하지 않았습니다.

Activity 구현

이제 ContentProvider, Database를 모두 구현했습니다. 이제 내 앱에서 이 Provider에 접근하는 코드를 구현할 것입니다.

MainActivity.kt의 layout인 activity_main.xml을 다음과 같이 수정해 줍니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <LinearLayout
        android:id="@+id/buttonLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="5dp">
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Add"
            android:onClick="addItem">
        </Button>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Update"
            android:onClick="updateItem">
        </Button>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Remove"
            android:onClick="removeItem">
        </Button>
    </LinearLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        android:scrollbars="vertical"/>

</LinearLayout>

MainActivity.kt는 다음과 같이 수정합니다.

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"
        const val LOADER_CHEESES = 1
    }
    private var cheeseAdapter: CheeseAdapter = CheeseAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        populateInitialDataIfNeeded()

        val list = findViewById<RecyclerView>(R.id.list)
        list.layoutManager = LinearLayoutManager(list.context)
        list.adapter = cheeseAdapter

        LoaderManager.getInstance(this)
            .initLoader(`LOADER_CHEESES`, null, loaderCallbacks)
    }

    private val loaderCallbacks: LoaderManager.LoaderCallbacks<Cursor> =
        object : LoaderManager.LoaderCallbacks<Cursor> {
            override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor?> {
                return CursorLoader(
                    applicationContext,
                    SampleContentProvider.URI_CHEESE, // uri
                    arrayOf<String>(Cheese.COLUMN_NAME), // projection
                    null, // selection
                    null, // selectionArgs
                    Cheese.COLUMN_NAME // sortOrder
                )
            }

            override fun onLoadFinished(loader: Loader<Cursor?>, data: Cursor?) {
                cheeseAdapter.setCheeses(data)
            }

            override fun onLoaderReset(loader: Loader<Cursor?>) {
                cheeseAdapter.setCheeses(null)
            }
        }

    internal class CheeseAdapter : RecyclerView.Adapter<CheeseAdapter.ViewHolder?>() {
        private var cursor: Cursor? = null

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
            return ViewHolder(parent)
        }

        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            if (cursor!!.moveToPosition(position)) {
                holder.text.text = cursor!!.getString(
                    cursor!!.getColumnIndexOrThrow(Cheese.COLUMN_NAME)
                )
            }
        }

        fun setCheeses(cursor: Cursor?) {
            this.cursor = cursor
            notifyDataSetChanged()
        }

        internal class ViewHolder(parent: ViewGroup) :
            RecyclerView.ViewHolder(
                LayoutInflater.from(parent.context).inflate(
                    android.R.layout.simple_list_item_1, parent, false
                )
            ) {
            val text: TextView = itemView.findViewById(android.R.id.text1)
        }

        override fun getItemCount(): Int {
            return if (cursor == null) {
                0
            } else {
                cursor!!.count
            }
        }
    }

    private fun populateInitialDataIfNeeded() {
        val cursor = contentResolver.query(
            SampleContentProvider.URI_CHEESE,
            null,
            null,
            null,
            null
        )
        if (cursor != null && cursor.count == 0) {
            Log.d(TAG, "Add initial data")
            for (cheese in  Cheese.CHEESES) {
                val values = ContentValues()
                values.put(Cheese.COLUMN_NAME, cheese)
                contentResolver.insert(SampleContentProvider.URI_CHEESE, values)
            }
        }
    }

    fun addItem(view: View) {
        val values = ContentValues()
        values.put(Cheese.COLUMN_NAME, "New Item")
        val uri = contentResolver.insert(SampleContentProvider.URI_CHEESE, values)
        Log.d(TAG, "Added item: $uri")
    }

    fun updateItem(view: View) {
        val uri = queryAndGetOne()
        if (uri != null) {
            Log.d(TAG, "Update item: $uri")
            val values = ContentValues()
            values.put(Cheese.COLUMN_NAME, "Updated Item")
            contentResolver.update(uri, values, null, null)
        }
    }

    fun removeItem(view: View) {
        val uri = queryAndGetOne()
        if (uri != null) {
            Log.d(TAG, "Remove item: $uri")
            contentResolver.delete(
                uri,
                null,
                null
            )
        }
    }

    private fun queryAndGetOne() : Uri? {
        val cursor = contentResolver.query(
            SampleContentProvider.URI_CHEESE, // uri
            null, // projection
            null, // selection
            null, // selectionArgs
            Cheese.COLUMN_NAME // sortOrder
        )
        return if (cursor != null && cursor.count != 0) {
            cursor.moveToFirst()
            val id = cursor.getString(cursor.getColumnIndex(Cheese.COLUMN_ID))
            val name = cursor.getString(cursor.getColumnIndex(Cheese.COLUMN_NAME));
            val uri = ContentUris.withAppendedId(SampleContentProvider.URI_CHEESE, id.toLong())
            Log.d(TAG, "query and return uri: $uri (id: $id, name: $name)")
            uri
        } else {
            null
        }
    }
}

위의 코드에서 RecyclerView를 사용하기 때문에 앱의 build.gradle의 의존성에 RecyclerView 라이브러리를 추가해야 합니다.

dependencies {
    ...
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
}

LoaderManager, CursorLoader

LoaderManager는 비동기적으로 ContentProvider의 데이터를 가져오는데 도와주는 클래스입니다. LoaderManager에 다수의 Loader를 등록할 수 있으며, CursorLoader를 이용하여 ContentProvider의 데이터를 가져올 수 있습니다.

다음 코드에서 onCreateLoader()는 CursorLoader를 정의합니다. CursorLoader에는 어떤 프로바이더를 읽을지 Uri 정보와 query에 필요한 selection 등의 정보를 인자로 전달합니다. query가 완료되면 onLoadFinished()으로 데이터가 전달됩니다. Cursor에서 데이터를 읽어 RecyclerView에 아이템들이 보이도록 구현하면 됩니다.

companion object {
    const val LOADER_CHEESES = 1
}

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

    LoaderManager.getInstance(this)
        .initLoader(`LOADER_CHEESES`, null, loaderCallbacks)
}

private val loaderCallbacks: LoaderManager.LoaderCallbacks<Cursor> =
    object : LoaderManager.LoaderCallbacks<Cursor> {
        override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor?> {
            return CursorLoader(
                applicationContext,
                SampleContentProvider.URI_CHEESE, // uri
                arrayOf<String>(Cheese.COLUMN_NAME), // projection
                null, // selection
                null, // selectionArgs
                Cheese.COLUMN_NAME // sortOrder
            )
        }

        override fun onLoadFinished(loader: Loader<Cursor?>, data: Cursor?) {
            cheeseAdapter.setCheeses(data)
        }

        override fun onLoaderReset(loader: Loader<Cursor?>) {
            cheeseAdapter.setCheeses(null)
        }
    }

초기값 추가

앱을 실행하며 데이터베이스에 데이터가 없기 때문에 동작을 확인하기에 좋지는 않습니다. 다음과 같은 코드로, 데이터가 없을 때 dummy 데이터를 추가하도록 하였습니다.

private fun populateInitialDataIfNeeded() {
    val cursor = contentResolver.query(
        SampleContentProvider.URI_CHEESE,
        null,
        null,
        null,
        null
    )
    if (cursor != null && cursor.count == 0) {
        Log.d(TAG, "Add initial data")
        for (cheese in  Cheese.CHEESES) {
            val values = ContentValues()
            values.put(Cheese.COLUMN_NAME, cheese)
            contentResolver.insert(SampleContentProvider.URI_CHEESE, values)
        }
    }
}

insert(), update(), remove()

각각의 버튼이 눌릴 때, insert, update, remove를 호출하는 코드를 구현하였습니다.

fun addItem(view: View) {
    val values = ContentValues()
    values.put(Cheese.COLUMN_NAME, "New Item")
    val uri = contentResolver.insert(SampleContentProvider.URI_CHEESE, values)
    Log.d(TAG, "Added item: $uri")
}

fun updateItem(view: View) {
    val uri = queryAndGetOne()
    if (uri != null) {
        Log.d(TAG, "Update item: $uri")
        val values = ContentValues()
        values.put(Cheese.COLUMN_NAME, "Updated Item")
        contentResolver.update(uri, values, null, null)
    }
}

fun removeItem(view: View) {
    val uri = queryAndGetOne()
    if (uri != null) {
        Log.d(TAG, "Remove item: $uri")
        contentResolver.delete(
            uri,
            null,
            null
        )
    }
}

update와 remove는 이미 저장된 아이템에 대해서 수행을 하는데요. 다음과 같이 COLUMN_NAME column으로 정렬된 데이터에서 가장 첫번째 아이템을 사용하도록 구현하였습니다.

private fun queryAndGetOne() : Uri? {
    val cursor = contentResolver.query(
        SampleContentProvider.URI_CHEESE, // uri
        null, // projection
        null, // selection
        null, // selectionArgs
        Cheese.COLUMN_NAME // sortOrder
    )
    return if (cursor != null && cursor.count != 0) {
        cursor.moveToFirst()
        val id = cursor.getString(cursor.getColumnIndex(Cheese.COLUMN_ID))
        val name = cursor.getString(cursor.getColumnIndex(Cheese.COLUMN_NAME));
        val uri = ContentUris.withAppendedId(SampleContentProvider.URI_CHEESE, id.toLong())
        Log.d(TAG, "query and return uri: $uri (id: $id, name: $name)")
        uri
    } else {
        null
    }
}

정리

Room with Content Providers Sample를 참고하여 ContentProvider를 간단히 구현해보았습니다. 참고한 Sample은 Room을 사용하고 Java로 구현되어있는데요, 이것을 SQLite로 변경하고 kotlin으로 구현해보았습니다.

이 글에서 소개된 Sample은 GitHub - ContentProvider Sample에서 확인할 수 있습니다.

참고

Loading script...

Related Posts

codechachaCopyright ©2019 codechacha