对 Android 系统的SQLiteOpenHelper
和 ContentResolver
的轻量级封装,配合Rxjava使用。
github地址: https://github.com/square/sqlbrite
ps: 2017年3月15号为止,还不支持Rxjava2,有点遗憾。
在module的builde.gradle依赖加入以下,如果你没导入Rxjava,那么他会自动导入:
compile 'com.squareup.sqlbrite:sqlbrite:1.1.1'
创建一个 SqlBrite 对象,该对象是该库的入口:
SqlBrite sqlBrite = new SqlBrite.Builder().build();
通过SQLiteOpenHelper实例和调度创建BriteDatabase,由于数据库操作是个耗时操作,不建议在 UI 线程中执行的,所以这里可以添加调度器进行线程的控制Scheduler ,一般指定 Schedulers.io() 。
//根据SQLiteOpenHelper和Scheduler来创建BriteDatabase,指定调度器为io操作
BriteDatabase db = sqlBrite.wrapDatabaseHelper(helper, Schedulers.io());
如果是使用 ContentProvider 的话,需要使用 ContentResolver
来创建一个 BriteContentResolver 对象:
BriteContentResolver resolver = sqlBrite.wrapContentProvider(contentResolver, Schedulers.io());
Observable query = resolver.createQuery(/*...*/);
增删改查基本用法。
BriteDatabase.createQuery方法类似于SQLiteDatabase.rawQuery,但是它需要一个附加的表参数来监听更改。
Subscribe 返回的Observable ,它将立即通知Query运行。
Observable users = db.createQuery("users", "SELECT * FROM users");
users.subscribe(new Action1() {
@Override public void call(Query query) {
Cursor cursor = query.run();
// TODO parse data...
}
});
与传统的rawQuery不同,对指定表的更新将触发其他通知,只要您保持订阅Observable,
意思就是,当您插入,更新或删除数据时,任何subscribed的查询会立即更新新的数据。
插入方法调用的就是SqlDataBase的插入方法,这个没什么好说的了。
//根据todo_list表的id,插入数据到todo_item表,
db.insert(TodoItem.TABLE, null, new TodoItem.Builder()
.listId(groceryListId)
.description("Beer")
.build());
有5个参数
例如官方栗子里面的:
//更新数据库
db.update(TodoItem.TABLE, new TodoItem.Builder().complete(newValue).build(),
TodoItem.ID + " = ?", String.valueOf(event.id()));
}
从表中删除指定的行,有三个参数:
db.delete(TodoItem.TABLE,"");
删除表就执行sql语句了要。
在下面的例子中,重用了BriteDatabase对象“db”作为插入。所有插入,更新或删除操作都必须通过此对象才能正确通知subscribers。
Unsubscribe取消订阅退回对应的subscriber,可以停止获取更新。
final AtomicInteger queries = new AtomicInteger();
Subscription s = users.subscribe(new Action1() {
@Override public void call(Query query) {
queries.getAndIncrement();
}
});
System.out.println("Queries: " + queries.get()); // Prints 1
db.insert("users", createUser("jw", "Jake Wharton"));
db.insert("users", createUser("mattp", "Matt Precious"));
s.unsubscribe();
db.insert("users", createUser("strong", "Alec Strong"));
System.out.println("Queries: " + queries.get()); // Prints 3
使用transactions可防止数据的大量更改导致subscribers频繁调用。
final AtomicInteger queries = new AtomicInteger();
users.subscribe(new Action1() {
@Override public void call(Query query) {
queries.getAndIncrement();
}
});
System.out.println("Queries: " + queries.get()); // Prints 1
Transaction transaction = db.newTransaction();
try {
db.insert("users", createUser("jw", "Jake Wharton"));
db.insert("users", createUser("mattp", "Matt Precious"));
db.insert("users", createUser("strong", "Alec Strong"));
transaction.markSuccessful();
} finally {
transaction.end();
}
System.out.println("Queries: " + queries.get()); // Prints 2
ps: 可以用try-with-resources语法使用transaction。可以简化代码,例如:
try (BriteDatabase.Transaction transaction = db.newTransaction()){
db.insert("users", createUser("jw", "Jake Wharton"));
db.insert("users", createUser("mattp", "Matt Precious"));
db.insert("users", createUser("strong", "Alec Strong"));
transaction.markSuccessful();
}
由于查询只是常规的RxJava Observable对象,运算符也可以用于控制向订阅者发送通知的频率。
users.debounce(500, MILLISECONDS).subscribe(new Action1() {
@Override public void call(Query query) {
// TODO...
}
});
官方的demo是一个记事本备忘录的功能的一个app,名为todo。 谷歌的栗子也是这个todo。
导入了官方的demo,可以发现,里面有利用的Dagger2、AutoValue、ButterKnife等等这些包。对这些知识还不会用的朋友请先看:
Dagger2:
Android快速依赖注入框架Dagger2使用1
Android快速依赖注入框架Dagger2使用2
AutoValue:
Android AutoValue使用和扩展库
ButterKnife: ButterKnife8.5.1 使用方法教程总结
我们应该怎么分析一个app呢? 我一般的习惯是
然后就一个页面一个页面地看下去
ui目录放view: activity和fragment和适配器
外层是Dagger2: Component和Module和Application
利用Dagger2在ListsFragmnet、ItemsFragment、NewItemFragment、NewListFragment进行注入,
AppModule提供单例的Application,TodoModule里面包括了DbModule。
@Singleton
@Component(modules = TodoModule.class)
public interface TodoComponent {
void inject(ListsFragment fragment);
void inject(ItemsFragment fragment);
void inject(NewItemFragment fragment);
void inject(NewListFragment fragment);
}
在DbModule中进行了DbOpenHelper、SqlBrite、BriteDatabase的初始化,提供注入。
@Module
public final class DbModule {
//初始化提供SQLiteOpenHelper
@Provides
@Singleton
SQLiteOpenHelper provideOpenHelper(Application application) {
return new DbOpenHelper(application);
}
//初始化提供SqlBrite
@Provides
@Singleton
SqlBrite provideSqlBrite() {
return SqlBrite.create(new SqlBrite.Logger() {
@Override
public void log(String message) {
Timber.tag("Database").v(message);
}
});
}
//初始化提供BriteDatabase
@Provides
@Singleton
BriteDatabase provideDatabase(SqlBrite sqlBrite, SQLiteOpenHelper helper) {
BriteDatabase db = sqlBrite.wrapDatabaseHelper(helper, Schedulers.io());
db.setLoggingEnabled(true);
return db;
}
}
DbOpenHelper进行了表的创建和插入对应的数据:
final class DbOpenHelper extends SQLiteOpenHelper {
private static final int VERSION = 1;
//创建todo_list表的语句
private static final String CREATE_LIST = ""
+ "CREATE TABLE " + TodoList.TABLE + "("
+ TodoList.ID + " INTEGER NOT NULL PRIMARY KEY,"
+ TodoList.NAME + " TEXT NOT NULL,"
+ TodoList.ARCHIVED + " INTEGER NOT NULL DEFAULT 0"
+ ")";
//创建todo_item表的语句
private static final String CREATE_ITEM = ""
+ "CREATE TABLE " + TodoItem.TABLE + "("
+ TodoItem.ID + " INTEGER NOT NULL PRIMARY KEY,"
+ TodoItem.LIST_ID + " INTEGER NOT NULL REFERENCES " + TodoList.TABLE + "(" + TodoList.ID + "),"
+ TodoItem.DESCRIPTION + " TEXT NOT NULL,"
+ TodoItem.COMPLETE + " INTEGER NOT NULL DEFAULT 0"
+ ")";
//创建 单列索引,加速查询
private static final String CREATE_ITEM_LIST_ID_INDEX =
"CREATE INDEX item_list_id ON " + TodoItem.TABLE + " (" + TodoItem.LIST_ID + ")";
//构造器,创建数据库
public DbOpenHelper(Context context) {
super(context, "todo.db", null /* factory */, VERSION);
}
//创建数据库的时候回调
@Override
public void onCreate(SQLiteDatabase db) {
//创建对应的表和索引
db.execSQL(CREATE_LIST);
db.execSQL(CREATE_ITEM);
db.execSQL(CREATE_ITEM_LIST_ID_INDEX);
//插入数据到todo_list表,返回id
long groceryListId = db.insert(TodoList.TABLE, null, new TodoList.Builder()
.name("Grocery List")
.build());
//根据todo_list表的id,插入数据到todo_item表,
db.insert(TodoItem.TABLE, null, new TodoItem.Builder()
.listId(groceryListId)
.description("Beer")
.build());
db.insert(TodoItem.TABLE, null, new TodoItem.Builder()
.listId(groceryListId)
.description("Point Break on DVD")
.build());
db.insert(TodoItem.TABLE, null, new TodoItem.Builder()
.listId(groceryListId)
.description("Bad Boys 2 on DVD")
.build());
//下面的三列和上面的套路一样,就不贴代码了
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
这时候我就就知道应用创建了两个表: todo_list和todo_item,根据todo_list的id进行关联。就相当于todo_list表存放的是标题,todo_item存放的是所有的数据。
TodoList和TodoItem是bean类,使用了AutoValue进行bean类代码生成。
里面存放了表名、字段对应的常量,还有对应的数据获取。还有使用Builder用来生成ContentValues。
(这里贴出TodoList的代码,TodoItem的套路一样)
@AutoValue
public abstract class TodoList implements Parcelable {
public static final String TABLE = "todo_list";
public static final String ID = "_id";
public static final String NAME = "name";
public static final String ARCHIVED = "archived";
public abstract long id();
public abstract String name();
public abstract boolean archived();
//利用RXjava进行数据的获取,返回bean列表
public static Func1> MAP = new Func1>() {
@Override
public List call(final Cursor cursor) {
try {
List values = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) {
long id = Db.getLong(cursor, ID);
String name = Db.getString(cursor, NAME);
boolean archived = Db.getBoolean(cursor, ARCHIVED);
values.add(new AutoValue_TodoList(id, name, archived));
}
return values;
} finally {
cursor.close();
}
}
};
//构建器,用来生成ContentValues
public static final class Builder {
private final ContentValues values = new ContentValues();
public Builder id(long id) {
values.put(ID, id);
return this;
}
public Builder name(String name) {
values.put(NAME, name);
return this;
}
public Builder archived(boolean archived) {
values.put(ARCHIVED, archived);
return this;
}
public ContentValues build() {
return values; // TODO defensive copy?
}
}
}
接下来看回到Application初始化TodoComponent。供给Fragment进行注入。
public final class TodoApp extends Application {
private TodoComponent mainComponent;
@Override
public void onCreate() {
super.onCreate();
if (BuildConfig.DEBUG) {
Timber.plant(new Timber.DebugTree());
}
mainComponent = DaggerTodoComponent.builder().todoModule(new TodoModule(this)).build();
}
public static TodoComponent getComponent(Context context) {
return ((TodoApp) context.getApplicationContext()).mainComponent;
}
}
整个app就一个activity,其他都是Fragment。MainActivity利用了系统自带的布局进行初始化显示ListsFragment,
并且回调ListsFragment操作的接口。
public final class MainActivity extends FragmentActivity
implements ListsFragment.Listener, ItemsFragment.Listener {
// 设置内容为Listsfragment
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.add(android.R.id.content, ListsFragment.newInstance())
.commit();
}
}
//ListsFragment的item点击回调
@Override
public void onListClicked(long id) {
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left,
R.anim.slide_out_right)
.replace(android.R.id.content, ItemsFragment.newInstance(id))
.addToBackStack(null)
.commit();
}
//菜单的newList点击回调
@Override
public void onNewListClicked() {
NewListFragment.newInstance().show(getSupportFragmentManager(), "new-list");
}
/**
* 菜单的newItem点击回调
* @param listId 对应todo_list表的列的id
*/
@Override
public void onNewItemClicked(long listId) {
NewItemFragment.newInstance(listId).show(getSupportFragmentManager(), "new-item");
}
}
ListsFragment为单例,是todo_list表的数据,在初始化时候回调onAttach进行Dagger2的注入、初始化ListsAdapter适配器、开启右上角的菜单。
布局主要是一个ListView用来显示TodoList表的数据。界面实现完毕进行设置标题、查询数据库。
public final class ListsFragment extends Fragment {
//回调到MainActivity的接口
interface Listener {
//列表点击
void onListClicked(long id);
//新建列表点击
void onNewListClicked();
}
//单例实现
static ListsFragment newInstance() {
return new ListsFragment();
}
@Inject
BriteDatabase db; //注入获取BriteDatabase
//使用ButterKnife获取View
@BindView(android.R.id.list)
ListView listView;
@BindView(android.R.id.empty)
View emptyView;
//回调接口
private Listener listener;
//适配器
private ListsAdapter adapter;
//Rxjava的订阅者,在离开界面进行接触订阅
private Subscription subscription;
//初始化时候进行Dagger2的注入
@Override
public void onAttach(Activity activity) {
if (!(activity instanceof Listener)) {
throw new IllegalStateException("Activity must implement fragment Listener.");
}
super.onAttach(activity);
//Dagger2的注入
TodoApp.getComponent(activity).inject(this);
//设置菜单
setHasOptionsMenu(true);
listener = (Listener) activity;
//初始化适配器
adapter = new ListsAdapter(activity);
}
//添加菜单为 NEW LIST
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
//点击菜单回调
MenuItem item = menu.add(R.string.new_list)
.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
listener.onNewListClicked();
return true;
}
});
MenuItemCompat.setShowAsAction(item, SHOW_AS_ACTION_IF_ROOM | SHOW_AS_ACTION_WITH_TEXT);
}
//创建Fragment的布局
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.lists, container, false);
}
//view创建完毕,开始绑定Butterknife、适配器
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ButterKnife.bind(this, view);
listView.setEmptyView(emptyView);
listView.setAdapter(adapter);
}
@OnItemClick(android.R.id.list)
void listClicked(long listId) {
listener.onListClicked(listId);
}
//界面显示完毕设置标题、查询数据库
@Override
public void onResume() {
super.onResume();
getActivity().setTitle("To-Do");
subscription = db.createQuery(ListsItem.TABLES, ListsItem.QUERY)
.mapToList(ListsItem.MAPPER) //映射到ListItem的MAPPER
.observeOn(AndroidSchedulers.mainThread())//设置订阅者在主线程进行
.subscribe(adapter);
}
@Override
public void onPause() {
super.onPause();
//界面隐藏解除订阅
subscription.unsubscribe();
}
}
ListsAdapter作为数据的适配显示,实现了Rxjava的Action1
final class ListsAdapter extends BaseAdapter implements Action1<List<ListsItem>> {
private final LayoutInflater inflater;
private List items = Collections.emptyList();
public ListsAdapter(Context context) {
this.inflater = LayoutInflater.from(context);
}
//在ListsFragment里面Rxjava查询数据库完毕,在这里更新适配器
@Override
public void call(List items) {
this.items = items;
notifyDataSetChanged();
}
....(下面的就省略了)
}
ListsItem是ListsFragment的数据list的bean,里面定义了一个查询语句,这个语句可能比较复杂,意思就是 根据todo_list的_id分组
,选择_id,name,todo_listd _id对应的todo_item的数量(Sql语句不懂的先去补习一下吧)。 还有一个MAPPER映射,Rxjava的处理把Cursor转换为ListsItem。
SELECT list._id, list.name, COUNT(item._id) as item_count
FROM todo_list AS list
LEFT OUTER JOIN todo_item AS item ON list._id = item.todo_list_id
GROUP BY list._id
ItemsFragment是todo_item表的数据,存放的是所有的item。
通过在ListsFragment中点击item的时候传递todo_list表的_id来,然后ItemFragmnet根据list的_id查询得到item显示,和查询数量和todo_list的nane作为标题。
基本套路和ListsFragmnet差不多。
ItemsFragmeng用到了Rxbinding,对listView的item点击进行监听,每点击一次就更新todo_item表对应的数据,就是备忘录的任务完成了的意思。
public final class ItemsFragment extends Fragment {
private static final String KEY_LIST_ID = "list_id";
//根据todo_list_id查询todo_item表的所有的数据
private static final String LIST_QUERY = "SELECT * FROM "
+ TodoItem.TABLE
+ " WHERE "
+ TodoItem.LIST_ID
+ " = ? ORDER BY "
+ TodoItem.COMPLETE
+ " ASC";
//根据todo_list_id查询todo_item表所有的数据的总数
private static final String COUNT_QUERY = "SELECT COUNT(*) FROM "
+ TodoItem.TABLE
+ " WHERE "
+ TodoItem.COMPLETE
+ " = "
+ Db.BOOLEAN_FALSE
+ " AND "
+ TodoItem.LIST_ID
+ " = ?";
//根据_id查询todo_list表的数据的name
private static final String TITLE_QUERY =
"SELECT " + TodoList.NAME + " FROM " + TodoList.TABLE + " WHERE " + TodoList.ID + " = ?";
public interface Listener {
void onNewItemClicked(long listId);
}
public static ItemsFragment newInstance(long listId) {
Bundle arguments = new Bundle();
arguments.putLong(KEY_LIST_ID, listId);
ItemsFragment fragment = new ItemsFragment();
fragment.setArguments(arguments);
return fragment;
}
@Inject
BriteDatabase db;
@BindView(android.R.id.list)
ListView listView;
@BindView(android.R.id.empty)
View emptyView;
private Listener listener;
private ItemsAdapter adapter;
private CompositeSubscription subscriptions; //可以组合多个subscriptions,便于解除订阅
private long getListId() {
return getArguments().getLong(KEY_LIST_ID);
}
@Override
public void onAttach(Activity activity) {
if (!(activity instanceof Listener)) {
throw new IllegalStateException("Activity must implement fragment Listener.");
}
super.onAttach(activity);
TodoApp.getComponent(activity).inject(this);
setHasOptionsMenu(true);
listener = (Listener) activity;
adapter = new ItemsAdapter(activity);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
MenuItem item = menu.add(R.string.new_item)
.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
listener.onNewItemClicked(getListId());
return true;
}
});
MenuItemCompat.setShowAsAction(item, SHOW_AS_ACTION_IF_ROOM | SHOW_AS_ACTION_WITH_TEXT);
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.items, container, false);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ButterKnife.bind(this, view);
listView.setEmptyView(emptyView);
listView.setAdapter(adapter);
//监听Listview的点击,更新todo_item表的状态
RxAdapterView.itemClickEvents(listView) //
.observeOn(Schedulers.io())
.subscribe(new Action1() {
@Override
public void call(AdapterViewItemClickEvent event) {
//取相反值
boolean newValue = !adapter.getItem(event.position()).complete();
//更新数据库
db.update(TodoItem.TABLE, new TodoItem.Builder().complete(newValue).build(),
TodoItem.ID + " = ?", String.valueOf(event.id()));
}
});
}
@Override
public void onResume() {
super.onResume();
//得到todo_list表对应的_id
String listId = String.valueOf(getListId());
//创建订阅者
subscriptions = new CompositeSubscription();
//查询todo_item表的对应todo_list_id的总数
Observable itemCount = db.createQuery(TodoItem.TABLE, COUNT_QUERY, listId) //
.map(new Func1() {
@Override
public Integer call(Query query) {
Cursor cursor = query.run();
try {
if (!cursor.moveToNext()) {
throw new AssertionError("No rows");
}
return cursor.getInt(0);
} finally {
cursor.close();
}
}
});
//根据_id查询todo_list表的数据的name
Observable listName =
db.createQuery(TodoList.TABLE, TITLE_QUERY, listId).map(new Func1() {
@Override
public String call(Query query) {
Cursor cursor = query.run();
try {
if (!cursor.moveToNext()) {
throw new AssertionError("No rows");
}
return cursor.getString(0);
} finally {
cursor.close();
}
}
});
//取得对应list的名字和todo_item表对应的数量数量作为标题。
subscriptions.add(
Observable.combineLatest(listName, itemCount, new Func2() {
@Override
public String call(String listName, Integer itemCount) {
return listName + " (" + itemCount + ")";
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1() {
@Override
public void call(String title) {
getActivity().setTitle(title);
}
}));
//根据todo_list_id查询todo_item表的所有的数据,更新到适配器
subscriptions.add(db.createQuery(TodoItem.TABLE, LIST_QUERY, listId)
.mapToList(TodoItem.MAPPER)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(adapter));
}
@Override
public void onPause() {
super.onPause();
//解除订阅多个subscriptions
subscriptions.unsubscribe();
}
}
好了,SqlBrite官方的demo基本功能就这样。