Content Providers Why?
왜 Content Providers를 사용하는가? 묻는 것은 이상한것이 아닙니다 데이터를 안전하게 공유하고 효율적으로 app의 경계선을 넘어서 파일들이나 또는 정말 어떤 다른 것과 같이 넘으려고 하는 것을 허용하기 때문이다 캘린더, SMS와 같은 것은 Shared content providers를 이용하여 작동된다 Content Providers를 설계하는것은 나중에 다른 App에 날씨 데이터를 공유 하기 위해서 입니다
만약 공유할 필요가 없으면 넘어 가면 됩니다! 하지만, 우리는 공부가 목적이닌깐 만듭시다!!
content providers
ContentProvider를 이용해서 widgets, Search가 작동하는 방법입니다 APIs의 상당량은 syncing, querying data, UI의 updating을 적절히 하는 과정의 최적화 설계가 되어있습니다 Syncadapter, Cursorloader를 나중에 이용하게 됩니다 Content Observer를 이용해서 UI를 자동적으로 업데이트 하게 만들 수 있습니다
Content Uri
우리는 이제 실제로 작업을 시작해야합니다 다음과 같은 순서로 진행을 합니다
Application 에서 지원하는 URI를 결정합니다
Contract의 내용을 추가합니다
URI를 각각 지원하는 UriMatcher를 사용합니다
ContentProvider의 6개의 함수를 구현합니다
1. Application 에서 지원하는 URI를 결정합니다 1 content://com.study.sunshine.app/weather/Seoul,kr
다음과 같이 Seoul지역의 날씨를 조회하는 URI를 사용합니다
커서를 통해서 다음 URI의 반환값을 줍니다
1 2 3 content://com.study.sunshine.app/weather/Busan,kr content://com.study.sunshine.app/weather/Tokyo,jp
다음과 같이 URI를 변경하면 다른 국가, 다른 지역의 날씨 정보를 가져올 수 있습니다
SCHEME : URI에 사용되는 프로토콜을 식별합니다
AUTHORITY : 콘텐츠를 찾는 데 사용되는 고유 글자, 거의 항상 Application의 패키지 이름으로 합니다
데이터베이스의 테이블을 가리키는 위치입니다
QUERY : 테이블의 원하는 쿼리 데이터를 넣는 것 입니다
다음과 같은 형태로 일반적인 쿼리 형태를 취할 수도 있습니다 DATE -> 1970. 01. 01. 00:00:00 을 기준으로 합니다
다음과 같은 URI를 정했습니다
2번은 우리의 첫페이지에서 날씨 예보를 보는데 사용하겠고 3번은 상세 날씨보기에서 사용을 합니다
1,4번은 데이터 삽입, 수정, 삭제에 사용합니다 기본 URI를 잡아두면 테스팅할때 매우 유용합니다
1 2 3 4 5 6 7 8 static final int WEATHER = 100 ;static final int WEATHER_WITH_LOCATION = 101 ;static final int WEATHER_WITH_LOCATION_AND_DATE = 102 ;static final int LOCATION = 300 ;
androidTest - data 폴더TestProvider.java TestUriMatcher.java TestWeatherContract.java
androidTest 폴더TestFetchWeatherTask.java
기본package data 폴더WeatherProvider.java WeatherContract.java
WeatherContract 에 URI 관련 정보를 추가했습니다
1 2 3 public static final String CONTENT_AUTHORITY = "본인패키지명.앱이름.app" ;public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
CONTENT_AUTHORITY를 이용해서 CONTENT_AUTHORITY_URI를 만들어 줍니다
1 2 public static final String PATH_WEATHER = "weather" ;public static final String PATH_LOCATION = "location" ;
각각 테이블을 의미합니다
1 2 3 4 5 6 7 public static Uri buildWeatherUri (long id) public static Uri buildWeatherLocation (String locationSetting) public static Uri buildWeatherLocationWithStartDate (String locationSetting, long startDate) public static Uri buildWeatherLocationWithDate (String locationSetting, long date)
EnCoding
1 2 3 4 5 public static String getLocationSettingFromUri (Uri uri) public static long getDateFromUri (Uri uri) public static long getStartDateFromUri (Uri uri)
DeCoding
UriBuilder API
연습
WeatherContract.WeatherEntry.buildWeatherLocation() 완성합니다
TestWeatherContract.testBuildWeatherLocation() 이용해서 테스트합니다
정답 1 2 3 public static Uri buildWeatherLocation (String locationSetting) { return CONTENT_URI.buildUpon().appendPath(locationSetting).build(); }
UriMatcher ContentProvider에는 UriMatcher를 쉽게 사용할 방법을 제공하고 있습니다 ContentProvider는 전달된 URI 따라서 각각의 기능을 수행합니다 우리는 4가지의 URI를 구현합니다
각각 다른 작업을 수행합니다
1 2 3 4 5 6 7 8 static final int WEATHER = 100 ;static final int WEATHER_WITH_LOCATION = 101 ;static final int WEATHER_WITH_LOCATION_AND_DATE = 102 ;static final int LOCATION = 300 ;
정해진 static final int 값과 비교하여서 작업을 수행합니다 ContentProvider에 전달된 URI의 형태를 알수있는 방법이 필요하기 때문입니다 switch - case 문을 이용해서 손 쉽게 처리할 수 있습니다 UriMatcher는 다양한 형태의 URI에 표현 구문을 일치 시킬 정규표현식과 같은 역할을 합니다
# : 숫자* : 모든 문자열
UriMatcher API
연습
WeatherProvider.buildUriMatcher() 완성합니다
TestUriMatcher.testUriMatcher() 이용해서 테스트합니다
정답 1 2 3 4 5 6 7 8 9 10 11 static UriMatcher buildUriMatcher () { final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); final String authority = WeatherContract.CONTENT_AUTHORITY; matcher.addURI(authority, WeatherContract.PATH_WEATHER, WEATHER); matcher.addURI(authority, WeatherContract.PATH_WEATHER + "/*" , WEATHER_WITH_LOCATION); matcher.addURI(authority, WeatherContract.PATH_WEATHER + "/*/#" , WEATHER_WITH_LOCATION_AND_DATE); matcher.addURI(authority, WeatherContract.PATH_LOCATION, LOCATION); return matcher; }
ContentProvider Registry UriMatcher는 ContentProvider를 구현할 때 핵심입니다!! ContentProvider를 사용하기 전에 AndroidManifest.xml에 등록을 하여야 합니다
1 2 3 <provider android:name =[CONTENT PROVIDER CLASS ] android:authorities =[CONTENT_AUTHORITY]/ >
ContentResolver라는 Android Util Class를 사용할 수 있습니다 ContentResolver는 AUTHORITY를 사용하여 클래스를 찾아 WeatherProvider를 호출합니다 AndroidManifest에 ContentProvider를 등록하여서 WeatherContract와 WeatherContract.CONTENT_AUTHORITY를 등록합시다
Manifest - Providers ContentProvider
연습
AndroidManifest에 provider를 등록하세요
TestProvider.testProviderRegistry() 이용해서 테스트합니다
정답 1 2 3 <provider android:name =".data.WeatherProvider" android:authorities ="본인패키지명.앱이름.app" />
ContentProvider 6 function AndroidManifest에 provider를 등록했습니다 이제 WeatherProvider를 코딩합시다
ContentProvider 구현이 가장 어려운 부분입니다6개의 함수를 구현해야(Override) 합니다
1 2 3 4 5 6 7 8 boolean onCreate () Cursor query (Uri uri, String[] projection,String selection, String[] selectionArgs, String sortOrder) Uri insert (Uri uri, ContentValues values) int update (Uri uri, ContentValues values, String selection, String[] selectionArgs) int delete (Uri uri,String selection, String[] selectionArgs) String getType (Uri uri)
1 2 3 4 public boolean onCreate () { mOpenHelper = new WeatherDbHelper(getContext()); return true ; }
OpenHelper를 인스턴스 등록하여서 사용합니다
1 2 3 4 5 6 7 8 9 10 11 12 public String getType (Uri uri) { final int match = sUriMatcher.match(uri); switch (match) { case WEATHER: return WeatherContract.WeatherEntry.CONTENT_TYPE; case LOCATION: return WeatherContract.LocationEntry.CONTENT_TYPE; default : throw new UnsupportedOperationException("Unknown uri: " + uri); } }
입력된 URI가 어떤 형태를 요구하는지 UriMatcher를 통해서 알려줍니다
연습
WeatherProvider.getType(Uri uri) 완성합니다
TestProvider.testGetType() 이용해서 테스트합니다
정답 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public String getType (Uri uri) { final int match = sUriMatcher.match(uri); switch (match) { case WEATHER_WITH_LOCATION_AND_DATE: return WeatherContract.WeatherEntry.CONTENT_ITEM_TYPE; case WEATHER_WITH_LOCATION: return WeatherContract.WeatherEntry.CONTENT_TYPE; case WEATHER: return WeatherContract.WeatherEntry.CONTENT_TYPE; case LOCATION: return WeatherContract.LocationEntry.CONTENT_TYPE; default : throw new UnsupportedOperationException("Unknown uri: " + uri); } }
WEATHER_WITH_LOCATION_AND_DATE 은 하나의 아이템이 나와야 합니다 WEATHER_WITH_LOCATION은 디렉토리가 나와야 합니다
1 2 public Cursor query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
query 함수는 제일 구현하기 복잡하고 어렵습니다 기본 코드에 뼈대만 잡혀 있습니다 이것을 이용해서 구현합니다
switch - case 문을 이용해서 각각의 Cursor를 반환합니다 함수 끝 부분에서 전달된 URI를 설정합니다 URI의 변화를 모니터링 하기 위해서 입니다
연습
WeatherProvider.query() 완성합니다
TestProvider.testBasicWeatherQuery(), TestProvider.testBasicLocationQueries() 이용해서 테스트합니다
정답 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 case WEATHER: { retCursor = mOpenHelper.getReadableDatabase().query( WeatherContract.WeatherEntry.TABLE_NAME, projection, selection, selectionArgs, null , null , sortOrder ); break ; } case LOCATION: { retCursor = mOpenHelper.getReadableDatabase().query( WeatherContract.LocationEntry.TABLE_NAME, projection, selection, selectionArgs, null , null , sortOrder ); break ; }
다음과 같이 우리는 테이블관의 관계를 설정했습니다 Join을 사용하여 쿼리를 구현하는 방법을 설명하겠습니다
1 private static final SQLiteQueryBuilder sWeatherByLocationSettingQueryBuilder;
SQLiteQueryBuilder는 Query를 완성하는데 도움을 줍니다
1 2 3 4 5 6 7 sWeatherByLocationSettingQueryBuilder.setTables( WeatherContract.WeatherEntry.TABLE_NAME + " INNER JOIN " + WeatherContract.LocationEntry.TABLE_NAME + " ON " + WeatherContract.WeatherEntry.TABLE_NAME + "." + WeatherContract.WeatherEntry.COLUMN_LOC_KEY + " = " + WeatherContract.LocationEntry.TABLE_NAME + "." + WeatherContract.LocationEntry._ID);
setTables는 SQL 쿼리의 내용의 일부를 설명하게 됩니다
1 2 3 private static final String sLocationSettingSelection = WeatherContract.LocationEntry.TABLE_NAME + "." + WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ? " ;
기본형 쿼리를 완성해 두고 ? 에는 우리가 전달하는 값이 들어갑니다
getWeatherByLocationSetting() getWeatherByLocationSettingAndDate()
메서드에서 사용하는 모습을 볼 수 있습니다
이제 insert를 해봅시다
다른 함수와 마찬가지로 UriMatcher를 이용해서 합니다 insert는 단순합니다 테이블에 맞는 데이터를 맞게 insert 하면 됩니다 ContentValues를 이용해서 삽입합니다
그러나 어려운 점이 존재합니다! 데이터베이스의 데이터를 삽입하면 변경된 데이터를 가질 가능성이 있는 모든 곳에 알려줘야 합니다
루트가 되는 URI에 알림을 하면 하위의 모든 URI에 알림이 됩니다 ContentResolver를 사용해서 ContentObserver에게 알려 줄 수 있다
하위 URI에만 알림을 해주면 루트 URI에는 알림이 오지 않습니다 항상 이점을 명심하고 주의해야합니다
1 2 3 4 5 6 7 8 9 case WEATHER: { normalizeDate(values); long _id = db.insert(WeatherContract.WeatherEntry.TABLE_NAME, null , values); if (_id > 0 ) returnUri = WeatherContract.WeatherEntry.buildWeatherUri(_id); else throw new android.database.SQLException("Failed to insert row into " + uri); break ; }
데이터 삽입이 정상적으로 이루어졌는지 검사를 하고 애러 처리까지 해줍니다 _id 까지 포함이 된 Uri를 만들어서 반환해 주면 끝입니다
ContentProvider를 완벽히 구현한다면 Contract, UriMatcher, query 함수 의 URI형을 다 구현해야 합니다
연습
WeatherProvider.insert() 완성합니다
TestProvider.testInsertReadProvider 이용해서 테스트합니다
정답 1 2 3 4 5 6 7 8 case LOCATION: { long _id = db.insert(WeatherContract.LocationEntry.TABLE_NAME, null , values); if (_id > 0 ) returnUri = WeatherContract.LocationEntry.buildLocationUri(_id); else throw new android.database.SQLException("Failed to insert row into " + uri); break ; }
남은 것은 update, delete입니다 insert와 비슷합니다
다만 다른 점이라고 하면 삭제된 곳이나 변경이 된곳의 _id를 알려줘야 합니다 URI가 아닌 int (_id) 라는 점 입니다
테스트를 하기 전에 다음과 같이 변경 합니다
TestDb.java
1 2 3 public void deleteAllRecords () { deleteAllRecordsFromProvider(); }
더 이상 DB로 직접 지우는 게 아니라 ContentProvider를 이용해야 하기 때문에 변경합니다
연습
WeatherProvider.update, WeatherProvider.delete 완성합니다
TestProvider.testUpdateLocation, TestProvider.testDeleteRecords 이용해서 테스트합니다
정답 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 @Override public int delete (Uri uri, String selection, String[] selectionArgs) { final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); final int match = sUriMatcher.match(uri); int rowsDeleted; if (null == selection) selection = "1" ; switch (match) { case WEATHER: rowsDeleted = db.delete( WeatherContract.WeatherEntry.TABLE_NAME, selection, selectionArgs); break ; case LOCATION: rowsDeleted = db.delete( WeatherContract.LocationEntry.TABLE_NAME, selection, selectionArgs); break ; default : throw new UnsupportedOperationException("Unknown uri: " + uri); } if (rowsDeleted != 0 ) { getContext().getContentResolver().notifyChange(uri, null ); } db.close(); return rowsDeleted; }
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 @Override public int update (Uri uri, ContentValues values, String selection, String[] selectionArgs) { final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); final int match = sUriMatcher.match(uri); int rowsUpdated; switch (match) { case WEATHER: normalizeDate(values); rowsUpdated = db.update(WeatherContract.WeatherEntry.TABLE_NAME, values, selection, selectionArgs); break ; case LOCATION: rowsUpdated = db.update(WeatherContract.LocationEntry.TABLE_NAME, values, selection, selectionArgs); break ; default : throw new UnsupportedOperationException("Unknown uri: " + uri); } if (rowsUpdated != 0 ) { getContext().getContentResolver().notifyChange(uri, null ); } db.close(); return rowsUpdated; }
우리는 다 완성 했습니다 하지만 좀 더 효율적으로 할 필요가 있습니다 Database를 다루다 보면 하나의 데이터를 삽입하는 것보다 여러개 데이터를 한번에 처리하는것이 더 빠릅니다 트랜잭션으로 그룹화 해서 처리하는게 더 효율적입니다
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 @Override public int bulkInsert (Uri uri, ContentValues[] values) { final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); final int match = sUriMatcher.match(uri); switch (match) { case WEATHER: db.beginTransaction(); int returnCount = 0 ; try { for (ContentValues value : values) { normalizeDate(value); long _id = db.insert(WeatherContract.WeatherEntry.TABLE_NAME, null , value); if (_id != -1 ) { returnCount++; } } db.setTransactionSuccessful(); } finally { db.endTransaction(); } getContext().getContentResolver().notifyChange(uri, null ); return returnCount; default : return super .bulkInsert(uri, values); } }
setTransactionSuccessful()를 호출하지 않으면 기록이 되지 않는다는 점을 명심하세여
ContentProvider를 완벽하게 다 만들었습니다
Application 에서 지원하는 URI를 결정했습니다
Contract의 UriMatcher와 관련된 내용을 추가하였습니다
URI를 각각 지원하는 UriMatcher를 사용해서 구현했습니다
ContentProvider의 6개의 함수를 구현했습니다
FetchWeatherTask Refactoring ContentProvider를 완성했으니 이제 코드를 Refactoring을 합시다 기존에 FetchWeatherTask class를 파일로 빼고 수정을 합니다
package 폴더FetchWeatherTask.java
1 2 3 4 private void updateWeather () { FetchWeatherTask fetchWeatherTask = new FetchWeatherTask(getActivity(), mForecastAdapter); }
FetchWeatherTask의 생성자를 살펴보면 다음 코드를 이해할 수 있습니다
1 2 3 4 public FetchWeatherTask (Context context, ArrayAdapter<String> forecastAdapter) { mContext = context; mForecastAdapter = forecastAdapter; }
FetchWeatherTask.addLocation() 메서드를 완성 해 봅시다 도시의 이름, 위도, 경도를 입력하게 됩니다
DB에 있는 도시이면 id 값을 찾아서 반환하고, 없는 도시이면 저장 후 반환합니다
연습
FetchWeatherTask.addLocation() 완성합니다
TestFetchWeatherTask.testAddLocation() 이용해서 테스트합니다
정답 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 32 long addLocation (String locationSetting, String cityName, double lat, double lon) { long locationId; Cursor locationCursor = mContext.getContentResolver().query( WeatherContract.LocationEntry.CONTENT_URI, new String[]{WeatherContract.LocationEntry._ID}, WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING + " = ?" , new String[]{locationSetting}, null ); if (locationCursor.moveToFirst()) { int locationIdIndex = locationCursor.getColumnIndex(WeatherContract.LocationEntry._ID); locationId = locationCursor.getLong(locationIdIndex); } else { ContentValues locationValues = new ContentValues(); locationValues.put(WeatherContract.LocationEntry.COLUMN_CITY_NAME, cityName); locationValues.put(WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING, locationSetting); locationValues.put(WeatherContract.LocationEntry.COLUMN_COORD_LAT, lat); locationValues.put(WeatherContract.LocationEntry.COLUMN_COORD_LONG, lon); Uri insertedUri = mContext.getContentResolver().insert( WeatherContract.LocationEntry.CONTENT_URI, locationValues ); locationId = ContentUris.parseId(insertedUri); } locationCursor.close(); return locationId; }
bulkInsert는 트랙잭션을 효율적으로 이용하여서 여러개의 데이터를 효율적으로 삽입할 수 있습니다
우리는 기존의 일주일 데이터가 아닌 2주일 데이터를 저장도록 합시다
연습
FetchWeatherTask.getWeatherDataFromJson() 완성합니다
TestProvider.testBulkInsert() 이용해서 테스트합니다
정답 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if (cVVector.size() > 0 ) { ContentValues[] cvArray = new ContentValues[cVVector.size()]; cVVector.toArray(cvArray); mContext.getContentResolver().bulkInsert(WeatherEntry.CONTENT_URI, cvArray); } String sortOrder = WeatherEntry.COLUMN_DATE + " ASC" ; Uri weatherForLocationUri = WeatherEntry.buildWeatherLocationWithStartDate( locationSetting, System.currentTimeMillis()); Cursor cur = mContext.getContentResolver().query(weatherForLocationUri, null , null , null , sortOrder); cVVector = new Vector<ContentValues>(cur.getCount()); if (cur.moveToFirst()) { do { ContentValues cv = new ContentValues(); DatabaseUtils.cursorRowToContentValues(cur, cv); cVVector.add(cv); } while (cur.moveToNext()); }
이제 실행하면 이주일 동안의 날씨 정보를 볼 수 있습니다
복습은 필수
ContentProvider
URI 설계
UriMatcher
ContentProvider Registry
ContentProvider 6 function
FetchWeatherTask Refactoring