ContentProvider는 앱이 데이터를 다른 앱과 공유하는 것을 도와줍니다. 만약 내 앱에서 ContentProvider를 구현하여 제공한다면, 다른 앱들은 ContentResolver를 통해서 내 앱에 구현된 ContentProvider에 접근할 수 있습니다. ContentProvider는 데이터베이스와 유사하게 query, insert, update, delete 등의 API를 제공합니다.
다른 앱들은 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을 해 줍니다.
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에서 확인할 수 있습니다.
프로젝트 생성
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)
}
}
}
}
코드를 보시면 SQLiteDatabase
와 SQLiteOpenHelper
등을 이용하여 구현하였습니다.
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에서 확인할 수 있습니다.
참고
Related Posts
- Android 14 - 사진/동영상 파일, 일부 접근 권한 소개
- Android - adb push, pull로 파일 복사, 다운로드
- Android 14 - 암시적 인텐트 변경사항 및 문제 해결
- Jetpack Compose - Row와 Column
- Android 13, AOSP 오픈소스 다운로드 및 빌드
- Android 13 - 세분화된 미디어 파일 권한
- Android 13에서 Notification 권한 요청, 알림 띄우기
- Android 13에서 'Access blocked: ComponentInfo' 에러 해결
- 에러 해결: android gradle plugin requires java 11 to run. you are currently using java 1.8.
- 안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제
- 안드로이드 - 코루틴으로 URL 이미지 불러오기
- Android - 진동, Vibrator, VibrationEffect 예제
- Some problems were found with the configuration of task 에러 수정
- Query method parameters should either be a type that can be converted into a database column or a List
- 우분투에서 Android 12 오픈소스 다운로드 및 빌드
- Android - ViewModel을 생성하는 방법
- Android - Transformations.map(), switchMap() 차이점
- Android - Transformations.distinctUntilChanged() 소개
- Android - TabLayout 구현 방법 (+ ViewPager2)
- Android - 휴대폰 전화번호 가져오는 방법
- Android 12 - Splash Screens 알아보기
- Android 12 - Incremental Install (Play as you Download) 소개
- Android - adb 명령어로 bugreport 로그 파일 추출
- Android - adb 명령어로 App 데이터 삭제
- Android - adb 명령어로 앱 비활성화, 활성화
- Android - adb 명령어로 특정 패키지의 PID 찾기
- Android - adb 명령어로 퍼미션 Grant 또는 Revoke
- Android - adb 명령어로 apk 설치, 삭제
- Android - adb 명령어로 특정 패키지의 프로세스 종료
- Android - adb 명령어로 screen capture 저장
- Android - adb 명령어로 System 앱 삭제, 설치
- Android - adb 명령어로 settings value 확인, 변경
- Android 12 - IntentFilter의 exported 명시적 선언
- Android - adb 명령어로 공장초기화(Factory reset)
- Android - adb logcat 명령어로 로그 출력