출처: http://unikys.tistory.com/221
[Android] ContentProvider(컨텐트프로바이더)로 ExpandableList(확장 가능한 목록) 구현 예제 |
ContentProvider를 구현했으면 사용을 해야할 것이다.
Activity안에서 query로 데이터를 가져올 수 있지만 목록에는 CursorAdapter나 CursorTreeAdapter로 해당하는 목록에
적용시켜야할 것이다.
지금부터 ExpandableList에 CursorTreeAdapter를 사용해서 ContentProvider를 적용하는 방법을 알아볼 것이고
BaseExpandableListActivity를 확장하는 Activity안에서 작업을 할 것이다.
기본적인 ExpandableList를 사용하는 방법은 조만간 정리할 예정이다.
0. ContentProvider를 구현해서 준비
: 아래 참고
[안드로이드] 컨텐트프로바이더(ContentProvider) 예제
[안드로이드] 컨텐트프로바이더(ContentProvider) 예제
지금부터 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의 각 함수를 생성하는 부분에 대해서 집중적으로 다루는 글을 써볼 예정이다.
끝.
1. BaseExpandableListActivity를 확장하는 Activity와 ExpandableList가 들어있는 Layout을 준비
:ProjectCampusActivity.java
public class ProjectCampusActivity extends BaseExpandableListActivity { Context mContext; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mContext = this; setContentView(R.layout.main); } } |
:layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <ExpandableListView android:id="@+id/android:list" android:layout_width="match_parent" android:layout_height="wrap_content" > </ExpandableListView> </LinearLayout> |
2. CursorTreeAdapter를 확장하는 Adapter 생성
: ProjectCampusActivity의 sub class로 생성
: StateSchoolExpandableListAdapter 는 CursorTreeAdapter를 확장하고 필요한 함수들을 구현하면 된다.
public class ProjectCampusActivity extends BaseExpandableListActivity { .... public class StateSchoolExpandableListAdapter extends CursorTreeAdapter{ LayoutInflater mLayoutInflater; public StateSchoolExpandableListAdapter(Cursor cursor, Context context) { super(cursor, context); this.mLayoutInflater = LayoutInflater.from(context); }
public StateSchoolExpandableListAdapter(Context context) { super(context.getContentResolver().query(State.getListUri(), State.PROJECTION, null, null, null) , context); this.mLayoutInflater = LayoutInflater.from(context); } @Override protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) { TextView textView = (TextView)view.findViewById(R.id.textViewSchoolName); textView.setText(School.getName(cursor));
textView = (TextView)view.findViewById(R.id.textViewEventNumber); textView.setText("EventNum");
textView = (TextView)view.findViewById(R.id.textViewTodayEvent); textView.setText("TodayEvent");
} @Override protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) { TextView textView = (TextView)view.findViewById(R.id.textViewStateName); textView.setText(State.getName(cursor));
textView = (TextView)view.findViewById(R.id.textViewEventCount); textView.setText("T");
} @Override protected Cursor getChildrenCursor(Cursor groupCursor) { Long id = State.getId(groupCursor);
return getContentResolver().query(School.getListUri(id), School.PROJECTION, null, null, null); } @Override protected View newChildView(Context context, Cursor cursor, boolean isLastChild, ViewGroup parent) { View childView = mLayoutInflater.inflate(R.layout.school_item_view , null , false);
return childView; } @Override protected View newGroupView(Context context, Cursor cursor, boolean isExpanded, ViewGroup parent) { View groupView = mLayoutInflater.inflate(R.layout.state_item_view , null , false);
return groupView; }
} .... } |
: newGroupView = 그룹 뷰를 새로 생성할때 호출
: bindGroupView = 그룹 뷰에 데이터를 적용시킬때 호출
: newChildView = 자식 뷰를 새로 생성할때 호출
: bindGroupView = 자식 뷰에 데이터를 적용시킬때 호출
: getChildrenCursor = 자식 커서를 가져올때 호출 : 다시 ContentProvider의 Uri를 통해서 Cursor 생성/반환
3. Activity에 Adapter를 적용
: ProjectCampusActivity의 onCreate에서 적용
public class ProjectCampusActivity extends BaseExpandableListActivity { private static final int SCHOOL_VIEW = 0; /** Called when the activity is first created. */
StateSchoolExpandableListAdapter adapter; Context mContext; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mContext = this; setContentView(R.layout.main);
adapter = new StateSchoolExpandableListAdapter(this); this.setListAdapter(adapter);
} } |
구현 끝.
실행한 결과 아래와 같이 잘된다!
* 주의 : CursorTreeAdapter를 사용하는 경우 테이블에 아이디는 _id 로 명명해줘야한다.
끝.
'IT_Programming > Android_Java' 카테고리의 다른 글
“Time Since/Ago” Library for Android/Java (0) | 2012.11.01 |
---|---|
ContentProvider 앱 간 데이터 공유 기본 (0) | 2012.10.29 |
[펌] AndroidManifest 에 있는 Debuggable 항목 값 읽어오기 (0) | 2012.10.05 |
[펌] 동일 Task로 앱 실행하기 (0) | 2012.10.05 |
[펌] 간단히 다중 위젯의 인스턴스 확보하기 (0) | 2012.10.05 |