IT_Programming/Android_Java

ContentProvider로 ExpandableList 구현 예제 (CursorTreeAdapter 사용)

JJun ™ 2012. 10. 28. 13:53



 출처: http://unikys.tistory.com/221





[Android] ContentProvider(컨텐트프로바이더)로 ExpandableList(확장 가능한 목록) 구현 예제 

            (CursorTreeAdapter (커서 트리 아답터) 사용) 튜토리얼





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 로 명명해줘야한다.



끝.