티스토리 뷰

[Android] ContentProvider example and tutorial


지금부터 ContentProvider를 앱에 적용하는 방법을 살펴보자.

최종적인 목표는 SyncAdapter와 ContentProvider를 조합해서 사용하는 것으로 일단 ContentProvider부터 앱에 맞게 설정을 하도록 해보자.


컨텐트프로바이더의 기본에 대해서는 여기서 읽어보면 유용하다. 

http://developer.android.com/guide/topics/providers/content-provider-basics.html

이론 공부는 일단 코딩하면서 하나하나 매치 시켜나가보자.

이론적인 공부는 나중에 더 자세하게 다뤄볼 생각이다.



* 아래의 소스 코드는 처음으로 ContentProvider가 어떻게 돌아가는지 이해하기 전에 작성한 코드이므로 효율적이지 않을 수 있다. 이론적인 공부를 하고 작성한다면 더욱더 도움이 될 것이다. 이론적인 공부를 안했다면 맨 아래의 6번 항목을 읽어보고 따라간다면 도움이 될 것이다.




0. 시작하기 전에

: 로컬 데이터베이스로 SQLite를 이용할것이고 나중에 네트워크를 이용해서 중앙 서버와 동기화를 하는 것이 목표이다. 나중에 사진등을 동기화하기 위해 file data도 활용을 할 것이다.

: 테이블 = User, Event, State, School, EventMember, EventMessage, PeepsMember, Peeps, PeepsMessage

: 테이블은 1:n, n:n의 데이터들을 이루고 있다.

: 테이블에서 CursorAdapter를 이용하려면 아이디 칼럼은 _ID로 설정하는 것이 용이하다.



1. Content URI 디자인하기

: authority (provider간에 충돌을 피하기 위한 고유한 이름) = com.project_campus.provider

: path structure (보통 authority에 테이블명을 첨부하여 이루어진다) = com.project_campus.provider/user 등

: content URI ID (보통 ID는 URI의 맨 뒤에 첨부된다)


- UriMatcher를 사용하면 특정 URI패턴을 쉽게 구분할 수 있다.

: 사용할 수 있는 와일드카드로는 *, #이 있다. *은 아무 문자(또는 숫자), #는 오직 숫자

- 예 : content//com.project_campus.provier/user/# 는 맨 뒤에 아이디가 오는 패턴으로 사용될 수 있다.



2. ContentProvider 구현하기

: 여기서 고민을 해본 결과 Provider를 테이블별로 나누기 위해 update, query, insert, query는 abstract인 상태로 놔두고 나머지만 구현하기로 했다.

: 기본 스켈레톤을 만들었다. 일단 2개의 테이블에 대해서만 적용해볼 것이다.

public abstract class CampusEventsProvider extends ContentProvider {


private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

private static final int STATE = 1;

private static final int SCHOOL = 2;

private static final int SCHOOL_ID = 3;

private static final int USER = 4;

private static final int USER_ID = 5;

private static final int USER_FILTER = 6;

private static final int PEEPS = 10;

private static final int PEEPS_ID = 11;

private static final int PEEPS_FILTER = 12;


private static final int PEEPS_MEMBER = 13;

private static final int PEEPS_MESSAGE = 14;


private static final int EVENT = 20;

private static final int EVENT_ID = 21;


private static final int EVENT_MEMBER = 22;


private static final int EVENT_MESSAGE = 23;


public static final String AUTHORITY = "com.project_campus";

static

{

sUriMatcher.addURI(AUTHORITY, State.PATH, STATE);

sUriMatcher.addURI(AUTHORITY, School.PATH, SCHOOL);

sUriMatcher.addURI(AUTHORITY, School.PATH + "/#", SCHOOL_ID);

}


DatabaseManager mOpenHelper;

private SQLiteDatabase db;

@Override

public boolean onCreate() {

mOpenHelper = new DatabaseManager(getContext()) {

};

return true;

}

@Override

public String getType(Uri uri) {

switch (sUriMatcher.match(uri)){

case STATE:

return "vnd.android.cursor.dir/vnd.com.project_campus.provider.state";

case SCHOOL:

return "vnd.android.cursor.dir/vnd.com.project_campus.provider.school";

case SCHOOL_ID:

return "vnd.android.cursor.item/vnd.com.project_campus.provider.school";

default:

throw new IllegalArgumentException("Unsupported URI: " + uri);

}

}


public static UriMatcher getUriMatcher() {

return sUriMatcher;

}


}


: onCreate 함수 안에서 새로운 SQLiteOpenHelper를 생성해주도록 한다.




3. ContentProvider 내 함수들을 구현하기

: ContentProvider에는 update, query, insert, query의 함수가 있다. 이 함수들을 구현함으로써 ContentProvider가 작동을 한다.

: 위의 클래스를 확장하면서 두개의 테이블을 다루는 클래스를 만들어볼 것이다.

: school 테이블은 State 테이블을 N:1로 참조하는 테이블이고 state_id를 가지고 있다.

public class StateSchoolProvider extends CampusEventsProvider{


private String getTableName(Uri uri)

{

switch (this.getUriType(uri)){

case STATE:

return "state";

case SCHOOL:

return "school";

}

return "";

}

@Override

public int delete(Uri uri, String selection, String[] selectionArgs) {

int count=0;

String table = this.getTableName(uri);

if(this.getUriType(uri) == SCHOOL_ID){

selection = School.SCHOOL_ID + "=" + uri.getPathSegments().get(1) + (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");

}

count = this.mOpenHelper.getWritableDatabase().delete(table, selection, selectionArgs);


getContext().getContentResolver().notifyChange(uri, null);


return count;

}


@Override

public Uri insert(Uri uri, ContentValues values) {

String table = this.getTableName(uri);

long rowID = mOpenHelper.getWritableDatabase().insert(table, "", values);


//---if added successfully---

if (rowID>0)

{

Uri _uri = ContentUris.withAppendedId(uri, rowID);

getContext().getContentResolver().notifyChange(_uri, null);

return _uri;

}

throw new SQLException("Failed to insert row into " + uri);

}


@Override

public Cursor query(Uri uri, String[] projection, String selection,

String[] selectionArgs, String sortOrder) {

SQLiteQueryBuilder sqlBuilder = new SQLiteQueryBuilder();

switch(this.getUriType(uri))

{

case STATE:

sqlBuilder.setTables("state");

sortOrder = State.STATE_NAME;

break;

case SCHOOL:

sqlBuilder.setTables("school");

sqlBuilder.appendWhere(School.STATE_ID + "=" + uri.getPathSegments().get(1));

sortOrder = State.STATE_NAME;

break;

case SCHOOL_ID:

sqlBuilder.setTables("school");

sqlBuilder.appendWhere(School.STATE_ID + "=" + uri.getPathSegments().get(1) + " AND " + School.SCHOOL_ID + "=" + uri.getPathSegments().get(3));

break;

}

Cursor c = sqlBuilder.query(mOpenHelper.getWritableDatabase(), projection, selection, selectionArgs, null, null, sortOrder);


c.setNotificationUri(getContext().getContentResolver(), uri);

return c;

}


@Override

public int update(Uri uri, ContentValues values, String selection,

String[] selectionArgs) {

int count = 0;

String table = this.getTableName(uri);

switch (this.getUriType(uri))

{

case STATE:

selection = School.STATE_ID + "=" + uri.getPathSegments().get(1) + (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");

break;

case SCHOOL:

selection = School.SCHOOL_ID + "=" + uri.getPathSegments().get(3) + (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");

break;

default: 

throw new IllegalArgumentException("Unknown URI " + uri);

}

count = this.mOpenHelper.getWritableDatabase().update(table,values,selection,selectionArgs);


getContext().getContentResolver().notifyChange(uri, null);

return count;

}

public Uri getStateList()

{

return Uri.parse("content://" + AUTHORITY + "/state");

}


public Uri getSchoolList(Long stateId)

{

return Uri.parse("content://" + AUTHORITY + "/state/" + stateId);

}

}

- getTableName 함수는 uri에 따라 다른 테이블명을 리턴하는 함수이다. 위의 CampusEventProvider에서 선언한 UriMatcher를 이용한다.

- 각 함수는 CampusEventProvider에서 선언한 mOpenHelper에서 디비를 읽어와서 처리를 한다. query만 조금 눈여겨 보면 될것 같고, 나머지는 쉽게 알아볼 수 있을 것이다.

- query() 함수 : 실제로 switch문으로 나뉘어져 있어서 복잡해보이지만 하나하나 따라가보면 아주 간단하다. 테이블을 설정하고 where절이 들어왔는지, 아이디가 있는지에 따라 다른 where절을 생성하는 것이다.





4. AndroidManifest.xml 설정하기

: Activity에서 ContentProvider를 사용한다는 설정을 AndroidManifest.xml에서 설정해야한다.

<application ...


        <provider android:name=".db.StateSchoolProvider" android:authorities="@string/authority"></provider>

</application>

- @string/authority는 ContentProvider 안에서 설정한 authority를 사용하자.




5. ContentProvider 사용하기

: Activity안에서 호출하면 된다.

: 아래는 onCreate 함수 안에서 간단한 테스트 코드를 작성한 것이다.


        String[] projection = new String[]{

        "state_id" , "state_name"

        };

Cursor cur = this.getContentResolver().query(Uri.parse("content://" + CampusEventsProvider.AUTHORITY + "/state"), projection, null, null, null);

        while(cur.moveToNext())

        {

        System.out.println(cur.getLong(0) + " , " + cur.getString(1));

        }


: 그러면 무사히 결과들이 출력되는 것을 확인할 수 있다.





6. 하면서 알게 된 점. 이론적인 깨달음.

: 여기서 중요한 점이 위처럼 짜면 다수의 ContentProvider를 작성할때 문제가 발생할 수 있다는 점이다. 즉, AUTHORITY는 각 ContentProvider마다 고유한 아이디이다. ContentProvider의 최하위 항목에다가 넣어야 할 것이다. (여기서는 StateSchoolContentProvider 안에다가 넣어야할 것이다.)

: 따라서 AUTHORITY는 안드로이드 사이트에서 보이는 것처럼 패키지만으로 하는게 아니라 각각 다른 ContentProvider까지 포함시키는 것이 좋다. (여기서는 com.campus_event.state_school_provider 이런식으로 해야하는 것이다.)

: query함수를 호출할때 사용하는 Projection 은 모델 클래스에 넣어서 관리를 하는 것이 용이하다.

: 모델 클래스 안에 Uri를 생성하는 함수를 넣어두어도 편리할 것이다.

: 디자인을 잘 고려해서 Uri들도 설정하는 것이 좋다. 여기서는 state/school 과 같은 하위 개념

: ContentProvider는 반드시 필요한 경우가 아니라면 가독성을 많이 줄이므로 피하는 것도 방법이다 (부정적인 글들이 인터넷에서 많이 발견했다. 하지만 SyncAdapter를 사용하려고 한다면 적용을 하는 것이 편할 것이다. 현재 SyncAdapter 에 대해서 ContentProvider이외에 일반적으로 데이터베이스를 제대로 사용하는 예제가 없으므로.. 동기화 부분을 직접 짠다면 굳이 사용할 필요는 없을 것이다.)



나중에 ContentProvider의 각 함수를 생성하는 부분에 대해서 집중적으로 다루는 글을 써볼 예정이다.


끝.





공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함