【Android开发基础】第七章 数据存储

第七章 数据存储

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;
        }
    }
}
  1. insert返回一个行号,没成功的话就是返回-1
  2. 游标相当于指针一样
  3. 如果事务结束的时候没有运行成功,就会回滚,就是执行数据库事务时,若检测到冲突、错误或主动触发回滚操作,会撤销事务内已执行 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可以使用性能更好的BufferedOutputStreamBufferedInputStream
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 销毁时,由于静态变量的存在,该视图对象无法被回收,造成内存泄漏。
  • ApplicationApplication 实例的生命周期和应用的生命周期一致。虽然它也会在内存中持续存在,但开发者可以在合适的时机(如应用状态改变、特定功能模块结束使用等),在 Application 中清理相关数据,合理管理内存,降低内存泄漏风险。比如在用户注销登录时,在 Application 中清除与用户登录相关的信息。
灵活性和可维护性方面
  • 静态变量:静态变量属于类,作用域局限于类内部,使用场景相对单一。当项目规模变大,代码结构复杂时,静态变量的管理和维护会变得困难。不同类中的静态变量之间缺乏统一的管理机制,难以形成清晰的全局数据管理体系。例如,多个工具类都有各自的静态变量,在修改或扩展功能时,很难快速理清这些静态变量之间的关系和影响范围。
  • ApplicationApplication 是 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的关系型数据存取、存储卡的文件读取操作。

共享参数、数据库和文件都是持久化的存储方式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值