数据存储方案
Android系统中主要提供了三种方式用于简单地实现数据持久化功能,文件存储、SharedPreference存储和数据库存储。当然除了这三种方式外还可以将数据保存在手机的SD卡中,前三种更简单,保存在SD卡中更安全。
-
文件存储
Context类提供了一个openFileOutput()方法和一个openFileInput()方法,它返回的是一个FileOutputStream对象和FileInputStream对象,得到这个对象之后就可以使用java流的方式将数据写入到文件中。
可以用于将数据存储带指定的文件中,它接收两个参数,第一个是文件名,注意这里指定的文件名不能包含路径,所有的文件都是默认保存到/data/data/<packagename>/files/目录下,第二个参数是文件的操作模式,主要有两种模式可选,MODE_PRIVATE和MODE_APPEND。
MODE_PRIVATE是默认的操作模式,表示当指定同样文件名的时候,所写入的内容就会覆盖原内容。
MODE_APPEND表示如果该文件存在的话,就往里面追加内容,不存在就创建新文件
其实还有两种模式,但在Android4.2之后被遗弃了,是MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE,这两种模式允许其他程序对我们的程序的文件进行读写操作,这两种模式过于危险所以被遗弃了。
看下面的例子:
public class MainActivity extends AppCompatActivity {
private EditText editText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editText=(EditText)findViewById(R.id.edit);
if(read()!=null){
editText.setText(read());
editText.setSelection(read().length());//将光标定位到文本的末尾
}
}
private String read() {
FileInputStream fis=null;
BufferedReader br=null;
StringBuilder builder=new StringBuilder();
try {
fis=openFileInput("data.txt");//获取一个FileInputStream
br=new BufferedReader(new InputStreamReader(fis));
String line="";
while ((line=br.readLine())!=null){
builder.append(line);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(br!=null){
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return builder.toString();
}
@Override
protected void onDestroy() {
super.onDestroy();
String inputText=editText.getText().toString();
save(inputText);
}
private void save(String inputText) {
FileOutputStream fos=null;
BufferedWriter bw=null;
try {
fos=openFileOutput("data.txt", Context.MODE_PRIVATE);//获取一个FileOutputStream对象
bw=new BufferedWriter(new OutputStreamWriter(fos));
bw.write(inputText);
bw.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
if(bw!=null){
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
我们创建了一个EditText,在onDestroy里面将输入的文本保存,在onCreate里面读取
-
SharedPreference存储
不同于文件存储,SharedPreference是使用键值对的方式来存储数据的,下面看它的用法。
- 要想使用SharedPreference来存储数据首先要获取到SharedPreference对象。Android中提供了三种方法获取SharedPreference对象。
- Context类中的getSharedPreference()方法
此方法接收两个参数,第一个参数用于指定SharedPreference文件的名称,如果指定的文件不存在就会创建一个,SharedPreference文件都是存在/data/data/<package name>/shared_prefs/目录下的。第二个参数用于指定操作模式,目前只有MODE_PRIVATE这一种模式可选,和直接传入0是同样的效果,它是默认的操作模式
- Context类中的getSharedPreference()方法
- Activity类中的getPreferences()方法
这个方法和Context中的getSharedPreference方法类似,但他只接收一个操作模式参数,因为使用这个方法时会自动将当前活动的类名作为SharedPreference的文件名。
- Activity类中的getPreferences()方法
- PreferenceManager类中的getDefaultSharedPreference()方法
这是一个静态方法,它接收一个Context参数,并自动使用当前应用程序的包名作为前缀来命名SHaredPreference文件。
- PreferenceManager类中的getDefaultSharedPreference()方法
- 得到SharedPreference对象之后,就可以向SharedPreference文件中存储数据了,主要分为三步:
- 调用SharedPreference的edit()方法来获取一个SharedPreference.Editor对象
- 向SharedPreference.Editor对象中添加数据。
- 调用apply()方法将添加的数据提交,从而完成数据存储。
- 读数据就比较简单了,调用SharedPreference对象的一系列get方法就行了。
看下面一个例子:
public class SharePreferenceActivity extends AppCompatActivity {
public static final String TAG = "SharePreferenceActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_share_preference);
}
public void savedata(View view) {
SharedPreferences.Editor editor=getSharedPreferences("data",MODE_PRIVATE).edit();
editor.putString("name","HouChongmu");
editor.putInt("age",22);
editor.putBoolean("married",false);
editor.apply();
}
public void showdata(View view) {
SharedPreferences preferences=getSharedPreferences("data",MODE_PRIVATE);
String name=preferences.getString("name","");
int age=preferences.getInt("age",0);
boolean married=preferences.getBoolean("married",false);
Log.d(TAG, "showdata: "+name);
Log.d(TAG, "showdata: "+age);
Log.d(TAG, "showdata: "+married);
}
}
在布局里面设置了两个Button,一个用来保存数据,一个用来读取数据。
点击保存之后我们能看到系统目录下生成了对应的文件
-
SQLite数据库存储
首先要知道Android是内置数据库的。SQLite是一款轻量级的数据库,运算速度非常快,占用资源很少,不用设置用户名和密码就可以使用,而且SQLite比一般的数据库简单得多,所以将这个轻量级数据库嵌入到系统当中,使本地持久化的功能有了一个质的飞跃。接下来看怎么使用。
创建数据库
我们首先看一下SQLiteOpenHelper类:
1.Android为了让我们能够方便地管理数据库,专门提供了一个SQLiteOpenHelper帮助类,可以对数据库进行创建和升级,但这个类是一个抽象类,使用之前就必须创建一个类去继承它并实现他的两个抽象方法,一个是onCreate方法另一个是onUpgrade方法,然后分别在这两个方法中去实现创建和升级数据库的逻辑。
- SQLiteOpenHelper中有两个构造方法可供重写,一把使用参数少一点的那个构造方法即可,这个构造方法接收4个参数,第一个参数是Context,第二个参数是数据库名,创建数据库的时候就是在这里指定数据库名字。第三个参数允许我们在查询数据的时候返回一个自定义的Cusor,一般是传入null,第四个参数是表示当前数据库的版本号,可用于对数据库进行升级操作
- SQLiteOpenHelper中还有两个重要的实例方法:getReadableDatabase()和getWritableDatabase()方法,这两个方法都可以创建和打开一个现有的数据库(如果数据库已经存在就直接打开,不存在就创建一个),并返回一个可对数据库进行读写操作的对象,此时重写的onCreate方法会执行不同的是,当数据库不可写入的时候(如磁盘已满),getReadableDatabase()方法返回的对象将以只读的方式去打开数据库,而getWritableDatabase()方法将会出现异常。
- 数据库文件都会存放在/data/data/<package name>/databases/目录下。
接下来看一个例子:
- 新建项目DatabaseTest
- 新建MyDatabaseHelper类继承SQLiteOpenHelper
package com.example.houchongmu.databasetest;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;
public class MyDatabaseHelper extends SQLiteOpenHelper {
private Context mcontext;
public static final String CREATE_BOOK = "create table Book (id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text)";
public static final String CREATE_CATEGORY="create table Category(id integer primary key autoincrement," +
"category_name text" +
"category_code integer)";
//real:从 -3.40E + 38 到 3.40E + 38 的浮动精度数字数据。
//text:可变长度的字符串。最多 2GB 字符数据。
public MyDatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
mcontext = context;
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
sqLiteDatabase.execSQL(CREATE_BOOK);
Toast.makeText(mcontext, "创建成功", Toast.LENGTH_SHORT).show();
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
//i是oldversion i1是newversion
}
}
在里面我们自定义了一个建表语句的字符串常量,在onCreate里面创建了一个Toast,如果建表成功就会有Toast提示,onUpgrade我们等会再看。
- 创建数据库
我们在布局里面设置一个Button,点击这个Button创建数据库。
- 创建数据库
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
private MyDatabaseHelper databaseHelper = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
databaseHelper = new MyDatabaseHelper(this, "BookStore", null, 3);//返回一个SQLiteDatabase对象
}
public void create_database(View view) {
databaseHelper.getWritableDatabase();//返回一个SQLiteDatabase对象
}
}
- 升级数据库
升级数据库就要用到SQLiteOpenHelper里面的onUpgrade方法,修改MyDatabaseHelper
- 升级数据库
public class MyDatabaseHelper extends SQLiteOpenHelper {
private Context mcontext;
public static final String CREATE_BOOK = "create table Book (id integer primary key autoincrement," +
"author text," +
"price real," +
"pages integer," +
"name text)";
public static final String CREATE_CATEGORY="create table Category(id integer primary key autoincrement," +
"category_name text" +
"category_code integer)";
//real:从 -3.40E + 38 到 3.40E + 38 的浮动精度数字数据。
//text:可变长度的字符串。最多 2GB 字符数据。
public MyDatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
mcontext = context;
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
sqLiteDatabase.execSQL(CREATE_BOOK);
sqLiteDatabase.execSQL(CREATE_CATEGORY);
Toast.makeText(mcontext, "创建成功", Toast.LENGTH_SHORT).show();
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
//i是oldversion i1是newversion
sqLiteDatabase.execSQL("drop table if exists Book");
sqLiteDatabase.execSQL("drop table if exists Category");
onCreate(sqLiteDatabase);
}
}
我们在onUpgrade里面添加了两个drop语句,如果发现数据库中存在Book表和Category表就删掉,然后调用了onCreate方法重建这两张表。但是怎样让onUpgrade方法执行呢?还记得构造函数里面的第四个参数吗,它表示的是当前数据库的版本号,之前是传入1,现在只要传入一个比1大的数onUpgrade方法就能得到执行。看下面:
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
private MyDatabaseHelper databaseHelper = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
databaseHelper = new MyDatabaseHelper(this, "BookStore", null, 3);//返回一个SQLiteDatabase对象
}
public void create_database(View view) {
databaseHelper.getWritableDatabase();
}
public void updata_database(View view) {
databaseHelper.getWritableDatabase();
}
}
updata_database是我们设置的一个Button,点击这个Button,onUpgrade方法就能执行,但这样做得先将之前建的表drop掉,有时候不能保证数据的完整性。
- 添加数据
SQLiteDatabase提供了一个insert方法,这个方法是专门用于添加数据的。它接收三个参数,第一个参数是表名;第二参数是用于在未指定添加数据的情况下给某些可为空的列自动赋值为NULL,一般我们用不到这个功能,直接传入null就行了;第三个参数是一个ContentValues对象,它提供了一系列的put方法重载,用于向ContentValues中添加数据,只需将表中的每个列名以及相应的待添加的数据传入就行了。看下面例子
- 添加数据
public void addData(View view) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", "The Da Vinci Code");
values.put("author", "Dan Brown");
values.put("pages", 454);
values.put("price", 19.50);
database.insert("Book", null, values);//插入第一条数据
values.clear();
values.put("name", "Android开发艺术与探索");
values.put("author", "unknown");
values.put("pages", 454);
values.put("price", 66);
database.insert("Book", null, values);
}
在布局添加了一个addData的Button,在这个点击事件里面我们首先获取到SQLiteDatabase的对象,然后使用ContentValues来对要添加的数据进行封装,这里支队四列数据进行了封装,因为创建表的时候将id设置为自增长了,它的值在入库的时候自动生成,所以不需要手动赋值了。数据封装好了以后调用insert方法添加到表中,注意这里添加了两条数据。调用了两次insert方法。
- 更新数据
SQLiteDatabase中也提供了非常好用的updata方法,用于对数据进行更新,这个方法接收四个参数,第一个参数是表名,指定更新哪张表里面的数据;第二个参数是ContentValues对象,我们将要更新的数据封装进去;第三、四个参数用于约束更新某一行或某几行的数据,不指定的话就是更新所有行。看下面
- 更新数据
public void updata_data(View view) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("pages", 501);
database.update("Book", values, "name = ?", new String[]{"The Da Vinci Code"});
}
在布局中创建了一个updata_data的Button,上面代码中update方法的第三个参数对应的是SQL语句的where部分,表示更新所有name等于?的行,?是一个占位符,可以通过第四个参数提供的一个字符串数组作为第三个参数中的每个占位符指定相应的内容。因此上面代码的意思就是,将书名为The Da Vinci Code的书的页数改为501。
- 删除数据
SQLiteDatabase中提供了一个delete方法专门用于删除数据,用法和updata方法类似。
- 删除数据
public void delete_data(View view) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete("Book", "pages > ?", new String[]{"500"});
}
上面的代码意思是删除所有页数大于500的书
- 查询数据
SQLiteDatabase专门提供了一个query方法用于对数据库的查询,这个方法的参数比较复杂:
参数一:表名
参数二:用于指定去查询哪几列 ,不指定则查询所有列
参数三、四:用于约束查询某一行或某几行的数据,不指定则默认查询所有行的数据
参数五:用于指定需要去group by的列,不指定则表示对查询结果不进行group by操作
参数六:用于对group by之后的数据进行进一步过滤,不指定则表示不进行过滤
参数七:用于指定查询结果的排序方式,不指定则表示默认的排序方式
多数情况下传入少数几个参数就可以完成查询操作了。调用query方法后返回一个Cursor对象,查询到的结果都从这个对象中取出。
看下面的例子
- 查询数据
public void query_data(View view) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
Cursor cursor = database.query("Book", null, null, null, null, null, null);
if (cursor.moveToFirst()) {
//cursor.moveToFirst():Move the cursor to the first row
do {
String name = cursor.getString(cursor.getColumnIndex("name"));
//cursor.getColumnIndex:the zero-based column index for the given column name
String author = cursor.getString(cursor.getColumnIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
double price = cursor.getDouble(cursor.getColumnIndex("price"));
Log.d(TAG, "query_data: " + name);
Log.d(TAG, "query_data: " + author);
Log.d(TAG, "query_data: " + pages);
Log.d(TAG, "query_data: " + price);
} while (cursor.moveToNext());
}
cursor.close();
}
再布局里面设置一个query_data的Button,这里的query方法非常简单,只是使用了第一个参数指明去查Book表,后面的参数全部为null。这表示希望查询表中的所有数据,查询完之后得到一个Cursor对象,接着调用它的moveToFirst方法将数据的指针移动到第一行的位置,然后进入到一个循环当中,去遍历查询每一行的数据,在这个循环中可以通过Cursor的getColumnIndex方法获取到某一列在表中对应的位置索引,然后将这个索引传入到相应的取值方法中,就可以得到从数据库中读取到的数据了,然后用log的方式打印出来,最后别忘记关闭Cursor。
关于SQLiteDatabase就讲到这
-
使用LitePal操作数据库
LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,并将我们平时开发最常用到的一些数据库功能进行了封装,所以可以不用编写SQL语句就可以完成各种建表和增删改查的操作。
- 配置LitePal,编辑build.gradle
implementation 'org.litepal.android:core:1.6.1'
1.6.1是截止目前的最新版本,附上GitHub地址:https://github.com/LitePalFramework/LitePal这样就成功把LitePal引入项目中去了
- 接下来需要配置litepal.xml文件,在app/src/main目录下新建directory,创建一个assets目录,然后在assets目录下新建一个litepal.xml文件,接着编辑这个文件:
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<dbname value="BookStore" />
<version value="1" />
<list>
</list>
</litepal>
dbname标签用于指定数据库名字;version用于指定数据库版本号,list用于指定所有的映射模型,在下面讲
- 配置LitePalApplication,修改AndroidManifest.xml文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.houchongmu.litepaltest">
<。
android:name="org.litepal.LitePalApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
加入 android:name="org.litepal.LitePalApplication",将application配置为org.litepal.LitePalApplication这样才能让LitePal所有功能都可以正常工作。
- 新建Book类
package com.example.houchongmu.litepaltest;
import org.litepal.crud.DataSupport;
public class Book extends DataSupport{
private int id;
private String name;
private String author;
private int pages;
private double price;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public int getPages() {
return pages;
}
public void setPages(int pages) {
this.pages = pages;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
这是典型的JavaBean,我们的这个类要继承DataSupport,DataSupport类是LitePal提供的,要不然后面的一些方法就失效。后面再解释。
- 这里使用<mapping>标签来声明我们要配置的映射模型类。注意一定要使用完整的类名,不管要使用多少模型类需要映射,都使用同样的方式配置在<list>标签下即可。
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<dbname value="BookStore" />
<version value="2" />
<list>
<mapping class="com.example.houchongmu.litepaltest.Book" />
<mapping class="com.example.houchongmu.litepaltest.Category"/>
</list>
</litepal>
- 创建数据库
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void create_database(View view) {
LitePal.getDatabase();//return A writable SQLiteDatabase instance
}
}
再布局里面设置一个create_database的Button,点击来创建数据库,调用了Litepal.getDatabase()方法,该方法返回的是一个writable SQLiteDatabase对象,作用跟SQLiteOpenHelper类的getWritableDatabase()方法效果是一样的,数据库存在就不创建,不存在就创建。我们点击Button后就创建了一个数据库:
- 在Book表中新加press列,与SQLiteOpenHelper不同的是前者需要把之前的表drop掉,这样很容易造成数据丢失,而LitePal只需在Book类里面添加press字段就行了
...
private String press;
...
public String getPress() {
return press;
}
public void setPress(String press) {
this.press = press;
}
...
与此同时如果想在添加一张Category表,只需新建一个类并继承DataSupport就行了
package com.example.houchongmu.litepaltest;
import org.litepal.crud.DataSupport;
public class Category extends DataSupport{
private int id;
private String categoryName;
private int categoryCode;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public int getCategoryCode() {
return categoryCode;
}
public void setCategoryCode(int categoryCode) {
this.categoryCode = categoryCode;
}
}
修改完之后记得将Category添加到litepal.xml文件中去,并将版本加一
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<dbname value="BookStore" />
<version value="2" />
<list>
<mapping class="com.example.houchongmu.litepaltest.Book" />
<mapping class="com.example.houchongmu.litepaltest.Category"/>
</list>
</litepal>
- 使用LitePal添加数据
因为模型类Book、category都继承了DataSupport所以才能进行CRUD操作,如果只进行表管理操作就可以不添加这个继承结构
我们在布局中添加一个addData的Button
public void addData(View view) {
Book book = new Book();
book.setName("Android开发艺术探索");
book.setAuthor("任玉刚");
book.setPages(501);
book.setPress("电子工业出版社");
book.setPrice(66.66);
book.save();
}
看最后一行的save方法,这就是父类DataSupport带有的方法,只要设置完数据后调用save方法这样就完成数据添加操作了。
- 接下来看怎么更新数据
- 最简单的一个方式就是对已存储的对象重新设值,然后重新调用save方法就行了
public void updata_data(View view) {
Book book = new Book();
book.setName("Android源码设计模式");
book.setPrice(88.88);
book.setPress("人民邮电出版社");
book.setPages(448);
book.save();
book.setPages(499);
book.save();
}
这种更新的方式限制比较大,只能对已存储的对象进行操作,看下面另一种更新方式
- updataAll方法,先看代码
public void updata_database(View view) {
Book book = new Book();
book.setPrice(14.95);
book.setPress("Anchor");
book.updateAll("name = ? and author = ?", "Android源码设计模式", "任玉刚");
/* The first question mark is
replaced by the second element of the array, the next question
mark by the third, and so on. Passing empty string will update
all rows.
*/
}
这里我们先new出一个Book实例,然后设置一些要更新的值,最后调用updataAll方法去执行更新操作,这个方法中可以制定一个条件约束,和SQLiteDatabase中的updata方法的where参数部分类似,如果不指定条件语句的话就是默认更新所有数据,这里我们指定将书名为“Android源码设计模式”,作者为“任玉刚”的书的价格改为14.95,出版社改为Anchor。
- 设置默认值
设置默认值我们可以一个一个调用set方法,但是这样太繁琐了,LitePal提供了一个setToDefault方法
public void set_toDeault(View view) {
Book book = new Book();
book.setToDefault("pages");//将pages列全部设置为0
book.updateAll();//执行更新操作
}
先new一个Book实例,然后将列的名字传进去,再调用updataAll方法就行了,这样就把表中所有书的页数设置为0了
- 删除数据,使用litePal删除数据的方式一般有两种
第一种就是直接调用已存储对象的delete方法就行了
第二种是调用DataSupport的deleteAll方法来删除数据,我们直接看第二种
public void delete_data(View view) {
DataSupport.deleteAll(Book.class,"price > ?","15");
}
deleteAll方法中第一个参数指明删除那张表里面的数据,后面两个参数的意思就是删除价格大于15的书。总的意思就是删除Book表中价格大于15的书。
- 查询数据,我们先看代码。
public void query_data(View view) {
List<Book> books=DataSupport.findAll(Book.class);
for(Book book:books){
Log.d(TAG, "query_data: "+book.getName()+book.getPrice());
}
}
这里findAll方法返回一个Book类的集合,然后可以从这个集合里面取值
当然除了findAll方法之外,还有很多其他查询的API
例如:
看书吧!!!
记住这些查询的API都是DataSupport提供的,约束条件都加在find方法之前