[Manifest-Android] 11. 컨텐츠 프로바이더
구조화된 데이터 집합에 대한 접근 관리, 앱 간 데이터 공유를 위한 표준화된 인터페이스를 제공하는 컴포넌트
다른 앱, 컴포넌트가 데이터 쿼리, 삽입, 업데이트, 삭제에 쓸 수 있는 중앙 저장소 역할을 해서 앱 간 안전하고 일관된 데이터 공유를 보장한다.
여러 앱에서 같은 데이터에 접근해야 하거나 DB, 내부 저장소 구조 노출 없이 다른 앱에 데이터를 줘야 할 경우 유용하다
컨텐츠 프로바이더의 목적
- 데이터 접근 로직을 캡슐화해서 앱 간 데이터를 쉽고 안전하게 공유할 수 있게 한다
- SQLite, 파일 시스템, 네트워크 기반 데이터 등 기본 데이터 소스를 추상화하고 데이터와 상호작용할 수 있는 통합 인터페이스를 제공한다
컨텐츠 프로바이더의 주요 구성요소
데이터 접근을 위해 URI를 사용하는데 URI는 아래 항목들로 구성된다.
- 권한(authority) : 컨텐츠 공급자를 식별하는 데 사용. 보통 앱 패키지명 뒤에 ".provider"가 붙은 형태다
- 경로(path) : 데이터 유형을 지정. /users 또는 /products 같은 형태일 수 있다
- id : 옵셔널 값. 데이터 세트 안의 특정 아이템을 나타냄
구현 방법
ContentProvider를 상속하고 아래 함수들을 재정의해야 한다.
- onCreate : 컨텐츠 프로바이더 초기화
- query
- insert
- update
- delete
- getType : 데이터의 MIME 타입 리턴
class MyContentProvider : ContentProvider() {
private lateinit var database: SQLiteDatabase
override fun onCreate(): Boolean {
database = MyDatabaseHelper(context!!).writableDatabase
return true
}
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
return database.query("users", projection, selection, selectionArgs, null, null, sortOrder)
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val id = database.insert("users", null, values)
return ContentUris.withAppendedId(uri, id)
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
return database.update("users", values, selection, selectionArgs)
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
return database.delete("users", selection, selectionArgs)
}
override fun getType(uri: Uri): String? {
return "vnd.android.cursor.dirvnd.com.example.myapp.users"
}
}
컨텐츠 프로바이더 등록하기
다른 앱에서 내가 만든 컨텐츠 프로바이더에 접근할 수 있게 하려면 매니페스트에 선언해야 한다. authorities는 앞서 말한 대로 컨텐츠 프로바이더를 고유하게 식별하는 값이다.
<provider
android:name=".MyContentProvider"
android:authorities="com.example.myapp.provider"
android:exported="true"
android:grantUriPermissions="true" />
컨텐츠 프로바이더에서 데이터에 접근
ContentResolver를 써서 다른 앱의 컨텐츠 프로바이더와 상호작용할 수 있다. 이 클래스는 데이터 쿼리, 삽입, 업데이트, 삭제 함수를 제공한다.
val contentResolver = context.contentResolver
// Query data
val cursor = contentResolver.query(
Uri.parse("content://com.example.myapp.provider/users"),
null,
null,
null,
null
)
// Insert data
val values = ContentValues().apply {
put("name", "John Doe")
put("email", "johndoe@example.com")
}
contentResolver.insert(Uri.parse("content://com.example.myapp.provider/users"), values)
컨텐츠 프로바이더 사용 사례는 아래와 같다.
- 서로 다른 앱 간 데이터 공유, 앱 시작 프로세스 중 컴포넌트 or 리소스 초기화
- 연락처, 미디어 파일, 앱 별 데이터 같은 구조화된 데이터에 대한 접근 제공
- 연락처 앱 or 파일 선택기 같은 안드로이드 시스템 기능과의 통합 활성화
- 세분화된 보안 제어를 통해 데이터 접근 허용
앱 시작 시 리소스, 구성 초기화를 위해 컨텐츠 프로바이더를 쓰는 사용 사례
컨텐츠 프로바이더의 다른 사용 사례는 앱 시작 시 리소스나 구성 초기화다. 일반적으로 이 작업들은 Application 클래스에서 수행하지만 이걸 별도의 컨텐츠 프로바이더에 캡슐화해서 문제(아마 관심사)를 더 잘 분리할 수 있다.
커스텀 컨텐츠 프로바이더를 만든 후 매니페스트에 등록하면 초기화 작업을 효율적으로 위임할 수 있다. 컨텐츠 프로바이더의 onCretae는 Application.onCreate보다 먼저 호출되기 때문에 첫 초기화를 위한 좋은 진입점이다.
파이어베이스 SDK는 커스텀 컨텐츠 프로바이더를 써서 SDK를 자동 초기화하는 게 예시다. 이렇게 하면 Application 클래스에선 FirebaseApp.initializeApp(this)를 직접 호출할 필요가 없다.
public class FirebaseInitProvider extends ContentProvider {
/** Called before {@link Application#onCreate()}. */
@Override
public boolean onCreate() {
try {
currentlyInitializing.set(true);
if (FirebaseApp.initializeApp(getContext()) == null) {
Log.i(TAG, "FirebaseApp initialization unsuccessful");
} else {
Log.i(TAG, "FirebaseApp initialization successful");
}
return false;
} finally {
currentlyInitializing.set(false);
}
}
}
그리고 매니페스트에 등록한다.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!--Although the *SdkVersion is captured in gradle build files, this is required for non gradle builds -->
<!--<uses-sdk android:minSdkVersion="21"/>-->
<application>
<provider
android:name="com.google.firebase.provider.FirebaseInitProvider"
android:authorities="${applicationId}.firebaseinitprovider"
android:directBootAware="true"
android:exported="false"
android:initOrder="100" />
</application>
</manifest>
이 패턴은 필수 리소스, 라이브러리를 앱 생명주기 초기에 자동 초기화시켜 더 깔끔하고 모듈화된 디자인을 제공한다.
다른 사용 사례는 앱 시작 시 컴포넌트를 초기화하는 방법을 제공하는 제트팩 App Startup 라이브러리다. 내부 구현에선 컨텐츠 프로바이더를 써서 initialize 인터페이스를 구현한 모든 사전 정의된 클래스를 초기화하는 InitializationProvider라는 클래스를 사용한다. 내부 구현은 아래와 같다.
/**
* The {@link ContentProvider} which discovers {@link Initializer}s in an application and
* initializes them before {@link Application#onCreate()}.
*/
public class InitializationProvider extends ContentProvider {
@Override
public final boolean onCreate() {
Context context = getContext();
if (context != null) {
Context applicationContext = context.getApplicationContext();
if (applicationContext != null) {
// Initialize all registered Initializer classes.
AppInitializer.getInstance(context).discoverAndInitialize(getClass());
} else {
StartupLogger.w("Deferring initialization because `applicationContext` is null.");
}
} else {
throw new StartupException("Context cannot be null");
}
return true;
}
}