第七章 数据存储
7.1共享参数SharedPreferences
-安卓独有的数据存储方式
7.1.1共享参数的用法
- SharedPreferences 是Android的一个轻量级存储工具,采用的存储结构是Key - Value的键值对方式。
- 共享参数的存储介质是符合XML规范的配置文件。保存路径是:
/data/data/应用包名/shared_prefs/文件名.xml
,符合程序的这样存储
java中properties用于配置config
XML和HTML都是文本标记语言
使用场景
- 共享参数主要适用于如下场合:
- 简单且孤立的数据。若是复杂且相互间有关的数据,则要保存在数据库中。
- 文本形式的数据。若是二进制数据,则要保存在文件中。
- 需要持久化存储的数据。在App退出后再次启动时,之前保存的数据仍然有效。
- 实际开发中,共享参数经常存储的数据有App的个性化配置信息、用户使用App的行为信息、临时需要保存的片段信息等。
SharedPreferences
的用法
LinearLayout就像div块一样
代码案例:只展示关键部分
public class MainActivity extends AppCompatActivity {
private static final String PREF_NAME = "MyAppPrefs";
private static final String KEY_NAME = "userName";
private EditText editTextName;
private TextView textViewDisplay;
private SharedPreferences sharedPreferences;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editTextName = findViewById(R.id.editTextName);
textViewDisplay = findViewById(R.id.textViewDisplay);
Button buttonSave = findViewById(R.id.buttonSave);
Button buttonLoad = findViewById(R.id.buttonLoad);
sharedPreferences = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
buttonSave.setOnClickListener(v -> {
String name = editTextName.getText().toString().trim();
if (!name.isEmpty()) {
saveName(name);
Toast.makeText(MainActivity.this, "姓名已保存", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "请输入姓名", Toast.LENGTH_SHORT).show();
}
});
buttonLoad.setOnClickListener(v -> {
String loadedName = loadName();
if (!loadedName.isEmpty()) {
textViewDisplay.setText("你保存的姓名是: " + loadedName);
} else {
textViewDisplay.setText("未找到保存的姓名");
}
});
}
private void saveName(String name) {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(KEY_NAME, name);
editor.apply();
}
private String loadName() {
return sharedPreferences.getString(KEY_NAME, "");
}
}
解析:
1、
sharedPreferences = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
创建一个共享首选项文件,Context.MODE_PRIVATE)
表示只有当前应用可以使用2.
SharedPreferences.Editor editor = sharedPreferences.edit();
编辑器3.
editor.putString(KEY_NAME, name);
存入内容,用键值对的方式4.
editor.apply();
保存数据5.
sharedPreferences.getString(KEY_VALUE, null)
;获取数据,SharedPreferences
中尝试获取以KEY_VALUE
为键的字符串数据,若找到则返回该数据,若未找到则返回null
。
7.1.2实现记住密码功能
代码:
public class LoginMainActivity extends AppCompatActivity implements RadioGroup.OnCheckedChangeListener, View.OnClickListener {
private TextView tv_password;
private EditText et_password;
private Button btn_forget;
private CheckBox ck_remember;
private EditText et_phone;
private RadioButton rb_passoword;
private RadioButton rb_verifycode;
private ActivityResultLauncher<Intent> register;
private Button btn_login;
private String passwordOrigin = "111111";
private String verifycode;
private CheckBox ck_Remember;
private SharedPreferences sharedPreferences;
@SuppressLint("WrongViewCast")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RadioGroup rb_login = findViewById(R.id.rg_login);
tv_password = findViewById(R.id.tv_password);
et_phone = findViewById(R.id.et_phone);
et_password = findViewById(R.id.et_passowrd);
btn_forget = findViewById(R.id.btn_forget);
ck_remember = findViewById(R.id.ck_remember);
rb_passoword = findViewById(R.id.rb_passoword);
rb_verifycode = findViewById(R.id.rb_verifycode);
btn_login = findViewById(R.id.btn_login);
ck_Remember = findViewById(R.id.ck_remember);
// 读取 SharedPreferences 中的手机号和密码
sharedPreferences = getSharedPreferences("config", Context.MODE_PRIVATE);
String savedPhone = sharedPreferences.getString("phone", "");
String savedPassword = sharedPreferences.getString("password", "");
if (!savedPhone.isEmpty() &&!savedPassword.isEmpty()) {
et_phone.setText(savedPhone);
et_password.setText(savedPassword);
ck_remember.setChecked(true); // 设置记住密码复选框为选中状态
}
btn_login.setOnClickListener(this);
//给rg_login设置单选监听器
rb_login.setOnCheckedChangeListener(this);
//给et_phone添加文本变更监听器
et_phone.addTextChangedListener(new HideTextWatcher(et_phone, 11));
et_password.addTextChangedListener(new HideTextWatcher(et_password, 6));
btn_forget.setOnClickListener(this);
register = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult o) {
if (o.getResultCode() == RESULT_OK) {
Intent data = o.getData();
if (data != null) {
String newPassword = data.getStringExtra("password");
if (newPassword != null) {
passwordOrigin = newPassword;
}
}
}
}
});
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
String newPassword = intent.getStringExtra("password");
if (newPassword != null) {
passwordOrigin = newPassword;
}
}
@Override
public void onCheckedChanged(RadioGroup radioGroup, int i) {
if (i == R.id.rb_passoword) {
tv_password.setText(getString(R.string.login_password));
et_password.setHint(getString(R.string.input_password));
btn_forget.setText(getString(R.string.forget_password));
ck_remember.setVisibility(View.VISIBLE);
} else if (i == R.id.rb_verifycode) {
tv_password.setText(getString(R.string.verifycode));
et_password.setHint(getString(R.string.input_verifycode));
btn_forget.setText(getString(R.string.get_verifycode));
ck_remember.setVisibility(View.GONE);
}
}
@Override
public void onClick(View view) {
String phone = et_phone.getText().toString();
if (view.getId() == R.id.btn_forget) {
if (phone.length() < 11) {
//弹出一个短暂的提示框
Toast.makeText(this, "请输入正确的手机号码", Toast.LENGTH_SHORT).show();
return;
}
if (rb_passoword.isChecked()) {
//如果是密码登录,点击忘记密码应该跳转到密码找回界面
Intent intent = new Intent(this, LoginForgetActivity.class);
intent.putExtra("phone", phone);
register.launch(intent);
} else if (rb_verifycode.isChecked()) {
//如果是验证码登录,点击生成验证码应该生成验证码
//生成六位随机的验证码
verifycode = String.format("%06d", new Random().nextInt(999999));
//弹出对话框让其记住
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("请记住验证码");
builder.setMessage("手机号" + phone + "本次验证码是" + verifycode + ",请输入验证码");
builder.setPositiveButton("好的", null);
AlertDialog dialog = builder.create();
dialog.show();
}
} else if (view.getId() == R.id.btn_login) {
if (phone.length() < 11) {
//弹出一个短暂的提示框
Toast.makeText(this, "请输入正确的手机号码", Toast.LENGTH_SHORT).show();
return;
}
if (rb_passoword.isChecked()) {
//判断密码对不对
if (!passwordOrigin.equals(et_password.getText().toString())) {
//弹出一个短暂的提示框
Toast.makeText(this, "请输入正确的密码", Toast.LENGTH_SHORT).show();
return;
}
if (ck_remember.isChecked()) {
sharedPreferences = getSharedPreferences("config", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("phone", phone);
editor.putString("password", passwordOrigin);
editor.apply();
}
try {
loginSuccess();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "登录成功弹窗显示出错: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
} else if (rb_verifycode.isChecked()) {
if (!verifycode.equals(et_password.getText().toString())) {
//弹出一个短暂的提示框
Toast.makeText(this, "请输入正确的验证码", Toast.LENGTH_SHORT).show();
return;
}
try {
loginSuccess();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "登录成功弹窗显示出错: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
}
}
}
private void loginSuccess() {
String desc = String.format("您的手机号码是%s,恭喜您通过登录验证,点击“确定”按钮返回上一个页面", et_phone.getText().toString());
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("登录成功");
builder.setMessage(desc);
builder.setPositiveButton("确定返回", (dialog, which) -> {
// 移除 finish() 调用,避免关闭当前 Activity
finish();
});
builder.setNegativeButton("我再看看", null);
AlertDialog dialog = builder.create();
dialog.show();
}
//编辑框输入一定数目的内容就会收掉软键盘
private class HideTextWatcher implements TextWatcher {
private EditText mView;
private int maxLength;
public HideTextWatcher(EditText mView, int maxLength) {
this.mView = mView;
this.maxLength = maxLength;
}
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable s) {
if (s.toString().length() == maxLength) {
try {
ViewUtil.hideOneInputMethod(LoginMainActivity.this, mView);
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(LoginMainActivity.this, "隐藏软键盘出错: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
}
}
}
}
7.2数据库SQLite
7.2.1 SQL 的基本语法
标准的SQL语句分为三类,数据定义,数据操纵和数据控制。
SQLite是一种小的嵌入式数据库,使用方便、开销简单。如同MySQL、Oracle那样,SQLite也采用SQL语句管理数据,由于它属于轻型数据库,不涉及复杂的数据控制操作,因此App开发只用到数据定义和数据操纵两类SQL。此外,SQLite的SQL语法与通用的SQL语法略有不同,接下来介绍的一些SQL语法全部基于SQLite。
数据定义语言
数据定义语言全称Data Definition Language
,简称DDL,它描述了怎样变更数据实体的框架结构。就SQLite而言,DDL语言主要包括3种操作:创建表格、删除表格、修改表结构,分别说明如下。
创建表格
表格的创建动作由create命令完成,格式为“CREATE TABLE IF NOT EXISTS 表格名称 (以逗号分隔的各字段定义) ;”。以用户信息表为例,它的建表语句如下所示:
CREATE TABLE IF NOT EXISTS user_info (
_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name VARCHAR NOT NULL,
age INTEGER NOT NULL,
height LONG NOT NULL,
weight FLOAT NOT NULL,
married INTEGER NOT NULL,
update_time VARCHAR NOT NULL
);
上面的SQL语法与其他数据库的SQL语法有所出入,相关的注意点说明如下:
①SQL语句不区分大小写,无论是create与table这类关键词,还是表格名称、字段名称,都不区分大小写。唯一区分大小写的是被单引号括起来的字符串值。
②为避免重复建表,应加上IF NOT EXISTS关键词,例如CREATE TABLE IF NOT EXISTS 表格名称……
③SQLite支持整型INTEGER、长整型LONG、字符串VARCHAR、浮点数FLOAT ,但不支持布尔类型。布尔类型的数据要使用整型保存,如果直接保存布尔数据,在入库时SQLite会自动将它转为0或1,其中0表示false,1表示true。
④建表时需要唯一标识字段,它的字段名为_id。创建新表都要加上该字段定义,例如id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL。 4
删除表格
表格的删除动作由drop命令完成,格式为“DROP TABLE IF EXISTS
表格名称;”。下面是删除用户信息表的SQL语句例子:
DROP TABLE IF EXISTS user_info;
修改表结构
表格的修改动作由alter命令完成,格式为“ALTER TABLE 表格名称 修改操作;”。不过SQLite只支持增加字段,不支持修改字段,也不支持删除字段。对于字段增加操作,需要在alter之后补充add命令,具体格式如“ALTER TABLE 表格名称 ADD COLUMN 字段名称 字段类型;”。下面是给用户信息表增加手机号字段的SQL语句例子:
ALTER TABLE user_info ADD COLUMN phone VARCHAR;
注意,SQLite的ALTER语句每次只能添加一列字段,若要添加多列,就得分多次添加。
数据操纵语言
数据操纵语言全称Data Manipulation Language,简称DML,它描述了怎样处理数据实体的内部记录。表格记录的操作类型包括添加、删除、修改、查询4类,分别说明如下:
(1)添加记录
记录的添加动作由insert
命令完成,格式为“INSERT INTO 表格名称 (以逗号分隔的字段名列表) VALUES (以逗号分隔的字段值列表) ;
” 。下面是往用户信息表插入一条记录的SQL语句例子:
INSERT INTO user_info (name,age,height,weight,married,update_time)
VALUES ('张三',20,170,50,0,'20200504');
(2)删除记录
记录的删除动作由delete
命令完成,格式为“DELETE FROM 表格名称 WHERE 查询条件
”,其中查询条件的表达式形如“字段名=字段值”,多个字段的条件交集通过“AND”连接,条件并集通过“OR”连接。下面是从用户信息表删除指定记录的SQL语句例子:
DELETE FROM user_info WHERE name='张三';
(3)修改记录
记录的修改动作由update
命令完成,格式为“UPDATE 表格名称 SET 字段名=字段值 WHERE 查询条件;
” 。下面是对用户信息表更新指定记录的SQL语句例子:
UPDATE user_info SET married=1 WHERE name='张三';
(4)查询记录
记录的查询动作由select
命令完成,格式为“SELECT 以逗号分隔的字段名列表 FROM 表格名称 WHERE 查询条件;
” 。如果字段名列表填星号“*”,则表示查询该表的所有字段。下面是从用户信息表查询指定记录的SQL语句例子:
SELECT name FROM user_info WHERE name=‘张三’;
查询操作除了比较字段值条件之外,常常需要对查询结果排序,此时要在查询条件后面添加排序条件,对应的表达式为“ORDER BY 字段名 ASC或者DESC”,意指对查询结果按照某个字段排序,其中ASC代表升序,DESC代表降序。下面是查询记录并对结果排序的SQL语句例子:
SELECT * FROM user_info ORDER BY age ASC;
如果读者之前不熟悉SQL语法,建议下载一个SQLite管理软件,譬如SQLiteStudio,先在电脑上多加练习SQLite的常见操作语句。
7.2.2 数据库管理器SQLiteDatabase
-
SQLiteDatabase是SQLite的数据库管理类,它提供了若干操作数据表的API,常用的方法有3类:
-
管理类,用于数据库层面的操作。
- openDatabase:打开指定路径的数据库。
- isOpen:判断数据库是否已打开。
- close:关闭数据库。
- getVersion:获取数据库的版本号。
- setVersion:设置数据库的版本号。
-
事务类,用于事务层面的操作。
- beginTransaction:开始事务。
- setTransactionSuccessful:设置事务的成功标志。
- endTransaction:结束事务。
-
数据处理类,用于数据表层面的操作。
- execSQL:执行拼接好的SQL控制语句。
- delete:删除符合条件的记录。
- update:更新符合条件的记录。
- insert:插入一条记录。
- query:执行查询操作,返回结果集的游标。
- rawQuery:执行拼接好的SQL查询语句,返回结果集的游标。
关键代码示例:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 数据库文件路径,这里使用应用内部存储路径下的数据库文件,实际可按需修改
String dbPath = getDatabasePath("test.db").getPath();
// 打开数据库
SQLiteDatabase database = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.CREATE_IF_NECESSARY);
// 判断数据库是否打开
if (database.isOpen()) {
Toast.makeText(this, "数据库已成功打开", Toast.LENGTH_SHORT).show();
// 这里可以进行其他数据库操作,比如创建表、插入数据等,此处先略过
// 关闭数据库
database.close();
Toast.makeText(this, "数据库已关闭", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "数据库打开失败", Toast.LENGTH_SHORT).show();
}
}
}
7.2.3 数据库帮助器SQLiteOpenHelper
- SQLiteOpenHelper是Android提供的数据库辅助工具,用于指导开发者进行SQLite的合理使用。
- SQLiteOpenHelper的具体使用步骤如下:
- 新建一个继承自SQLiteOpenHelper的数据库操作类,提示重写onCreate和onUpgrade两个方法。
- 封装保证数据库安全的必要方法。
- 提供对表记录进行增加、删除、修改、查询的操作方法。
代码案例
public class UserDBHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "user.db";
private static final String TABLE_NAME = "user_info";
private static final int DB_VERSION = 2;
private static UserDBHelper mHelper = null;
private SQLiteDatabase mRDB = null;
private SQLiteDatabase mWDB = null;
private UserDBHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
// 利用单例模式获取数据库帮助器的唯一实例
public static UserDBHelper getInstance(Context context) {
if (mHelper == null) {
mHelper = new UserDBHelper(context);
}
return mHelper;
}
// 打开数据库的读连接
public SQLiteDatabase openReadLink() {
if (mRDB == null || !mRDB.isOpen()) {
mRDB = mHelper.getReadableDatabase();
}
return mRDB;
}
// 打开数据库的写连接
public SQLiteDatabase openWriteLink() {
if (mWDB == null || !mWDB.isOpen()) {
mWDB = mHelper.getWritableDatabase();
}
return mWDB;
}
// 关闭数据库连接
public void closeLink() {
if (mRDB != null && mRDB.isOpen()) {
mRDB.close();
mRDB = null;
}
if (mWDB != null && mWDB.isOpen()) {
mWDB.close();
mWDB = null;
}
}
// 创建数据库,执行建表语句
@Override
public void onCreate(SQLiteDatabase db) {
String sql = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
" name VARCHAR NOT NULL," +
" age INTEGER NOT NULL," +
" height LONG NOT NULL," +
" weight FLOAT NOT NULL," +
" married INTEGER NOT NULL);";
db.execSQL(sql);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
String sql = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN phone VARCHAR;";
db.execSQL(sql);
sql = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN password VARCHAR;";
db.execSQL(sql);
}
public long insert(User user) {
ContentValues values = new ContentValues();
values.put("name", user.name);
values.put("age", user.age);
values.put("height", user.height);
values.put("weight", user.weight);
values.put("married", user.married);
// 执行插入记录动作,该语句返回插入记录的行号
// 如果第三个参数values 为Null或者元素个数为0, 由于insert()方法要求必须添加一条除了主键之外其它字段为Null值的记录,
// 为了满足SQL语法的需要, insert语句必须给定一个字段名 ,如:insert into person(name) values(NULL),
// 倘若不给定字段名 , insert语句就成了这样: insert into person() values(),显然这不满足标准SQL的语法。
// 如果第三个参数values 不为Null并且元素的个数大于0 ,可以把第二个参数设置为null 。
//return mWDB.insert(TABLE_NAME, null, values);
try {
mWDB.beginTransaction();
mWDB.insert(TABLE_NAME, null, values);
//int i = 10 / 0;
mWDB.insert(TABLE_NAME, null, values);
mWDB.setTransactionSuccessful();
} catch (Exception e) {
e.printStackTrace();
} finally {
mWDB.endTransaction();
}
return 1;
}
public long deleteByName(String name) {
//删除所有
//mWDB.delete(TABLE_NAME, "1=1", null);
return mWDB.delete(TABLE_NAME, "name=?", new String[]{name});
}
public long update(User user) {
ContentValues values = new ContentValues();
values.put("name", user.name);
values.put("age", user.age);
values.put("height", user.height);
values.put("weight", user.weight);
values.put("married", user.married);
return mWDB.update(TABLE_NAME, values, "name=?", new String[]{user.name});
}
public List<User> queryAll() {
List<User> list = new ArrayList<>();
// 执行记录查询动作,该语句返回结果集的游标
Cursor cursor = mRDB.query(TABLE_NAME, null, null, null, null, null, null);
// 循环取出游标指向的每条记录
while (cursor.moveToNext()) {
User user = new User();
user.id = cursor.getInt(0);
user.name = cursor.getString(1);
user.age = cursor.getInt(2);
user.height = cursor.getLong(3);
user.weight = cursor.getFloat(4);
//SQLite没有布尔型,用0表示false,用1表示true
user.married = (cursor.getInt(5) == 0) ? false : true;
list.add(user);
}
return list;
}
public List<User> queryByName(String name) {
List<User> list = new ArrayList<>();
// 执行记录查询动作,该语句返回结果集的游标
Cursor cursor = mRDB.query(TABLE_NAME, null, "name=?", new String[]{name}, null, null, null);
// 循环取出游标指向的每条记录
while (cursor.moveToNext()) {
User user = new User();
user.id = cursor.getInt(0);
user.name = cursor.getString(1);
user.age = cursor.getInt(2);
user.height = cursor.getLong(3);
user.weight = cursor.getFloat(4);
//SQLite没有布尔型,用0表示false,用1表示true
user.married = (cursor.getInt(5) == 0) ? false : true;
list.add(user);
}
return list;
}
}
public class SQLiteHelperActivity extends AppCompatActivity implements View.OnClickListener {
private EditText et_name;
private EditText et_age;
private EditText et_height;
private EditText et_weight;
private CheckBox ck_married;
private UserDBHelper mHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sqlite_helper);
et_name = findViewById(R.id.et_name);
et_age = findViewById(R.id.et_age);
et_height = findViewById(R.id.et_height);
et_weight = findViewById(R.id.et_weight);
ck_married = findViewById(R.id.ck_married);
findViewById(R.id.btn_save).setOnClickListener(this);
findViewById(R.id.btn_delete).setOnClickListener(this);
findViewById(R.id.btn_update).setOnClickListener(this);
findViewById(R.id.btn_query).setOnClickListener(this);
}
@Override
protected void onStart() {
super.onStart();
// 获得数据库帮助器的实例
mHelper = UserDBHelper.getInstance(this);
// 打开数据库帮助器的读写连接
mHelper.openWriteLink();
mHelper.openReadLink();
}
@Override
protected void onStop() {
super.onStop();
// 关闭数据库连接
mHelper.closeLink();
}
@Override
public void onClick(View v) {
String name = et_name.getText().toString();
String age = et_age.getText().toString();
String height = et_height.getText().toString();
String weight = et_weight.getText().toString();
User user = null;
switch (v.getId()) {
case R.id.btn_save:
// 以下声明一个用户信息对象,并填写它的各字段值
user = new User(name,
Integer.parseInt(age),
Long.parseLong(height),
Float.parseFloat(weight),
ck_married.isChecked());
if (mHelper.insert(user) > 0) {
ToastUtil.show(this, "添加成功");
}
break;
case R.id.btn_delete:
if (mHelper.deleteByName(name) > 0) {
ToastUtil.show(this, "删除成功");
}
break;
case R.id.btn_update:
user = new User(name,
Integer.parseInt(age),
Long.parseLong(height),
Float.parseFloat(weight),
ck_married.isChecked());
if (mHelper.update(user) > 0) {
ToastUtil.show(this, "修改成功");
}
break;
case R.id.btn_query:
List<User> list = mHelper.queryAll();
//List<User> list = mHelper.queryByName(name);
for (User u : list) {
Log.d("ning", u.toString());
}
break;
}
}
}
insert
返回一个行号,没成功的话就是返回-1- 游标相当于指针一样
- 如果事务结束的时候没有运行成功,就会回滚,就是执行数据库事务时,若检测到冲突、错误或主动触发回滚操作,会撤销事务内已执行 SQL 语句对数据的修改,使数据库回到事务开始前状态。
游标Cursor
- 调用SQLiteDatabase的query和rawQuery方法时,返回的都是Cursor对象,因此获取查询结果要根据游标的指示一条一条遍历结果集合,Cursor的常用方法可分为3类。
- 游标控制类方法,用于指定游标的状态。
- close:关闭游标。
- isClosed:判断游标是否关闭。
- isFirst:判断游标是否在开头。
- isLast:判断游标是否在末尾。
- 游标移动类方法,把游标移动到指定位置。
- moveToFirst:移动游标到开头。
- moveToLast:移动游标到末尾。
- moveToNext:移动游标到下一条记录。
- moveToPrevious:移动游标到上一条记录。
- move:往后移动游标若干条记录。
- moveToPosition:移动游标到指定位置的记录。
- 获取记录类方法,可获取记录的数量、类型以及取值。
- getCount:获取结果记录的数量。
- getInt:获取指定字段的整型值。
- getLong:获取指定字段的长整型值。
- getFloat:获取指定字段的浮点数值。
- getString:获取指定字段的字符串值。
- getType:获取指定字段的字段类型。
7.2.4 优化记住密码功能
7.3存储卡的文件操作
7.3.1 私有存储空间与公共存储空间
sharepreferences以XML形式存在,存储少量的数据。数据库SQLite存储多一点的数据,是用数据库格式化存在的,更多的文本文件和图片文件用存储卡实现。
Android 把外部存储分成了两块区域,一块是所有应用均可访问的公共空间,另一块是只有应用自己才可访问的私有空间。
获取公共空间的存储路径,调用的是 Environment 类的 getExternalStoragePublicDirectory
方法;获取应用私有空间的存储路径,调用的是 getExternalFilesDir
方法。
获取内部存储的私有空间,用getFilesDir
方法
import android.content.Context;
import android.os.Environment;
import java.io.File;
public class StoragePathExample {
private Context context;
public StoragePathExample(Context context) {
this.context = context;
}
// 获取公共空间存储路径
public File getPublicStoragePath() {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
}
// 获取应用私有空间存储路径
public File getPrivateStoragePath() {
return context.getExternalFilesDir(null);
}
}
动态获取权限,运行时动态获取权限
外部私有空间软件卸载之后就没有了
内部存储的空间是有限的,希望百度网盘卸载之后相关文件还在,就是存储爱外部存储的公共空间里面
7.3.2 在存储卡上读写文本文件
文本文件的读写一般借助于 FileOutputStream 和 FileInputStream。
FileOutputStream
用于写文件。FileInputStream
用于读文件。
7.3.3在存储卡上读写图片文件
Android 的位图工具是Bitmap,App读写Bitmap可以使用性能更好的BufferedOutputStream
和BufferedInputStream
。
Android还提供了BitmapFactory工具用于读取各种来源的图片,相关方法如下:
- decodeResource:该方法可从资源文件中读取图片信息。
- decodeFile:该方法可将指定路径的图片读取到Bitmap对象。
- decodeStream:该方法从输入流中读取位图数据。
7.4 应用组件Application
7.4.1 Application 的生命周期
Application是Android的一大组件,在App运行过程中有且仅有一个Application对象贯穿整个生命周期。
整个应用都可用的全局组件
onCreate是在APP启动时候调用,应用挂掉Application就挂掉,在onTerminate的时候,以及onConfigurationChanged,例如屏幕旋转的时候,比如写文件的场景下,屏幕旋转之后填的内容就会不见。
7.4.2 利用 Application 操作全局变量
全局的意思是其他代码都可以引用该变量,因此全局变量是共享数据和消息传递的好帮手。
适合在Application中保存的全局变量主要有下面3类数据:
- 会频繁读取的信息,如用户名、手机号等。
- 不方便由意图传递的数据,例如位图对象、非字符串类型的集合对象等。
- 容易因频繁分配内存而导致内存泄漏的对象,如Handler对象等。
在 Activity
中,你可以通过 getApplication()
方法获取自定义的 Application
实例,然后访问和修改全局变量。
在 Android 开发中不推荐只用静态变量,而更倾向使用
Application
来管理全局变量,原因如下:内存管理方面
- 静态变量:静态变量生命周期和类加载周期相关,一旦类被加载,静态变量就会一直存在于内存中,直到类被卸载。若静态变量引用了大对象(如大图片、大型集合等),或者持有上下文(如 Activity 上下文),在不需要使用这些变量时,无法及时释放内存,极易导致内存泄漏。例如,一个静态变量引用了某个 Activity 中的视图对象,当 Activity 销毁时,由于静态变量的存在,该视图对象无法被回收,造成内存泄漏。
- Application:
Application
实例的生命周期和应用的生命周期一致。虽然它也会在内存中持续存在,但开发者可以在合适的时机(如应用状态改变、特定功能模块结束使用等),在Application
中清理相关数据,合理管理内存,降低内存泄漏风险。比如在用户注销登录时,在Application
中清除与用户登录相关的信息。灵活性和可维护性方面
- 静态变量:静态变量属于类,作用域局限于类内部,使用场景相对单一。当项目规模变大,代码结构复杂时,静态变量的管理和维护会变得困难。不同类中的静态变量之间缺乏统一的管理机制,难以形成清晰的全局数据管理体系。例如,多个工具类都有各自的静态变量,在修改或扩展功能时,很难快速理清这些静态变量之间的关系和影响范围。
- Application:
Application
是 Android 应用的核心类,可作为全局数据的统一管理入口。开发者可以在Application
中定义各种类型的全局变量,并且可以根据应用的业务逻辑,在不同阶段对这些变量进行初始化、更新和清理等操作。例如,在应用启动时初始化一些配置信息,在切换用户时更新用户相关的全局变量等,使代码的逻辑更加清晰,可维护性更高。线程安全方面
- 静态变量:在多线程环境下,静态变量容易出现线程安全问题。如果多个线程同时对静态变量进行读写操作,可能会导致数据不一致。要解决这个问题,需要额外添加同步机制(如
synchronized
关键字),增加了代码的复杂度和性能开销。例如,多个线程同时对一个静态的计数器变量进行自增操作,可能会出现结果不准确的情况。- Application:虽然
Application
本身并不直接解决线程安全问题,但开发者可以针对Application
中保存的不同变量,根据其实际使用场景,更灵活地添加同步控制逻辑。而且,由于Application
对全局变量进行了统一管理,在处理线程安全问题时,更容易把握整体情况,避免出现混乱。
7.4.3 利用 Room 简化数据库操作
使用数据库帮助器编码的时候,开发者每次都得手工实现以下代码逻辑:
- 重写数据库帮助器的onCreate方法,添加该表的建表语句;
- 在插入记录之时,必须将数据实例的属性值逐一赋给该表的各字段;
- 在查询记录之时,必须遍历结果集游标,把各字段值逐一赋给数据实例;
- 每次读写操作之前,都要先开启数据库连接;读写操作之后,又要关闭数据库连接;
Room框架的导入
Room是谷歌公司推出的数据库处理框架,该框架同样基于SQLite,但它通过注解技术极大简化了数据库操作,减少了原来相当一部分编码工作量。
在使用Room之前,要先修改模块的build.gradle文件,往dependencies节点添加下面两行配置,表示导入指定版本的Room库:
- implementation ‘androidx.room:room-runtime:2.2.5’
- annotationProcessor ‘androidx.room:room-compiler:2.2.5’
Room框架的编码步骤
以录入书籍信息为例,使用Room框架的编码过程分为下列五步:
- 编写书籍信息表对应的实体类,该类添加 “@Entity” 注解。
- 编写书籍信息表对应的持久化类,该类添加 “@Dao” 注解。
- 编写书籍信息表对应的数据库类,该类从RoomDatabase派生而来,并添加 “@Database” 注解。
- 在自定义的Application类中声明书籍数据库的唯一实例。
- 在操作书籍信息表的地方获取数据表的持久化对象。
代码应用实例:
-
编写书籍信息表对应的实体类,该类添加 “@Entity” 注解。
在 Room 框架里,实体类代表数据库中的一张表。每个实体类的实例就相当于表中的一行记录。
@Entity
注解的作用是把这个类标记为数据库表的实体,并且可以指定表名等信息。
package com.example.chapter06.entity;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity
public class BookInfo {
@PrimaryKey(autoGenerate = true)
private int id;
private String name;
private String author;
private String publisher;
private double price;
public int getId() {
return id;
}
public String getName() {
return name;
}
public String getPublisher() {
return publisher;
}
public double getPrice() {
return price;
}
public String getAuthor() {
return author;
}
public void setId(int id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setAuthor(String author) {
this.author = author;
}
public void setPublisher(String publisher) {
this.publisher = publisher;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public String toString() {
return "BookInfo{" +
"id=" + id +
", name='" + name + '\'' +
", author='" + author + '\'' +
", publisher='" + publisher + '\'' +
", price=" + price +
'}';
}
}
-
编写书籍信息表对应的持久化类,该类添加 “@Dao” 注解
数据访问对象(DAO)用于定义对数据库的操作,像插入、查询、更新和删除等。
@Dao
注解的作用是把这个类标记为 DAO 类,其中定义的方法会被 Room 框架自动实现,运行的时候会自创建schemas文件夹
package com.example.chapter06.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import com.example.chapter06.entity.BookInfo;
import java.util.List;
@Dao
public interface BookDao {
@Insert
void insert(BookInfo ... book);
@Delete
void delete(BookInfo ... book);
//清空所有数据信息
@Query("DELETE FROM BookInfo")
void deleteAll();
@Update
int update(BookInfo ... book);
@Query("SELECT * FROM BookInfo")
List<BookInfo> quaryAll();
@Query("SELECT * FROM BookInfo WHERE name=:name ORDER BY id DESC LIMIT 1")
BookInfo quarybyName(String name);
}
-
编写书籍信息表对应的数据库类,该类从 RoomDatabase 派生而来,并添加 “@Database” 注解。
数据库类是 Room 框架的核心,它负责管理数据库的创建和版本控制。这个类要继承自
RoomDatabase
,并且使用@Database
注解来指定包含哪些实体类以及数据库的版本号。
package com.example.chapter06.database;
import androidx.room.Database;
import androidx.room.RoomDatabase;
import com.example.chapter06.dao.BookDao;
import com.example.chapter06.entity.BookInfo;
//entities表示该数据库有哪些表,version表示数据库的版本号
//exportSchema表示是否导出数据库信息的json串,建议设为false,若设为true还需指定json文件的保存路径
@Database(entities = {BookInfo.class},version = 1,exportSchema = true)
public abstract class BookDatabase extends RoomDatabase {
//获取该数据库中的某张表的持久化对象
public abstract BookDao bookDao();
}
-
在自定义的 Application 类中声明书籍数据库的唯一实例
为了确保整个应用程序中只有一个数据库实例,避免资源浪费和数据不一致的问题,通常会在自定义的 Application 类中创建并管理数据库实例。
package com.example.chapter06;
import android.app.Application;
import android.content.res.Configuration;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.room.Room;
import com.example.chapter06.database.BookDatabase;
public class MyApplication extends Application {
private static MyApplication mApp;
private BookDatabase bookDatabase;
public static MyApplication getInstance(){
return mApp;
}
//在App启动时调用
@Override
public void onCreate() {
super.onCreate();
mApp = this;
Log.d("ning", "MyApplication onCreate");
// 构建书籍数据库的实例
bookDatabase = Room.databaseBuilder(this, BookDatabase.class, "book")
// 允许迁移数据库(发生数据库变更时,Room默认删除原数据库再创建新数据库。如此一来原来的记录会丢失,故而要改为迁移方式以便保存原有记录)
.addMigrations()
// 允许在主线程中操作数据库(Room默认不能在主线程中操作数据库)
.allowMainThreadQueries()
.build();
}
//在App终止时调用
@Override
public void onTerminate() {
super.onTerminate();
Log.d("ning", "onTerminate");
}
//在配置改变时调用,例如从竖屏变为横屏。
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.d("ning", "onConfigurationChanged");
}
// 获取书籍数据库的实例
public BookDatabase getBookDB() {
return bookDatabase;
}
}
-
在操作书籍信息表的地方获取数据表的持久化对象
在需要对书籍信息表进行操作的地方,通过自定义的 Application 类获取数据库实例,再通过数据库实例获取 DAO 对象,进而执行具体的数据库操作。
package com.example.chapter06;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import android.widget.EditText;
import android.widget.Toast;
import com.example.chapter06.dao.BookDao;
import com.example.chapter06.entity.BookInfo;
import com.example.chapter06.util.ToastUtil;
import java.util.List;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private EditText book_title_edit_text;
private EditText author_edit_text;
private EditText publisher_edit_text;
private EditText price_edit_text;
private BookDao bookDao;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
book_title_edit_text = findViewById(R.id.book_title_edit_text);
author_edit_text = findViewById(R.id.author_edit_text);
publisher_edit_text = findViewById(R.id.publisher_edit_text);
price_edit_text = findViewById(R.id.price_edit_text);
findViewById(R.id.add_button).setOnClickListener(this);
findViewById(R.id.delete_button).setOnClickListener(this);
findViewById(R.id.update_button).setOnClickListener(this);
findViewById(R.id.query_button).setOnClickListener(this);
bookDao = MyApplication.getInstance().getBookDB().bookDao();
}
@Override
public void onClick(View view) {
String name = book_title_edit_text.getText().toString();
String author = author_edit_text.getText().toString();
String publisher = publisher_edit_text.getText().toString();
String price = price_edit_text.getText().toString();
if (view.getId()==(R.id.add_button)){
BookInfo b1=new BookInfo();
b1.setName(name);
b1.setAuthor(author);
b1.setPublisher(publisher);
b1.setPrice(Double.parseDouble(price));
ToastUtil.show(this,"保存成功");
bookDao.insert(b1);
} else if(view.getId()==R.id.delete_button){
BookInfo b2=new BookInfo();
b2.setName("111");
bookDao.delete(b2);
}else if(view.getId()==R.id.update_button){
BookInfo b3=new BookInfo();
BookInfo b4=bookDao.quarybyName(name);
b3.setId(b4.getId());
b3.setName(name);
b3.setAuthor(author);
b3.setPublisher(publisher);
b3.setPrice(Double.parseDouble(price));
ToastUtil.show(this,"保存成功");
bookDao.update(b3);
} else if (view.getId()==R.id.query_button) {
List<BookInfo> list=bookDao.quaryAll();
for (BookInfo b:list){
Log.d("ning",b.toString());
}
}
}
}
运用自定义的Application的时候要在
AndroidManifest.xml
文件中,在<application>
标签中添加android:name
属性,并且将其值设置为.MyApplication
,这样才能确保应用启动时会初始化MyApplication
类。
7.5 实战项目:购物车
7.5.1 需求描述
7.5.2 界面设计
7.5.3 关键代码
总结:共享参数SharePrefernces的键值对存取、SQLite的关系型数据存取、存储卡的文件读取操作。
共享参数、数据库和文件都是持久化的存储方式