如何自定义一个优雅的ContentProvider
最近在code review的时候发现很多人的provider定义的不是很好,写的很粗糙 以至于代码健壮性不够好,可读性也不强
但是你既然写了content provider 就是要给别人调用的,如果provider写的漏洞百出的话 还不如不写,
要么别让别的app 对你的数据进行crud,要么就让自己的app 直接用db 来操作数据,既然要写provider,就要写的标准
优雅~~放一个provider的实例在这里,有大量注释 告诉你为什么要这么写。跟我一样有代码洁癖的人可以参考下。
1 package com.example.providertest; 2 3 import android.net.Uri; 4 import android.provider.BaseColumns; 5 6 /** 7 * 常量类 8 */ 9 public final class StudentProfile { 10 11 /** 12 * 一般来说 我们的authority都是设置成 我们这个常量类的包名+类名 13 */ 14 public static final String AUTHORITY = "com.example.providertest.StudentProfile"; 15 16 /** 17 * 注意这个构造函数 是私有的 目的就是让他不能被初始化 18 */ 19 private StudentProfile() { 20 21 } 22 23 /** 24 * 实现了这个BaseColumns接口 可以让我们少写几行代码 25 * 26 */ 27 public static final class Students implements BaseColumns { 28 /** 29 * 这个类同样也是不能被初始化的 30 */ 31 private Students() { 32 33 } 34 35 // 定义我们的表名 36 public static final String TABLE_NAME = "students"; 37 38 /** 39 * 下面开始uri的定义 40 */ 41 42 // uri的scheme部分 这个部分是固定的写法 43 private static final String SCHEME = "content://"; 44 45 // 部分学生 46 private static final String PATH_STUDENTS = "/students"; 47 48 // 某一个学生 49 private static final String PATH_STUDENTS_ID = "/students/"; 50 51 /** 52 * path这边的第几个值是指的位置 我们设置成第一个位置 53 */ 54 public static final int STUDENT_ID_PATH_POSITION = 1; 55 56 // 这个表的基本的uri格式 57 public static final Uri CONTENT_URI = Uri.parse(SCHEME + AUTHORITY 58 + PATH_STUDENTS); 59 // 某一条数据的基本uri格式 这个通常在自定義的provider的insert方法里面被调用 60 public static final Uri CONTENT_ID_URI_BASE = Uri.parse(SCHEME 61 + AUTHORITY + PATH_STUDENTS_ID); 62 63 /** 64 * 定义一下我们的mime类型 注意一下mime类型的写法 65 * 66 * 一般都是后面vnd.应用程序的包名.表名 67 */ 68 69 // 多行的mime类型 70 public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.com.example.providertest.students"; 71 // 单行的mime类型 72 public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.google.com.example.providertest.students"; 73 74 /** 75 * 既然provider提供了查询的方法 我们肯定要设置一个默认的排序方式 这里我们就默认让他根据创建的时间 来降序排序 76 */ 77 public static final String DEFAULT_SORT_ORDER = "created DESC"; 78 79 /** 80 * 下面就是表的列定义了 81 */ 82 83 // 学生的名字 84 public static final String COLUMN_NAME_NAME = "name"; 85 // 学生的年龄 86 public static final String COLUMN_NAME_AGE = "age"; 87 // 学生的学号 88 public static final String COLUMN_NAME_NUMBER = "number"; 89 // 这个学生创建的时间 90 public static final String COLUMN_NAME_CREATE_DATE = "created"; 91 // 这个学生入库以后修改的时间 92 public static final String COLUMN_NAME_MODIFICATION_DATE = "modified"; 93 94 } 95 96 }
1 package com.example.providertest; 2 3 import java.util.HashMap; 4 5 import android.content.ContentProvider; 6 import android.content.ContentUris; 7 import android.content.ContentValues; 8 import android.content.Context; 9 import android.content.UriMatcher; 10 import android.database.Cursor; 11 import android.database.SQLException; 12 import android.database.sqlite.SQLiteDatabase; 13 import android.database.sqlite.SQLiteOpenHelper; 14 import android.database.sqlite.SQLiteQueryBuilder; 15 import android.net.Uri; 16 import android.text.TextUtils; 17 import android.util.Log; 18 19 public class StudentProfileProvider extends ContentProvider { 20 21 // tag 打日志用 22 private static final String TAG = "StudentProfileProvider"; 23 24 // 数据库的名字 25 private static final String DATABASE_NAME = "students_info.db"; 26 27 // 数据库版本号 28 private static final int DATABASE_VERSION = 1; 29 30 /** 31 * A UriMatcher instance 32 */ 33 private static final UriMatcher sUriMatcher; 34 35 // 匹配成功的返回值 这里代表多行匹配成功 36 private static final int STUDENTS = 1; 37 38 // 匹配成功的返回值 这里代表多单行匹配成功 39 private static final int STUDENTS_ID = 2; 40 41 /** 42 * 注意看一下这个哈希表 这个哈希表实际上是主要为了SQLiteQueryBuilder这个类的 setProjectionMap这个方法使用的 43 * 44 * 他的值的初始化我放在静态代码块里面,这个地方实际上主要是为了多表查询而存在的 45 * 46 * 比如你要多表查询的时候 你有2个表 一个表A 一个表B 你join的时候 肯定需要重命名某个表的某个列 47 * 48 * 比如你要把表A的 name1 这个列名重命名成 a.name1 那你就可以add一个key value对,key为name1 49 * 50 * value 为a.name1 即可。当然咯 如果你不想重命名或者只是单表查询那就只需要吧key 和value 51 * 52 * 的值都写成 一样的即可 53 * 54 */ 55 private static HashMap<String, String> sStudentsProjectionMap; 56 57 // 定义数据库helper. 58 private DatabaseHelper mOpenHelper; 59 60 // 静态代码块执行 61 static { 62 63 // 先构造urimatcher 64 sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 65 66 sUriMatcher.addURI(StudentProfile.AUTHORITY, "students", STUDENTS); 67 68 // #代表任意数字 *一般代表任意文本 69 sUriMatcher.addURI(StudentProfile.AUTHORITY, "students/#", STUDENTS_ID); 70 71 // 因为我们这里是单表查询 所以这个地方key和value的值都写成固定的就可以了 72 sStudentsProjectionMap = new HashMap<String, String>(); 73 74 sStudentsProjectionMap.put(StudentProfile.Students._ID, 75 StudentProfile.Students._ID); 76 77 sStudentsProjectionMap.put(StudentProfile.Students.COLUMN_NAME_AGE, 78 StudentProfile.Students.COLUMN_NAME_AGE); 79 80 sStudentsProjectionMap.put( 81 StudentProfile.Students.COLUMN_NAME_CREATE_DATE, 82 StudentProfile.Students.COLUMN_NAME_CREATE_DATE); 83 84 sStudentsProjectionMap.put( 85 StudentProfile.Students.COLUMN_NAME_MODIFICATION_DATE, 86 StudentProfile.Students.COLUMN_NAME_MODIFICATION_DATE); 87 88 sStudentsProjectionMap.put(StudentProfile.Students.COLUMN_NAME_NAME, 89 StudentProfile.Students.COLUMN_NAME_NAME); 90 91 sStudentsProjectionMap.put(StudentProfile.Students.COLUMN_NAME_NUMBER, 92 StudentProfile.Students.COLUMN_NAME_NUMBER); 93 } 94 95 @Override 96 public boolean onCreate() { 97 // TODO Auto-generated method stub 98 mOpenHelper = new DatabaseHelper(getContext()); 99 return true; 100 } 101 102 /** 103 * 对于自定义contentprovider来说CRUD的这几个方法的写法 要尽量保证 代码优美 和 容错性高 104 * 105 */ 106 107 @Override 108 public Cursor query(Uri uri, String[] projection, String selection, 109 String[] selectionArgs, String sortOrder) { 110 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 111 qb.setTables(StudentProfile.Students.TABLE_NAME); 112 113 // 先匹配uri 114 switch (sUriMatcher.match(uri)) { 115 // 多行查询 116 case STUDENTS: 117 qb.setProjectionMap(sStudentsProjectionMap); 118 break; 119 // 单行查询 120 case STUDENTS_ID: 121 qb.setProjectionMap(sStudentsProjectionMap); 122 qb.appendWhere(StudentProfile.Students._ID 123 + "=" 124 + uri.getPathSegments().get( 125 StudentProfile.Students.STUDENT_ID_PATH_POSITION)); 126 break; 127 default: 128 throw new IllegalArgumentException("Unknown uri" + uri); 129 } 130 131 // 如果没有传orderby的值过来 那我们就使用默认的 132 String orderBy; 133 if (TextUtils.isEmpty(sortOrder)) { 134 orderBy = StudentProfile.Students.DEFAULT_SORT_ORDER; 135 } else { 136 // 如果传过来了 就使用传来的值 137 orderBy = sortOrder; 138 } 139 140 // 开始操作数据库 141 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 142 143 Cursor c = qb.query(db, projection, selection, selectionArgs, null, 144 null, orderBy); 145 146 // 这个地方要解释一下 这句语句的作用,很多人自定义provider的时候 在query方法里面都忘记 147 // 写这句话,有的人写了也不知道这句话是干嘛的,实际上这句话就是给我们的cursor加了一个观察者 148 // 有兴趣的可以看一下sdk里面这个函数的源码,非常简单。那么他的实际作用就是如果返回的cursor 149 // 被用在SimpleCursorAdapter 类似的这种adapter的话,一旦uri所对应的provider数据发生了变化 150 // 那么这个adapter里的数据是会自己变化刷新的。这句话起的就是这个作用 有兴趣的可以自己写代码 151 // 验证一下 如果把这句话删除掉的话 adapter里的数据是不会再uri更新的时候 自动更新的 152 c.setNotificationUri(getContext().getContentResolver(), uri); 153 return c; 154 } 155 156 /** 157 * 这个地方的返回值 一定要和manifest你配置activity的时候data 字段的值相同 不然会报错 158 */ 159 @Override 160 public String getType(Uri uri) { 161 switch (sUriMatcher.match(uri)) { 162 case STUDENTS: 163 return StudentProfile.Students.CONTENT_TYPE; 164 case STUDENTS_ID: 165 return StudentProfile.Students.CONTENT_ITEM_TYPE; 166 default: 167 // 注意这个地方记得不匹配的时候抛出异常信息 这样当比人调用失败的时候会知道哪里不对 168 throw new IllegalArgumentException("Unknown uri" + uri); 169 } 170 171 } 172 173 @Override 174 public Uri insert(Uri uri, ContentValues initialValues) { 175 176 if (sUriMatcher.match(uri) != STUDENTS) { 177 throw new IllegalArgumentException("Unknown URI " + uri); 178 } 179 180 ContentValues values; 181 182 if (initialValues != null) { 183 values = new ContentValues(initialValues); 184 } else { 185 values = new ContentValues(); 186 } 187 188 // 下面几行代码实际上就是告诉我们对于某些表而言 默认的字段的值 可以在insert里面自己写好 189 // 不要让调用者去手动再做重复劳动,我们应该允许调用者写入最少的字段的值 来完成db的insert 190 // 操作 191 Long now = Long.valueOf(System.currentTimeMillis()); 192 193 if (values.containsKey(StudentProfile.Students.COLUMN_NAME_CREATE_DATE) == false) { 194 values.put(StudentProfile.Students.COLUMN_NAME_CREATE_DATE, now); 195 } 196 if (values 197 .containsKey(StudentProfile.Students.COLUMN_NAME_MODIFICATION_DATE) == false) { 198 values.put(StudentProfile.Students.COLUMN_NAME_MODIFICATION_DATE, 199 now); 200 } 201 202 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 203 204 long rowId = db.insert(StudentProfile.Students.TABLE_NAME, 205 StudentProfile.Students.COLUMN_NAME_NAME, values); 206 207 if (rowId > 0) { 208 Uri stuUri = ContentUris.withAppendedId( 209 StudentProfile.Students.CONTENT_ID_URI_BASE, rowId); 210 // 用于通知所有观察者数据已经改变 211 getContext().getContentResolver().notifyChange(stuUri, null); 212 return stuUri; 213 } 214 215 // 如果插入失败也最好抛出异常 通知调用者 216 throw new SQLException("Failed to insert row into " + uri); 217 218 } 219 220 @Override 221 public int delete(Uri uri, String where, String[] whereArgs) { 222 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 223 String finalWhere; 224 225 int count; 226 227 switch (sUriMatcher.match(uri)) { 228 229 case STUDENTS: 230 count = db.delete(StudentProfile.Students.TABLE_NAME, where, 231 whereArgs); 232 break; 233 234 case STUDENTS_ID: 235 finalWhere = StudentProfile.Students._ID 236 + " = " 237 + uri.getPathSegments().get( 238 StudentProfile.Students.STUDENT_ID_PATH_POSITION); 239 240 if (where != null) { 241 finalWhere = finalWhere + " AND " + where; 242 } 243 244 count = db.delete(StudentProfile.Students.TABLE_NAME, finalWhere, 245 whereArgs); 246 break; 247 248 default: 249 throw new IllegalArgumentException("Unknown URI " + uri); 250 } 251 252 getContext().getContentResolver().notifyChange(uri, null); 253 254 return count; 255 } 256 257 @Override 258 public int update(Uri uri, ContentValues values, String where, 259 String[] whereArgs) { 260 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 261 int count; 262 String finalWhere; 263 264 switch (sUriMatcher.match(uri)) { 265 266 case STUDENTS: 267 268 count = db.update(StudentProfile.Students.TABLE_NAME, values, 269 where, whereArgs); 270 break; 271 272 case STUDENTS_ID: 273 274 finalWhere = StudentProfile.Students._ID 275 + " = " 276 + uri.getPathSegments().get( 277 StudentProfile.Students.STUDENT_ID_PATH_POSITION); 278 279 if (where != null) { 280 finalWhere = finalWhere + " AND " + where; 281 } 282 283 count = db.update(StudentProfile.Students.TABLE_NAME, values, 284 finalWhere, whereArgs); 285 break; 286 default: 287 throw new IllegalArgumentException("Unknown URI " + uri); 288 } 289 290 getContext().getContentResolver().notifyChange(uri, null); 291 292 return count; 293 } 294 295 // 自定义helper 296 static class DatabaseHelper extends SQLiteOpenHelper { 297 298 DatabaseHelper(Context context) { 299 super(context, DATABASE_NAME, null, DATABASE_VERSION); 300 } 301 302 @Override 303 public void onCreate(SQLiteDatabase db) { 304 // TODO Auto-generated method stub 305 db.execSQL("CREATE TABLE " + StudentProfile.Students.TABLE_NAME 306 + " (" + StudentProfile.Students._ID 307 + " INTEGER PRIMARY KEY," 308 + StudentProfile.Students.COLUMN_NAME_NAME + " TEXT," 309 + StudentProfile.Students.COLUMN_NAME_NUMBER + " TEXT," 310 + StudentProfile.Students.COLUMN_NAME_AGE + " INTEGER," 311 + StudentProfile.Students.COLUMN_NAME_CREATE_DATE 312 + " INTEGER," 313 + StudentProfile.Students.COLUMN_NAME_MODIFICATION_DATE 314 + " INTEGER" + ");"); 315 } 316 317 @Override 318 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 319 // TODO Auto-generated method stub 320 // 数据库升级的时候 这边的代码 不写了,看各自的业务逻辑了,一般建议大家在这个地方多打一些日志 321 } 322 323 } 324 }