《第一行代码:Android篇》学习笔记(六)

本文和接下来的几篇文章为阅读郭霖先生所著《第一行代码:Android(篇第2版)》的学习笔记,按照书中的内容顺序进行记录,书中的Demo本人全部都做过了。
每一章节本人都做了详细的记录,以下是我学习记录(包含大量书中内容的整理和自己在学习中遇到的各种bug及解决方案),方便以后阅读和查阅。最后,感激感激郭霖先生提供这么好的书籍。

第6章 数据存储全方案——详解持久化技术

任何一个应用程序,其实说白了就是在不停地和数据打交道,聊QQ、看新闻、刷微博,所关心的都是里面的数据,没有数据的应用程序就变成了一个空壳子,对用户来说没有任何实际用途。现在多数的数据基本都是由用户产生的,比如你发微博、评论新闻,其实都是在产生数据。

瞬时数据就是指那些存储在内存当中,有可能会因为程序关闭或其他原因导致内存被回收而丢失的数据。这对于一些关键性的数据信息来说是绝对不能容忍的,谁都不希望自己刚发出去的一条微博,刷新一下就没了吧。那么怎样才能保证一些关键性的数据不会丢失呢?这就需要用到数据持久化技术了。

6.1 持久化技术简介

数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或电脑关机的情况下,这些数据仍然不会丢失。保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的,持久化技术则提供了一种机制可以让数据在瞬时状态和持久状态之间进行转换。

持久化技术被广泛应用于各种程序设计的领域当中,而本书中要探讨的自然是Android中的数据持久化技术。Android系统中主要提供了3种方式用于简单地实现数据持久化功能,即文件存储、SharedPreferences存储以及数据库存储。

当然,除了这3种方式之外,你还可以将数据保存在手机的SD卡中,不过使用文件、SharedPreferences或数据库来保存数据会相对更简单一些,而且比起将数据保存在SD卡中会更加地安全。

6.2 文件存储

文件存储是Android中最基本的一种数据存储方式,它不对存储的内容进行任何的格式化处理,所有数据都是原封不动地保存到文件当中的,因而它比较适合用于存储一些简单的文本数据或二进制数据。

如果你想使用文件存储的方式来保存一些较为复杂的文本数据,就需要定义一套自己的格式规范,这样可以方便之后将数据从文件中重新解析出来。那么首先我们就来看一看,Android中是如何通过文件来保存数据的。

6.2.1 将数据存储到文件中

Context类中提供了一个openFileOutput()方法,可以用于将数据存储到指定的文件中。

  • 第一个参数是文件名,在文件创建的时候使用的就是这个名称,注意这里指定的文件名不可以包含路径,因为所有的文件都是默认存储到/data/data//files/目录下的。
  • 第二个参数是文件的操作模式,主要有两种模式可选,MODE_PRIVATE和MODE_APPEND。其中MODE_PRIVATE是默认的操作模式,表示当指定同样文件名的时候,所写入的内容将会覆盖原文件中的内容,而MODE_APPEND则表示如果该文件已存在,就往文件里面追加内容,不存在就创建新文件。

其实文件的操作模式本来还有另外两种:MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE,这两种模式表示允许其他的应用程序对我们程序中的文件进行读写操作,不过由于这两种模式过于危险,很容易引起应用的安全性漏洞,已在Android 4.2版本中被废弃。

openFileOutput()方法返回的是一个FileOutputStream对象,得到了这个对象之后就可以使用Java流的方式将数据写入到文件中了。以下是一段简单的代码示例,展示了如何将一段文本内容保存到文件中:

public void save() {
	String data = "Data to save";
	FileOutputStream out = null;
	BufferedWriter writer = null;
	try {
		out = openFileOutput("data",Context.MODE_PRIVATE);
		writer = new BufferedWriter(OutputStreamWriter(out));
		writer.write(data);
	} catch (IOException e) {
		e.printStackTrace();
	} finally {
		try {
			if (writer !=null) {
				writer.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

上面通过openFileOutput()方法能够得到一个FileOutputStream对象,然后再借助它构建出一个OutputStreamWriter对象,接着再使用OutputStreamWriter构建出一个BufferedWriter对象,这样你就可以通过BufferedWriter来将文本内容写入到文件中了。

下面就编写一个完整的例子,借此学习一下如何在Android项目中使用文件存储的技术。首先创建一个FilePersistenceTest项目,并修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <EditText
        android:id="@+id/edit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Type something here"/>
</LinearLayout>

在布局中加入了一个EditText,用于输入文本内容。现在运行一下程序,界面上会有一个文本输入框。然后在文本输入框中随意输入点什么内容,再按下Back键,这时输入的内容肯定就已经丢失了,因为它只是瞬时数据,在活动被销毁后就会被回收。而这里我们要做的,就是在数据被回收之前,将它存储到文件当中。修改MainActivity中的代码,如下所示:

package com.zhouzhou.filepersistencetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.os.Bundle;
import android.widget.EditText;

import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;

public class MainActivity extends AppCompatActivity {
    private EditText edit;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        edit = (EditText) findViewById(R.id.edit);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        String inputText = edit.getText().toString();
        save(inputText);
    }
    public void save(String inputText) {
        FileOutputStream out = null;
        BufferedWriter writer = null;
        try {
            out = openFileOutput("data",Context.MODE_PRIVATE);
            writer = new BufferedWriter(new OutputStreamWriter(out));
            writer.write(inputText);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (writer !=null) {
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

首先在onCreate()方法中获取了EditText的实例,然后重写了onDestroy()方法,这样就可以保证在活动销毁之前一定会调用这个方法。在onDestroy()方法中我们获取了EditText中输入的内容,并调用save()方法把输入的内容存储到文件中,文件命名为data。

现在重新运行一下程序,并在EditText中输入一些内容,如图所示:

image

然后按下Back键关闭程序,这时我们输入的内容就已经保存到文件中了。

那么如何才能证实数据确实已经保存成功了呢?

书中描述:“我们可以借助Android DeviceMonitor工具来查看一下。点击Android Studio导航栏中的Tools→Android,会看到如图所示的工具列表”。已经过时了,不是这样的的。

现在如下图所示寻找:

image

点击 Device File Explorer 进入其标签页,在这里找到/data/data/com.zhouzhou.filepersistencetest/files/目录,可以看到生成了一个data文件。(注:Android 7.0系统的模拟器可能无法正常查看FileExplorer中的内容,这或许是新版模拟器的一个bug,可能会在未来的版本中修复。如果你遇到了这种情况,创建一个Android 6.0系统的模拟器即可解决。)如图所示:

image

然后书中所述:“点击下图中左边的按钮” 然后,选中文件,鼠标右击——save as 可将这个文件导出到电脑上:

image

image

使用记事本打开这个文件,里面的内容如图:

image

这样就证实了,在EditText中输入的内容确实已经成功保存到文件中了。不过只是成功将数据保存下来还不够,我们还需要想办法在下次启动程序的时候让这些数据能够还原到EditText中,因此接下来我们就要学习一下如何从文件中读取数据。

6.2.2 从文件中读取数据

类似于将数据存储到文件中,Context类中还提供了一个openFileInput()方法,用于从文件中读取数据。这个方法要比openFileOutput()简单一些,它只接收一个参数,即要读取的文件名,然后系统会自动到/data/data//files/目录下去加载这个文件,并返回一个FileInputStream对象,得到了这个对象之后再通过Java流的方式就可以将数据读取出来了。

以下是一段简单的代码示例,展示了如何从文件中读取文本数据:

    public String load () {
        FileInputStream in = null;
        BufferedReader reader = null;
        StringBuilder content = new StringBuilder();
        try {
            in = openFileInput("data");
            reader = new BufferedReader(new InputStreamReader(in));
            String line = "";
            while ((line = reader.readLine())!=null) {
                content.append(line);
            }
        }catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return content.toString();
    }

在这段代码中,首先通过openFileInput()方法获取到了一个FileInputStream对象,然后借助它又构建出了一个InputStreamReader对象,接着再使用InputStreamReader构建出一个BufferedReader对象,这样我们就可以通过BufferedReader进行一行行地读取,把文件中所有的文本内容全部读取出来,并存放在一个StringBuilder对象中,最后将读取到的内容返回就可以了。

了解了从文件中读取数据的方法,来继续完善上一小节中的例子,使得重新启动程序时EditText中能够保留我们上次输入的内容。修改MainActivity中的代码,如下所示:

package com.zhouzhou.filepersistencetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.EditText;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

public class MainActivity extends AppCompatActivity {
    private EditText edit;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        edit = (EditText) findViewById(R.id.edit);
        String inputText = load();
        if (!TextUtils.isEmpty(inputText)) {
            edit.setText(inputText);
            //将输入光标移动到文本的末尾位置以便于继续输入
            edit.setSelection(inputText.length());
            Toast.makeText(this,"Restoring succeeded",Toast.LENGTH_SHORT).show();
        }
    }
    public String load () {
        FileInputStream in = null;
        BufferedReader reader = null;
        StringBuilder content = new StringBuilder();
        try {
            in = openFileInput("data");
            reader = new BufferedReader(new InputStreamReader(in));
            String line = "";
            while ((line = reader.readLine())!=null) {
                content.append(line);
            }
        }catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return content.toString();
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        String inputText = edit.getText().toString();
        save(inputText);
    }
    public void save(String inputText) {
        FileOutputStream out = null;
        BufferedWriter writer = null;
        try {
            out = openFileOutput("data",Context.MODE_PRIVATE);
            writer = new BufferedWriter(new OutputStreamWriter(out));
            writer.write(inputText);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (writer !=null) {
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

这里的思路非常简单,在onCreate()方法中调用load()方法来读取文件中存储的文本内容,如果读到的内容不为null,就调用EditText的setText()方法将内容填充到EditText里,并调用setSelection()方法将输入光标移动到文本的末尾位置以便于继续输入,然后弹出一句还原成功的提示。

注意,上述代码在对字符串进行非空判断的时候使用了TextUtils.isEmpty()方法,这是一个非常好用的方法,它可以一次性进行两种空值的判断。当传入的字符串等于null或者等于空字符串的时候,这个方法都会返回true,从而使得我们不需要先单独判断这两种空值再使用逻辑运算符连接起来了。

现在重新运行一下程序,刚才保存的Content字符串肯定会被填充到EditText中:

image

然后,编写一点其他的内容,比如在EditText中输入Hello,接着按下Back键退出程序,再重新启动程序,这时刚才输入的内容并不会丢失,而是还原到了EditText中,如图所示:

image

文件存储方面的知识,其实所用到的核心技术就是Context类中提供的openFileInput()和openFileOutput()方法,之后就是利用Java的各种流来进行读写操作。

文件存储的方式并不适合用于保存一些较为复杂的文本数据,下面学习一下Android中另一种数据持久化的方式,它比文件存储更加简单易用,而且可以很方便地对某一指定的数据进行读写操作。

6.3 SharedPreferences存储

不同于文件的存储方式,SharedPreferences是使用键值对的方式来存储数据的。

也就是说,当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过这个键把相应的值取出来。而且SharedPreferences还支持多种不同的数据类型存储,如果存储的数据类型是整型,那么读取出来的数据也是整型的;如果存储的数据是一个字符串,那么读取出来的数据仍然是字符串。

这样你应该就能明显地感觉到,使用SharedPreferences来进行数据持久化要比使用文件方便很多。

6.3.1 将数据存储到SharedPreferences中

要想使用SharedPreferences来存储数据,首先需要获取到SharedPreferences对象。Android中主要提供了3种方法用于得到SharedPreferences对象。

  1. Context类中的getSharedPreferences()方法

此方法接收两个参数,第一个参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存放在/data/data//shared_prefs/目录下的。

第二个参数用于指定操作模式,目前只有MODE_PRIVATE这一种模式可选,它是默认的操作模式,和直接传入0效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。

其他几种操作模式均已被废弃,MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE这两种模式是在Android 4.2版本中被废弃的,MODE_MULTI_PROCESS模式是在Android6.0版本中被废弃的。

  1. Activity类中的getPreferences()方法

这个方法和Context中的getSharedPreferences()方法很相似,不过它只接收一个操作模式参数,因为使用这个方法时会自动将当前活动的类名作为SharedPreferences的文件名。

  1. PreferenceManager类中的getDefaultSharedPreferences()方法

这是一个静态方法,它接收一个Context参数,并自动使用当前应用程序的包名作为前缀来命名SharedPreferences文件。得到了SharedPreferences对象之后,就可以开始向SharedPreferences文件中存储数据了,主要可以分为3步实现。

(1) 调用SharedPreferences对象的edit()方法来获取一个SharedPreferences.Editor对象。

(2) 向SharedPreferences.Editor对象中添加数据,比如添加一个布尔型数据就使用putBoolean()方法,添加一个字符串则使用putString()方法,以此类推。

(3) 调用apply()方法将添加的数据提交,从而完成数据存储操作。

通过一个例子来体验一下SharedPreferences存储的用法吧。新建一个SharedPreferencesTest项目,然后修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/save_data"
        android:text="Save data"/>
</LinearLayout>

只是简单地放置了一个按钮,用于将一些数据存储到SharedPreferences文件当中。然后修改MainActivity中的代码,如下所示:

package com.zhouzhou.sharedpreferencestest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button saveData = (Button) findViewById(R.id.save_data);
        saveData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SharedPreferences.Editor editor = getSharedPreferences("data",MODE_PRIVATE).edit();
                editor.putString("name","Zhouzhou");
                editor.putInt("age",28);
                editor.putBoolean("married",false);
                editor.apply();
            }
        });
    }
}

可以看到,这里首先给按钮注册了一个点击事件,然后在点击事件中通过getSharedPreferences()方法指定SharedPreferences的文件名为data,并得到了SharedPreferences.Editor对象。

接着向这个对象中添加了3条不同类型的数据,最后调用apply()方法进行提交,从而完成了数据存储的操作。

现在就可以运行一下程序了,进入程序的主界面后,点击一下Save data按钮。这时的数据应该已经保存成功了,证实一下,进入/data/data/com.zhouzhou.sharedpreferencestest/shared_prefs/目录下,可以看到生成了一个data.xml文件,如图:

image

在按钮的点击事件中添加的所有数据都已经成功保存下来了,并且SharedPreferences文件是使用XML格式来对数据进行管理的。那么接下来我们自然要看一看,如何从SharedPreferences文件中去读取这些数据了。

6.3.2 从SharedPreferences中读取数据

使用SharedPreferences来存储数据是非常简单的,从SharedPreferences文件中读取数据会更加地简单。

SharedPreferences对象中提供了一系列的get方法,用于对存储的数据进行读取,每种get方法都对应了SharedPreferences.Editor中的一种put方法,比如读取一个布尔型数据就使用getBoolean()方法,读取一个字符串就使用getString()方法。

这些get方法都接收两个参数,第一个参数是键,传入存储数据时使用的键就可以得到相应的值了;第二个参数是默认值,即表示当传入的键找不到对应的值时会以什么样的默认值进行返回。

在SharedPreferencesTest项目的基础上继续开发,修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/save_data"
        android:text="Save data"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/restore_data"
        android:text="Restore data"/>
</LinearLayout>

增加了一个还原数据的按钮,通过点击这个按钮来从SharedPreferences文件中读取数据。修改MainActivity中的代码,如下所示:

package com.zhouzhou.sharedpreferencestest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button saveData = (Button) findViewById(R.id.save_data);
        saveData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SharedPreferences.Editor editor = getSharedPreferences("data",MODE_PRIVATE).edit();
                editor.putString("name","Zhouzhou");
                editor.putInt("age",28);
                editor.putBoolean("married",false);
                editor.apply();
            }
        });
        Button restoreData = (Button) findViewById(R.id.restore_data);
        restoreData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SharedPreferences pref = getSharedPreferences("data",MODE_PRIVATE);
                String name = pref.getString("name","");
                int age = pref.getInt("age",0);
                boolean married = pref.getBoolean("married",false);
                Log.d("MainActivity","name is " + name);
                Log.d("MainActivity","age is " + age);
                Log.d("MainActivity","married is " + married);
            }
        });
    }
}

在还原数据按钮的点击事件中首先通过getSharedPreferences()方法得到了SharedPreferences对象,然后分别调用它的getString()、getInt()和getBoolean()方法,去获取前面所存储的姓名、年龄和是否已婚,如果没有找到相应的值,就会使用方法中传入的默认值来代替,最后通过Log将这些值打印出来。

现在重新运行一下程序,并点击界面上的Restore data按钮,然后查看logcat中的打印信息,如图:

image

相比之下,SharedPreferences存储确实要比文本存储简单方便了许多,应用场景也多了不少,比如很多应用程序中的偏好设置功能其实都使用到了SharedPreferences技术。下面我们就来编写一个记住密码的功能,通过这个例子能够加深对SharedPreferences的理解。

6.3.3 实现记住密码功能

在上一章中的最佳实践部分已经编写过一个登录界面了,那就首先打开BroadcastBestPractice项目,来编辑一下登录界面的布局。修改activity_login.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Account:"/>
        <EditText
            android:id="@+id/account"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical"/>
    </LinearLayout>
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="60dp">
        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Password"/>
        <EditText
            android:id="@+id/password"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical"
            android:inputType="textPassword"/>
    </LinearLayout>
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <CheckBox
            android:id="@+id/remember_pass"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:text="Remember password"/>
    </LinearLayout>
    <Button
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:text="Login"/>
</LinearLayout>

使用到了一个新控件CheckBox。这是一个复选框控件,用户可以通过点击的方式来进行选中和取消,我们就使用这个控件来表示用户是否需要记住密码。然后修改LoginActivity中的代码,如下所示:

package com.zhouzhou.broadcastbestpractice;

import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

import java.util.prefs.PreferenceChangeEvent;

public class LoginActivity extends BaseActivity {
    private SharedPreferences pref;
    private SharedPreferences.Editor editor;
    private EditText accountEdit;
    private EditText passwordEdit;
    private Button login;
    private CheckBox rememberPass;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        pref = PreferenceManager.getDefaultSharedPreferences(this);
        accountEdit = (EditText) findViewById(R.id.account);
        passwordEdit = (EditText) findViewById(R.id.password);
        rememberPass = (CheckBox) findViewById(R.id.remember_pass);
        login = (Button) findViewById(R.id.login);
        boolean isRemember = pref.getBoolean("remember_password",false);
        if (isRemember) {
            //将账号喝密码都设置到文本框中
            String account = pref.getString("account","");
            String password = pref.getString("password","");
            accountEdit.setText(account);
            passwordEdit.setText(password);
            rememberPass.setChecked(true);
        }
        login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String account = accountEdit.getText().toString();
                String password = passwordEdit.getText().toString();
                //如果账号是admin且密码是123456,就认为登录成功
                if (account.equals("admin") && password.equals("123456")) {
                    editor = pref.edit();
                    if (rememberPass.isChecked()) {
                        //检查复选框是否呗选中
                        editor.putBoolean("remember_password",true);
                        editor.putString("account",account);
                        editor.putString("password",password);
                    } else {
                        editor.clear();
                    }
                    editor.apply();
                    Intent intent = new Intent(LoginActivity.this,MainActivity.class);
                    startActivity(intent);
                    finish();
                } else {
                    Toast.makeText(LoginActivity.this,"account or password is invalid",Toast.LENGTH_SHORT).show();
                }
            }
        });
    }
}
  1. 首先在onCreate()方法中获取到了SharedPreferences对象。
  2. 然后调用它的getBoolean()方法去获取remember_password这个键对应的值。

(一开始当然不存在对应的值了,所以会使用默认值false,这样就什么都不会发生。)

  1. 接着在登录成功之后,会调用CheckBox的isChecked()方法来检查复选框是否被选中。

(如果被选中了,则表示用户想要记住密码,这时将remember_password设置为true,然后把account和password对应的值都存入到SharedPreferences文件当中并提交。如果没有被选中,就简单地调用一下clear()方法,将SharedPreferences文件中的数据全部清除掉。)

当用户选中了记住密码复选框,并成功登录一次之后,remember_password键对应的值就是true了,这个时候如果再重新启动登录界面,就会从SharedPreferences文件中将保存的账号和密码都读取出来,并填充到文本输入框中,然后把记住密码复选框选中,这样就完成记住密码的功能了。

现在重新运行一下程序,可以看到界面上多出了一个记住密码复选框,如图所示:

image

然后账号输入admin,密码输入123456,并选中记住密码复选框,点击登录,就会跳转到MainActivity。接着在MainActivity中发出一条强制下线广播,会让程序重新回到登录界面,此时你会发现,账号密码都已经自动填充到界面上了,如图所示:

image

这样我们就使用SharedPreferences技术将记住密码功能成功实现了。不过需要注意,这里实现的记住密码功能仍然只是个简单的示例,并不能在实际的项目中直接使用。因为将密码以明文的形式存储在SharedPreferences文件中是非常不安全的,很容易就会被别人盗取,因此在正式的项目里还需要结合一定的加密算法来对密码进行保护才行。

6.4 SQLite数据库存储

SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百KB的内存就足够了,因而特别适合在移动设备上使用。SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务。而SQLite又比一般的数据库要简单得多,它甚至不用设置用户名和密码就可以使用。Android正是把这个功能极为强大的数据库嵌入到了系统当中,使得本地持久化的功能有了一次质的飞跃。

前面所学的文件存储和SharedPreferences存储毕竟只适用于保存一些简单的数据和键值对,当需要存储大量复杂的关系型数据的时候,你就会发现以上两种存储方式很难应付得了。比如我们手机的短信程序中可能会有很多个会话,每个会话中又包含了很多条信息内容,并且大部分会话还可能各自对应了电话簿中的某个联系人。很难想象如何用文件或者SharedPreferences来存储这些数据量大、结构性复杂的数据吧?但是使用数据库就可以做得到。

6.4.1 创建数据库

Android为了让我们能够更加方便地管理数据库,专门提供了一个SQLiteOpenHelper帮助类,借助这个类就可以非常简单地对数据库进行创建和升级。

  1. SQLiteOpenHelper是一个抽象类,这意味着如果想要使用它的话,就需要创建一个自己的帮助类去继承它。
  • SQLiteOpenHelper中有两个抽象方法,分别是onCreate()onUpgrade(),必须在自己的帮助类里面重写这两个方法,然后分别在这两个方法中去实现创建、升级数据库的逻辑。
  1. SQLiteOpenHelper中还有两个非常重要的实例方法:getReadableDatabase()和get-WritableDatabase()。
  • 这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。
  • 不同的是,当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase()方法返回的对象将以只读的方式去打开数据库,而getWritableDatabase()方法则将出现异常。
  1. SQLiteOpenHelper中有两个构造方法可供重写,一般使用参数少一点的那个构造方法即可。
  • 这个构造方法中接收4个参数,第一个参数是Context,必须要有它才能对数据库进行操作。
  • 第二个参数是数据库名,创建数据库时使用的就是这里指定的名称。
  • 第三个参数允许我们在查询数据的时候返回一个自定义的Cursor,一般都是传入null。
  • 第四个参数表示当前数据库的版本号,可用于对数据库进行升级操作。

构建出SQLiteOpenHelper的实例之后,再调用它的getReadableDatabase()或getWritableDatabase()方法就能够创建数据库了,数据库文件会存放在/data/data//databases/目录下。此时,重写的onCreate()方法也会得到执行,所以通常会在这里去处理一些创建表的逻辑。

接下来通过例子的方式来更加直观地体会SQLiteOpenHelper的用法吧,首先新建一个DatabaseTest项目。这里我们希望创建一个名为BookStore.db的数据库,然后在这个数据库中新建一张Book表,表中有id(主键)、作者、价格、页数和书名等列。创建数据库表当然还是需要用建表语句的,这里也是要考验一下你的SQL基本功了,Book表的建表语句如下所示:

create table Book (
    id integer primary key autoincrement,
    author text,
    price real,
    pages integer,
    name text)

SQLite不像其他的数据库拥有众多繁杂的数据类型,它的数据类型很简单,integer表示整型,real表示浮点型,text表示文本类型,blob表示二进制类型。另外,上述建表语句中我们还使用了primary key将id列设为主键,并用autoincrement关键字表示id列是自增长的。

然后需要在代码中去执行这条SQL语句,才能完成创建表的操作。新建MyDatabaseHelper类继承自SQLiteOpenHelper,代码如下所示:

package com.zhouzhou.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;

import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper {
    public static final String CREATE_BOOK = "create table Book ("
            + "id integer primary key autoincrement, "
            + "author text, "
            + "price real, "
            + "pages integer, "
            + "name text)";
    private Context mContext;
    public MyDatabaseHelper(@Nullable Context context, @Nullable String name, @Nullable 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,"Create succeeded",Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
    }
}

可以看到,我们把建表语句定义成了一个字符串常量,然后在onCreate()方法中又调用了SQLiteDatabase的execSQL()方法去执行这条建表语句,并弹出一个Toast提示创建成功,这样就可以保证在数据库创建完成的同时还能成功创建Book表。现在修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/create_database"
        android:text="Create database"/>
</LinearLayout>

布局文件很简单,就是加入了一个按钮,用于创建数据库。最后修改MainActivity中的代码,如下所示:

package com.zhouzhou.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    private MyDatabaseHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 1);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dbHelper.getWritableDatabase();
            }
        });
    }
}

这里我们在onCreate()方法中构建了一个MyDatabaseHelper对象,并且通过构造函数的参数将数据库名指定为BookStore.db,版本号指定为1,然后在Create database按钮的点击事件里调用了getWritableDatabase()方法.

这样当第一次点击Create database按钮时,就会检测到当前程序中并没有BookStore.db这个数据库,于是会创建该数据库并调用MyDatabaseHelper中的onCreate()方法,这样Book表也就得到了创建,然后会弹出一个Toast提示创建成功。再次点击Create database按钮时,会发现此时已经存在BookStore.db数据库了,因此不会再创建一次。

现在就可以运行一下代码了,在程序主界面点击Create database按钮,结果如图:

image

此时BookStore.db数据库和Book表应该都已经创建成功了,因为当你再次点击Create database按钮时,不会再有Toast弹出。

怎样才能证实它们的确创建成功了?如果还是使用FileExplorer,那么最多你只能看到databases目录下出现了一个BookStore.db文件,Book表是无法通过FileExplorer看到的。

image

因此这次我们准备换一种查看方式,使用adb shell来对数据库和表的创建情况进行检查

adb是Android SDK中自带的一个调试工具,使用这个工具可以直接对连接在电脑上的手机或模拟器进行调试操作。它存放在sdk的platform-tools目录下,如果想要在命令行中使用这个工具,就需要先把它的路径配置到环境变量里。如果你使用的是Windows系统,可以右击计算机→属性→高级系统设置→环境变量,然后在系统变量里找到Path并点击编辑,将platform-tools目录配置进去,如图:

image

如果你使用的是Linux或Mac系统,可以在home路径下编辑.bash_文件,将platform-tools目录配置进去即可(注:这个网站https://www.androiddevtools.cn/ 下载的SDK Platform-Tools),如图:

export PATH=$PATH:$HOME/android-sdk-linux/platform-toos

配置好了环境变量之后,就可以使用adb工具了。打开命令行界面,输入adb shell,就会进入到设备的控制台,如图:

image

其中,#符号是超级管理员的意思,也就是说现在你可以访问模拟器中的一切数据。如果你的命令行上显示的是$符号,那么就表示你现在是普遍管理员,需输入su命令切换成超级管理员,才能执行下面的操作。

这里出现不能切换为超级管理员的问题了,解决方式如下:

  1. 配置好环境变量后,打开cmd输入adb shell 进入控制界面

  2. 发现是普通管理员,想利用su命令切换为超级管理员,不行

image

问题原因:这个问题是因为我们用的模拟器,带有了Googel play 是不允许获得管理员权限。

解决办法:

要去下载一个Target是Google APIS的模拟器,打开AVD Manager,选择创建一个新的设备并给它更换镜像。

image

image

image

再尝试一下su命令,成功解决了不存在的问题了,如图所示:

image

接下来使用cd命令进入到/data/data/com.zhouzhou.databasetest/databases/目录下,并使用ls命令查看到该目录里的文件,如图:

image

这个目录下出现了两个数据库文件,一个正是我们创建的BookStore.db,而另一个BookStore. db-journal则是为了让数据库能够支持事务而产生的临时日志文件,通常情况下这个文件的大小都是0字节。

接下来我们就要借助sqlite命令来打开数据库了,只需要键入sqlite3,后面加上数据库名即可,如图所示:

image

这时就已经打开了BookStore.db数据库,现在就可以对这个数据库中的表进行管理了。首先来看一下目前数据库中有哪些表,键入.table命令,如图:

image

可以看到,此时数据库中有两张表,android_metadata表是每个数据库中都会自动生成的,不用管它,而另外一张Book表就是我们在MyDatabaseHelper中创建的了。这里还可以通过.schema命令来查看它们的建表语句,如图所示:

image

由此证明,BookStore.db数据库和Book表确实已经创建成功了。之后键入.exit或.quit命令可以退出数据库的编辑,再键入exit命令就可以退出设备控制台了。

image

6.4.2 升级数据库

MyDatabaseHelper中还有一个onUpgrade()方法是用于对数据库进行升级的,它在整个数据库的管理工作当中起着非常重要的作用,可千万不能忽视它哟。

目前DatabaseTest项目中已经有一张Book表用于存放书的各种详细数据,如果我们想再添加一张Category表用于记录图书的分类,该怎么做呢?比如Category表中有id(主键)、分类名和分类代码这几个列,那么建表语句就可以写成:

create table Category (
    id integer primary key autoincrement,
    category_name text,
    category_code integer)

接下来我们将这条建表语句添加到MyDatabaseHelper中,代码如下所示:

package com.zhouzhou.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;

import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper {
    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)";
    private Context mContext;
    public MyDatabaseHelper(@Nullable Context context, @Nullable String name, @Nullable 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,"Create succeeded",Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
    }
}

看上去好像都挺对的吧?现在我们重新运行一下程序,并点击Createdatabase按钮,竟然没有弹出创建成功的提示。当然,你也可以通过adb工具到数据库中再去检查一下,这样你会更加地

实没有创建成功的原因不难思考,因为此时BookStore.db数据库已经存在了,之后不管我们怎样点击Create database按钮,MyDatabaseHelper中的onCreate()方法都不会再次执行,因此新添加的表也就无法得到创建了

解决这个问题的办法也相当简单,只需要先将程序卸载掉,然后重新运行,这时BookStore.db数据库已经不存在了,如果再点击Create database按钮,MyDatabaseHelper中的onCreate()方法就会执行,这时Category表就可以创建成功了。

不过,通过卸载程序的方式来新增一张表毫无疑问是很极端的做法,其实我们只需要巧妙地运用SQLiteOpenHelper的升级功能就可以很轻松地解决这个问题。修改MyDatabaseHelper中的代码,如下所示:

public class MyDatabaseHelper extends SQLiteOpenHelper {
    ...
    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        sqLiteDatabase.execSQL("drop table if exists Book");
        sqLiteDatabase.execSQL("drop table if exists Category");
        onCreate(sqLiteDatabase);
    }
}

可以看到,我们在onUpgrade()方法中执行了两条DROP语句,如果发现数据库中已经存在Book表或Category表了,就将这两张表删除掉,然后再调用onCreate()方法重新创建。

这里先将已经存在的表删除掉,因为如果在创建表时发现这张表已经存在了,就会直接报错。

接下来的问题就是如何让onUpgrade()方法能够执行了,还记得SQLiteOpenHelper的构造方法里接收的第四个参数吗?它表示当前数据库的版本号,之前我们传入的是1,现在只要传入一个比1大的数,就可以让onUpgrade()方法得到执行了。修改MainActivity中的代码,如下所示:

package com.zhouzhou.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    private MyDatabaseHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dbHelper.getWritableDatabase();
            }
        });
    }
}

这里将数据库版本号指定为2,表示我们对数据库进行升级了。现在重新运行程序,并点击Create database按钮,这时就会再次弹出创建成功的提示。

为了验证一下Category表是不是已经创建成功了,我们在adb shell中打开BookStore.db数据库,然后键入.table命令,结果如图:

image

接着键入.schema命令查看一下建表语句,结果如图:

image

由此可以看出,Category表已经创建成功了,同时也说明我们的升级功能的确起到了作用。

6.4.3 添加数据

我们可以对数据进行的操作无非有4种,即CRUD。其中C代表添加(Create), R代表查询(Retrieve), U代表更新(Update),D代表删除(Delete)。

每一种操作又各自对应了一种SQL命令,添加数据时使用insert,查询数据时使用select,更新数据时使用update,删除数据时使用delete。但是开发者的水平总会是参差不齐的,未必每一个人都能非常熟悉地使用SQL语言,因此Android也提供了一系列的辅助性方法,使得在Android中即使不去编写SQL语句,也能轻松完成所有的CRUD操作。

前面我们已经知道,调用SQLiteOpenHelper的getReadableDatabase()或getWritableDatabase()方法是可以用于创建和升级数据库的,不仅如此,这两个方法还都会返回一个SQLiteDatabase对象,借助这个对象就可以对数据进行CRUD操作了。

SQLiteDatabase中提供了一个insert()方法,这个方法就是专门用于添加数据的。它接收3个参数:

  • 第一个参数是表名,我们希望向哪张表里添加数据。
  • 第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL,一般我们用不到这个功能,直接传入null即可。
  • 第三个参数是一个ContentValues对象,它提供了一系列的put()方法重载,用于向ContentValues中添加数据,只需要将表中的每个列名以及相应的待添加数据传入即可。

介绍完了基本用法,接下来还是通过例子的方式来亲身体验一下如何添加数据吧。修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/create_database"
        android:text="Create database"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/add_data"
        android:text="Add data"/>
</LinearLayout>

在布局文件中又新增了一个按钮,稍后就会在这个按钮的点击事件里编写添加数据的逻辑。接着修改MainActivity中的代码,如下所示:

package com.zhouzhou.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    private MyDatabaseHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dbHelper.getWritableDatabase();
            }
        });
        Button addButton = (Button) findViewById(R.id.add_data);
        addButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                //开始组装第一条数据
                values.put("name","The Da Vinci Code");
                values.put("author","Dan Brown");
                values.put("pages",454);
                values.put("price",16.96);
                db.insert("Book",null,values);//插入第一条数据
                values.clear();;
                //开始第二条数据
                values.put("name","The Lost Symbol");
                values.put("author","Dan Brown");
                values.put("pages",510);
                values.put("price",19.95);
                db.insert("Book",null,values);//插入第二条数据
            }
        });
    }
}

在添加数据按钮的点击事件里面,我们先获取到了SQLiteDatabase对象,然后使用ContentValues来对要添加的数据进行组装。

这里只对Book表里其中四列的数据进行了组装,id那一列并没给它赋值。因为在前面创建表的时候,我们就将id列设置为自增长了,它的值会在入库的时候自动生成,所以不需要手动给它赋值了。

接下来调用了insert()方法将数据添加到表当中,注意这里我们实际上添加了两条数据,上述代码中使用ContentValues分别组装了两次不同的内容,并调用了两次insert()方法。

现在可以重新运行一下程序了,界面如图:

image

点击一下Add data按钮,此时两条数据应该都已经添加成功了,不过为了证实一下,我们还是打开BookStore.db数据库瞧一瞧。输入SQL查询语句select * from Book;,结果如图:(注:总是查不出来,我的办法是,到Decive File Explorer 中来到 com.zhouzhou.databasetest包,再将databases中的BookStroe.db和BookStroe.db-journal 都手动删除了。再次运行程序,两个按钮的Demo都成立,并且查出数据了。但是,这样做是极端且非常不正确的。

image

6.4.4 更新数据

SQLiteDatabase中也提供了一个非常好用的update()方法,用于对数据进行更新,这个方法接收4个参数:

  • 第一个参数和insert()方法一样,也是表名,在这里指定去更新哪张表里的数据。
  • 第二个参数是ContentValues对象,要把更新数据在这里组装进去。
  • 第三、第四个参数用于约束更新某一行或某几行中的数据,不指定的话默认就是更新所有行。

那么接下来仍然是在DatabaseTest项目的基础上修改,看一下更新数据的具体用法。比如说刚才添加到数据库里的第一本书,由于过了畅销季,卖得不是很火了,现在需要通过降低价格的方式来吸引更多的顾客,我们应该怎么操作呢?首先修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    ...
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/update_data"
        android:text="Update data"/>
</LinearLayout>

布局文件中的代码已经非常简单了,就是添加了一个用于更新数据的按钮。然后修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {
    private MyDatabaseHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
       ...
        Button updateData = (Button) findViewById(R.id.update_data);
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("price",99.9);
                db.update("Book",values,"name = ?",new String[]{
                        "The Da Vinci Code"
                });
            }
        });
    }
}

这里在更新数据按钮的点击事件里面构建了一个ContentValues对象,并且只给它指定了一组数据,说明我们只是想把价格这一列的数据更新成99.9。然后调用了SQLiteDatabase的update()方法去执行具体的更新操作,可以看到,这里使用了第三、第四个参数来指定具体更新哪几行。第三个参数对应的是SQL语句的where部分,表示更新所有name等于?的行,而?是一个占位符,可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应的内容。

现在重新运行一下程序,点击一下Update data按钮后,再次输入查询语句查看表中的数据情况,可以看到,The Da Vinci Code这本书的价格已经被成功改为99.9了。

6.4.5 删除数据

SQLiteDatabase中提供了一个delete()方法,专门用于删除数据,这个方法接收3个参数,第一个参数仍然是表名,这个已经没什么好说的了,第二、第三个参数又是用于约束删除某一行或某几行的数据,不指定的话默认就是删除所有行。修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    ...
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/delete_data"
        android:text="Delete data"/>
</LinearLayout>

仍然是在布局文件中添加了一个按钮,用于删除数据。然后修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {
    private MyDatabaseHelper dbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
        ...
        Button deleteButton = (Button) findViewById(R.id.delete_data);
        deleteButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                db.delete("Book", "pages > ?", new String[]{"500"});
            }
        });
    }
}

可以看到,我们在删除按钮的点击事件里指明去删除Book表中的数据,并且通过第二、第三个参数来指定仅删除那些页数超过500页的书。

6.4.6 查询数据

SQL的全称是Structured Query Language,翻译成中文就是结构化查询语言。它的大部功能都体现在“查”这个字上的,而“增删改”只是其中的一小部分功能。由于SQL查询涉及的内容实在是太多了,因此在这里我不准备对它展开来讲解,而是只会介绍Android上的查询功能。

SQLiteDatabase中还提供了一个query()方法用于对数据进行查询。这个方法的参数非常复杂,最短的一个方法重载也需要传入7个参数。

  • 第一个参数不用说,当然还是表名,表示我们希望从哪张表中查询数据。
  • 第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列。
  • 第三、第四个参数用于约束查询某一行或某几行的数据,不指定则默认查询所有行的数据。
  • 第五个参数用于指定需要去group by的列,不指定则表示不对查询结果进行group by操作。
  • 第六个参数用于对group by之后的数据进行进一步的过滤,不指定则表示不进行过滤。
  • 第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。更多详细的内容可以参考下表。其他几个query()方法的重载其实也大同小异,可以自己去研究一下。

image

然query()方法的参数非常多,但是不要对它产生畏惧,因为我们不必为每条查询语句都指定所有的参数,多数情况下只需要传入少数几个参数就可以完成查询操作了。调用query()方法后会返回一个Cursor对象,查询到的所有数据都将从这个对象中取出。下面还是让我们通过例子的方式来体验一下查询数据的具体用法,修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    ...
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/query_data"
        android:text="Query data"/>
</LinearLayout>

添加了一个按钮用于查询数据。然后修改MainActivity中的代码,如下所示:

package com.zhouzhou.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    private MyDatabaseHelper dbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dbHelper.getWritableDatabase();
            }
        });
        Button addButton = (Button) findViewById(R.id.add_data);
        addButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                //开始组装第一条数据
                values.put("name", "The Da Vinci Code");
                values.put("author", "Dan Brown");
                values.put("pages", 454);
                values.put("price", 16.96);
                db.insert("Book", null, values);//插入第一条数据
                values.clear();
                ;
                //开始第二条数据
                values.put("name", "The Lost Symbol");
                values.put("author", "Dan Brown");
                values.put("pages", 510);
                values.put("price", 19.95);
                db.insert("Book", null, values);//插入第二条数据
            }
        });
        Button updateData = (Button) findViewById(R.id.update_data);
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("price", 99.9);
                db.update("Book", values, "name = ?", new String[]{
                        "The Da Vinci Code"
                });
            }
        });
        Button deleteButton = (Button) findViewById(R.id.delete_data);
        deleteButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                db.delete("Book", "pages > ?", new String[]{"500"});
            }
        });
        Button queryButton = (Button) findViewById(R.id.query_data);
        queryButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                //查询Book表中的所有数据
                Cursor cursor = db.query("Book",null,null,null,null,null,null);
                if (cursor.moveToFirst()) {
                    do {
                        //遍历Cursor对象,取出数据并打印
                        @SuppressLint("Range") String name = cursor.getString(cursor.getColumnIndex("name"));
                        @SuppressLint("Range") String author = cursor.getString(cursor.getColumnIndex("author"));
                        @SuppressLint("Range") int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                        @SuppressLint("Range") double price = cursor.getDouble(cursor.getColumnIndex("price"));
                        Log.d("MainActivity","book name is " + name);
                        Log.d("MainActivity","book author is " + author);
                        Log.d("MainActivity","book pages is " + pages);
                        Log.d("MainActivity","book price is " + price);
                    } while (cursor.moveToNext());
                }
                cursor.close();
            }
        });
    }
}

首先在查询按钮的点击事件里面调用了SQLiteDatabase的query()方法去查询数据。这里的query()方法非常简单,只是使用了第一个参数指明去查询Book表,后面的参数全部为null。这就表示希望查询这张表中的所有数据,虽然这张表中目前只剩下一条数据了。查询完之后就得到了一个Cursor对象,接着我们调用它的moveToFirst()方法将数据的指针移动到第一行的位置,然后进入了一个循环当中,去遍历查询到的每一行数据。在这个循环中可以通过Cursor的getColumnIndex()方法获取到某一列在表中对应的位置索引,然后将这个索引传入到相应的取值方法中,就可以得到从数据库中读取到的数据了。接着我们使用Log的方式将取出的数据打印出来,借此来检查一下读取工作有没有成功完成。最后别忘了调用close()方法来关闭Cursor。现在再次重新运行程序,点击一下Query data按钮后,查看logcat的打印内容,结果如图:

image

这个例子只是对查询数据的用法进行了最简单的示范,在真正的项目中你可能会遇到比这要复杂得多的查询功能,更多高级的用法还需要你自己去慢慢摸索,毕竟query()方法中还有那么多的参数我们都还没用到呢。

6.4.7 使用SQL操作数据库

虽然Android已经给我们提供了很多非常方便的API用于操作数据库,不过总会有一些人不习惯去使用这些辅助性的方法,而是更加青睐于直接使用SQL来操作数据库。Android充分考虑到这种的编程习惯,同样提供了一系列的方法,使得可以直接通过SQL来操作数据库。下面简略演示一下,如何直接使用SQL来完成前面几小节中学过的CRUD操作。

  • 添加数据的方法如下:
db.execSQL("insert into Book (name,author,pages,price) values(?,?,?,?)",new String[] {"The Da Vinci Code","Dan Brown","454","16.96"});
db.execSQL("insert ioto Book (name,author,pages,price) values(?,?,?,?)",new String[] 
{"The Lost Symbol","Dan Brown","510","19.95"});
  • 更新数据的方法如下:
db.execSQL("update Book set price = ? where name = ? ",new String[]{"10.99","The Da Vinci Code" });
  • 删除数据的方法如下:
db.execSQL("delete from Book where pages > ? ",new String[] { "500" });
  • 查询数据的方法如下:
db.rawQuery("select * from Book",null);

除了查询数据的时候调用的是SQLiteDatabase的rawQuery()方法,其他的操作都是调用的execSQL()方法。

以上演示的几种方式,执行结果会和前面几小节中我们学习的CRUD操作的结果完全相同,选择使用哪一种方式就看你个人的喜好了。

6.5 使用LitePal操作数据库

新建一个LitePalTest项目,然后开始我们本节的学习之旅吧。

6.5.1 LitePal简介

现在开源的热潮让所有Android开发者都大大受益,GitHub上面有成百上千的优秀Android开源项目,很多之前我们要写很久才能实现的功能,使用开源库可能短短几分钟就能实现了

除此之外,公司里的代码非常强调稳定性,而我们自己写出的代码往往越复杂就越容易出问题。相反,开源项目的代码都是经过时间验证的,通常比我们自己的代码要稳定得多。因此,现在有很多公司为了追求开发效率以及项目稳定性,都会选择使用开源库。本书中我们将会学习多个开源库的使用方法,而现在你将正式开始接触第一个开源库——LitePal。

LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,并将我们平时开发最常用到的一些数据库功能进行了封装,使得不用编写一行SQL语句就可以完成各种建表和増删改查的操作。LitePal的项目主页上也有详细的使用文档,地址是:https://github.com/LitePalFramework/LitePal。

6.5.2 配置LitePal

image

那么怎样才能在项目中使用开源库呢?

过去的方式比较复杂,通常需要下载开源库的Jar包或者源码,然后再集成到项目当中。而现在就简单得多了,大多数的开源项目都会将版本提交到jcenter上,只需要在app/build.gradle文件中声明该开源库的引用就可以了。因此,要使用LitePal的第一步,就是编辑app/build.gradle文件,在dependencies闭包中添加如下内容:

dependencies {
    implementation 'org.litepal.guolindev:core:3.2.3'
}

这样就把LitePal成功引入到当前项目中了,接下来需要配置litepal.xml文件。右击app/src/main目录→New→Directory,创建一个assets目录,然后在assets目录右击→New→File 再新建一个litepal.xml文件(.xml后缀要保留),接着编辑litepal.xml文件中的内容,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <!--
    	Define the database name of your application.
    	By default each database name should be end with .db.
    	If you didn't name your database end with .db,
    	LitePal would plus the suffix automatically for you.
    	For example:
    	<dbname value="demo" />
    -->
    <dbname value="BookStore" />

    <!--
    	Define the version of your database. Each time you want
    	to upgrade your database, the version tag would helps.
    	Modify the models you defined in the mapping tag, and just
    	make the version value plus one, the upgrade of database
    	will be processed automatically without concern.
			For example:
    	<version value="1" />
    -->
    <version value="1" />

    <!--
    	Define your models in the list with mapping tag, LitePal will
    	create tables for each mapping class. The supported fields
    	defined in models will be mapped into columns.
    	For example:
    	<list>
    		<mapping class="com.test.model.Reader" />
    		<mapping class="com.test.model.Magazine" />
    	</list>
    -->
    <list>
    </list>

    <!--
        Define where the .db file should be. "internal" means the .db file
        will be stored in the database folder of internal storage which no
        one can access. "external" means the .db file will be stored in the
        path to the directory on the primary external storage device where
        the application can place persistent files it owns which everyone
        can access. "internal" will act as default.
        For example:
        <storage value="external" />
    -->

</litepal>

其中,<dbname>标签用于指定数据库名,<version>标签用于指定数据库版本号,<list>标签用于指定所有的映射模型,稍后就会用到。

错误示范:下载Litepal的依赖包(GitHub或者码云),下载之后将后缀.zip去掉,再复制到libs中,再Add As Library...玛雅!!!我是在哪里看到的要去这个依赖包,然后做如下图的骚操作,这样的错误操作,我找了好久好久的bug,都芭比Q了,愁死我了,整了阿里云的以赖,整了.gradle,重新安装AS...办法都试过了,最后,我看到一段可喜可贺的报错信息‘Execution failed for task ':app:checkDebugDuplicateClasses'这代表着以依赖了重复的资源文件,例如:lib中导入了A.aar文件,然后又在build.gradle中依赖了相同的A文件。或者,依赖两个不同的资源文件,但是,这两个资源文件里面包含了相同的内容。解决方式是,去除重复的资源文件即可。泪目泪目泪目!!!我删掉了项目中画蛇添足下载并导入的Litepal的依赖包,再次运行,完全OK了。呜呜呜~浪费好多时间

image

最后,还需要再配置一下LitePalApplication,修改AndroidManifest.xml中的代码,如下所示:

<manifest>
    <application
        android:name="org.litepal.LitePalApplication"
        ...
    >
        ...
    </application>
</manifest>

将项目的application配置为org.litepal.LitePalApplication,这样才能让LitePal的所有功能都可以正常工作。

Of course you may have your own Application and has already configured here, like:

<manifest>
    <application
        android:name="com.example.MyOwnApplication"
        ...
    >
        ...
    </application>
</manifest>

That's OK. LitePal can still live with that. Just call LitePal.initialize(context) in your own Application:

public class MyOwnApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        LitePal.initialize(this);
    }
    ...
}

Make sure to call this method as early as you can. In the onCreate()method of Application will be fine. And always remember to use the application context as parameter. Do not use any instance of activity or service as parameter, or memory leaks might happen.

现在LitePal的配置工作已经全部结束了,下面我们开始正式使用它吧。(可以使用GitHub中的例子,下面继续书中所述,继续记录书中的笔记)

6.5.3 创建和升级数据库

之前创建数据库是通过自定义一个类继承自SQLiteOpenHelper,然后在onCreate()方法中编写建表语句来实现的,而使用LitePal就不用再这么麻烦了。

先将activity_main.xml布局文件从DatabaseTest项目复制到LitePalTest项目中来。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/create_database"
        android:text="Create database"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/add_data"
        android:text="Add data"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/update_data"
        android:text="Update data"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/delete_data"
        android:text="Delete data"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/query_data"
        android:text="Query data"/>
</LinearLayout>

LitePal采取的是对象关系映射(ORM)的模式,对象关系映射,简单点说,我们使用的编程语言是面向对象语言,而使用的数据库则是关系型数据库,那么将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是对象关系映射了。

对象关系映射模式,它赋予了我们一个强大的功能,就是可以用面向对象的思维来操作数据库,而不用再和SQL语句打交道了,为了创建一张Book表,需要先分析表中应该包含哪些列,然后再编写出一条建表语句,最后在自定义的SQLiteOpenHelper中去执行这条建表语句。但是使用LitePal,就可以用面向对象的思维来实现同样的功能了,定义一个Book类,代码如下所示:

package com.zhouzhou.litepaltest;

public class Book {
    private int id;
    private String author;
    private double price;
    private int pages;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public int getPages() {
        return pages;
    }

    public void setPages(int pages) {
        this.pages = pages;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

这是一个典型的Java bean,在Book类中定义了id、author、price、pages、name这几个字段,并生成了相应的getter和setter方法。Book类就会对应数据库中的Book表,而类中的每一个字段分别对应了表中的每一个列,这就是对象关系映射最直观的体验。

接下来我们还需要将Book类添加到映射模型列表当中,修改litepal.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value="BookStore" />
    <version value="1" />
    <!--
    	Define your models in the list with mapping tag, LitePal will
    	create tables for each mapping class. The supported fields
    	defined in models will be mapped into columns.
    	For example:
    	<list>
    		<mapping class="com.test.model.Reader" />
    		<mapping class="com.test.model.Magazine" />
    	</list>
    -->
    <list>
        <mapping class="com.zhouzhou.litepaltest.Book"/>
    </list>
</litepal>

这里使用<mapping>标签来声明要配置的映射模型类,注意一定要使用完整的类名。不管有多少模型类需要映射,都使用同样的方式配置在<list>标签下即可。

这样就已经把所有工作都完成了,现在只要进行任意一次数据库的操作,BookStore.db数据库应该就会自动创建出来。那么我们修改MainActivity中的代码,如下所示:

package com.zhouzhou.litepaltest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                LitePal.getDatabase();
            }
        });
    }
}

其中,调用LitePal.getDatabase()方法就是一次最简单的数据库操作,只要点击一下按钮,数据库就会自动创建完成了。只要点击一下按钮,数据库就会自动创建完成了。运行一下程序,然后点击Createdatabase按钮,接着通过adb shell查看一下数据库创建情况,如图:

image

非常棒!数据库文件已经创建成功了。接下来我们使用sqlite3命令打开BookStore.db文件,然后再使用.schema命令来查看建表语句,如图:

image

可以看到,这里有3张表的建表语句,其中android_metadata表仍然不用管,table_schema表是LitePal内部使用的,也可以直接忽视,book表就是根据我们定义的Book类以及类中的字段来自动生成的了。

虽说功能是实现了,但你有没有发现一个问题,就是升级数据库的时候我们需要先把之前的表drop掉,然后再重新创建才行。这其实是一个非常严重的问题,因为这样会造成数据丢失,每当升级一次数据库,之前表中的数据就全没了。

当然如果你是非常有经验的程序员,也可以通过复杂的逻辑控制来避免这种情况,但是维护成本很高。而有了LitePal,这些就都不再是问题了,使用LitePal来升级数据库非常非常简单,你完全不用思考任何的逻辑,只需要改你想改的任何内容,然后将版本号加1就行了。比如我们想要向Book表中添加一个press(出版社)列,直接修改Book类中的代码,添加一个press字段即可,如下所示:

package com.zhouzhou.litepaltest2;

public class Book {
    ...
    private String press;
    ...
    public String getPress() {
        return press;
    }

    public void setPress(String press) {
        this.press = press;
    }

与此同时,我们还想再添加一张Category表,那么只需要新建一个Category类就可以了,代码如下所示:


改完了所有我们想改的东西,只需要记得将版本号加1就行了。当然由于这里还添加了一个新的模型类,因此也需要将它添加到映射模型列表中。修改litepal.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <!--  数据库名称  -->
    <dbname value="BookStore" />

    <!--  版本号 增加了Category数据库,版本号由1改为2-->
    <version value="2" />

    <list>
        <!--   数据库表路径     -->
        <mapping class="com.zhouzhou.litepaltest2.Book"></mapping>
        <mapping class="com.zhouzhou.litepaltest2.Category"></mapping>
    </list>
</litepal>

现在重新运行一下程序,然后点击Create database按钮,再查看一下最新的建表语句,结果如图:

image

可以看到,book表中新增了一个press列,category表也创建成功了,当然LitePal还自动帮我们做了一项非常重要的工作,就是保留之前表中的所有数据,这样就再也不用担心数据丢失的问题了。

6.5.4 使用LitePal添加数据

首先回顾一下之前添加数据的方法,我们需要创建出一个ContentValues对象,然后将所有要添加的数据put到这个ContentValues对象当中,最后再调用SQLiteDatabase的insert()方法将数据添加到数据库表当中。

而使用LitePal来添加数据,这些操作可以简单到让你惊叹!只需要创建出模型类的实例,再将所有要存储的数据设置好,最后调用一下save()方法就可以了。

下面开始来动手实现,观察现有的模型类,你会发现它们都是没有继承结构的。没错,因为LitePal进行表管理操作时不需要模型类有任何的继承结构,但是进行CRUD操作时就不行了,必须要继承自LitePalSupport类(书中的DataSupport类已经被弃用)才行,因此这里我们需要先把继承结构给加上。修改Book类中的代码,如下所示:

package com.zhouzhou.litepaltest2;

import org.litepal.crud.LitePalSupport;

public class Book extends LitePalSupport {
    ...
}

接着我们开始向Book表中添加数据,修改MainActivity中的代码,如下所示:

package com.zhouzhou.litepaltest2;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button createDatabase = (Button) findViewById(R.id.create_data);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                LitePal.getDatabase();
            }
        });
        Button addData = (Button) findViewById(R.id.add_data);
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Book book = new Book();
                book.setName("周周的Android开发");
                book.setAuthor("周周");
                book.setPages(345);
                book.setPrice(17.79);
                book.setPress("UnKnow");
                book.save();
            }
        });
    }
}

在添加数据按钮的点击事件里面,首先是创建出了一个Book的实例,然后调用Book类中的各种set方法对数据进行设置,最后再调用book.save()方法就能完成数据添加操作了。

那么这个save()方法是从哪儿来的呢?当然是从LitePalSupport类中继承而来的了。除了save()方法之外,LitePalSupport类还给我们提供了丰富的CRUD方法,现在重新运行程序,点击一下Add data按钮,此时数据应该已经添加成功了,打开BookStore.db数据库,输入SQL查询语句select *fromBook,结果如图:

image

6.5.5 使用LitePal更新数据

使用LitePal更新数据。更新数据要比添加数据稍微复杂一点,因为它的API接口比较多,这里我们只介绍最常用的几种更新方式。

首先,最简单的一种更新方式就是对已存储的对象重新设值,然后重新调用save()方法即可。那么这里我们就要了解一个概念,什么是已存储的对象?对于LitePal来说,对象是否已存储就是根据调用model.isSaved()方法的结果来判断的,返回true就表示已存储,返回false就表示未存储。

那么接下来的问题就是,什么情况下会返回true,什么情况下会返回false呢?实际上只有在两种情况下model.isSaved()方法才会返回true,一种情况是已经调用过model.save()方法去添加数据了,此时model会被认为是已存储的对象。另一种情况是model对象是通过LitePal提供的查询API查出来的,由于是从数据库中查到的对象,因此也会被认为是已存储的对象。由于查询API我们暂时还没学到,因此只能先通过第一种情况来进行验证。修改MainActivity中的代码,如下所示:

package com.zhouzhou.litepaltest2;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        Button updateData = (Button) findViewById(R.id.update_data);
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Book book = new Book();
                book.setName("Java开发");
                book.setAuthor("王大舸");
                book.setPages(732);
                book.setPrice(37.9);
                book.setPress("UnKnow");
                book.save();
                book.setPrice(99.9);
                book.save();
            }
        });
    }
}

在更新数据按钮的点击事件里面,先是通过上一小节中学习的知识添加了一条Book数据,然后调用setPrice()方法将这本书的价格进行了修改,之后再次调用了save()方法。此时LitePal会发现当前的Book对象是已存储的,因此不会再向数据库中去添加一条新数据,而是会直接更新当前的数据。

现在重新运行一下程序,然后点击Update data按钮,我们再次输入查询语句查看表中的数据情况,结果如图:

image

可以看到,Book表中新增了一条书的数据,但这本书的价格并不是一开始设置的37.9,而是99.9,说明我们的更新操作确实生效了。但是这种更新方式只能对已存储的对象进行操作,限制性比较大,接下来我们学习另外一种更加灵巧的更新方式。修改MainActivity中的代码,如下所示:

package com.zhouzhou.litepaltest2;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        Button updateData = (Button) findViewById(R.id.update_data);
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Book book = new Book();
                book.setPrice(100);
                book.setPress("Anchor");
                book.updateAll("name = ? and author = ? ","Java开发","王大舸");
            }
        });
    }
}

可以看到,这里首先new出了一个Book的实例,然后直接调用setPrice()和setPress()方法来设置要更新的数据,最后再调用updateAll()方法去执行更新操作。

注意updateAll()方法中可以指定一个条件约束,和SQLiteDatabase中update()方法的where参数部分有点类似,但更加简洁,如果不指定条件语句的话,就表示更新所有数据。现在重新运行程序并点击Update data按钮,我们再次查询一下表中的数据情况,结果如图:

image

LitePal的更新API明显比SQLiteDatabase的update()方法要好用多。

不过,在使用updateAll()方法时,还有一个非常重要的知识点,就是当你想把一个字段的值更新成默认值时,是不可以使用上面的方式来set数据的。

我们都知道,在Java中任何一种数据类型的字段都会有默认值,例如int类型的默认值是0, boolean类型的默认值是false, String类型的默认值是null。那么当new出一个Book对象时,其实所有字段都已经被初识化成默认值了,比如说pages字段的值就是0。因此,如果我们想把数据库表中的pages列更新成0,直接调用book.setPages(0)是不可以的,因为即使不调用这行代码,pages字段本身也是0, LitePal此时是不会对这个列进行更新的。

对于所有想要将为数据更新成默认值的操作,LitePal统一提供了一个setToDefault()方法,然后传入相应的列名就可以实现了。比如我们可以这样写:

Book book = new Book();
book.setToDefault("pages");
book.updateAll();

这段代码的意思是,将所有书的页数都更新为0,因为updateAll()方法中没有指定约束条件,因此更新操作对所有数据都生效了。

6.5.6 使用LitePal删除数据

使用LitePal删除数据的方式主要有两种,第一种比较简单,就是直接调用已存储对象的delete()方法就可以了,对于已存储对象的概念,我们在上一小节中已经学习过了。也就是说,调用过save()方法的对象,或者是通过LitePal提供的查询API查出来的对象,都是可以直接使用delete()方法来删除数据的。这种方式比较简单,我们就不进行代码演示了,下面直接来看另外一种删除数据的方式。修改MainActivity中的代码,如下所示:

package com.zhouzhou.litepaltest2;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        Button deleteButton = (Button) findViewById(R.id.delete_data);
        deleteButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //LitePal.deleteAll(Book.class,"price < ? ","20")
                LitePal.delete(Book.class,2);
            }
        });
    }
}

这里调用了LitePal.delete()方法来删除数据,其中deleteAll()方法的第一个参数用于指定删除哪张表中的数据,Book.class就意味着删除Book表中的数据,后面的参数用于指定约束条件。现在重新运行程序,并点击一下Delete data按钮,然后查询表中的数据情况,如图:

image

另外,deleteAll()方法如果不指定约束条件,就意味着你要删除表中的所有数据,这一点和updateAll()方法是比较相似的。

6.5.7 使用LitePal查询数据

想想之前我们所使用的query()方法,冗长的参数列表让人看得头疼,即使多数参数都是用不到的,也不得不传入null,如下所示:

Cursor cursor = db.query("Book",null,null,null,null,null,null);

像这样的代码恐怕是没人会喜欢的。为此LitePal在查询API方面做了非常多的优化,基本上可以满足绝大多数场景的查询需求,并且代码十分整洁。

首先分析一下上述代码,query()方法中使用了第一个参数指明去查询Book表,后面的参数全部为null,这就表示希望查询这张表中的所有数据。那么使用LitePal如何完成同样的功能呢?非常简单,只需要这样写:

List<Book> books = LitePal.findAll(Book.class);

没有冗长的参数列表,只需要调用一下findAll()方法,然后通过Book.class参数指定查询Book表就可以。

另外,findAll()方法的返回值是一个Book类型的List集合,也就是说,我们不用像之前那样再通过Cursor对象一行行去取值了,LitePal已经自动帮我们完成了赋值操作。下面通过一个完整的例子来实践一下吧,修改MainActivity中的代码,如下所示:

package com.zhouzhou.litepaltest2;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

import java.util.List;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        Button queryButton = (Button) findViewById(R.id.show_data);
        queryButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                List<Book> books = LitePal.findAll(Book.class);
                for (Book book: books) {
                    Log.d("MainActivity","book name is " + book.getName());
                    Log.d("MainActivity","book author is " + book.getAuthor());
                    Log.d("MainActivity","book pages is " + book.getPages());
                    Log.d("MainActivity","book price is " + book.getPrice());
                    Log.d("MainActivity","book press is " + book.getPress());
                }
            }
        });
    }
}

现在重新运行一下程序,点击Querydata按钮,然后查看logcat的打印内容,结果如图:

image

Book表中只剩下一条数据,由此可见,我们已经将这条数据成功查询出来了。除了findAll()方法之外,LitePal还提供了很多其他非常有用的查询API。

比如想要查询Book表中的第一条数据就可以这样写:

Book firstBook = LitePal.findFirst(Book.class);

查询Book表中的最后一条数据就可以这样写:

Book lastBook = LitePal.findLast(Book.class);

我们还可以通过连缀查询来定制更多的查询功能。

  • select()方法用于指定查询哪几列的数据,对应了SQL当中的select关键字。比如只查name和author这两列的数据,就可以这样写:
List<Book> books = LitePal.select("name","author").find(Book.class);
  • where()方法用于指定查询的约束条件,对应了SQL当中的where关键字。比如只查页数大于400的数据,就可以这样写:
List<Book> books = LitePal.where("pages > ?","400").find(Book.class);
  • order()方法用于指定结果的排序方式,对应了SQL当中的order by关键字。比如将查询结果按照书价从高到低排序,就可以这样写:
List<Book> books = LitePal.order("price desc").find(Book.class);

其中desc表示降序排列,asc或者不写表示升序排列。

  • limit()方法用于指定查询结果的数量,比如只查表中的前3条数据,就可以这样写:
List<Book> books = LitePal.limit(3).find(Book.class);
  • offset()方法用于指定查询结果的偏移量,比如查询表中的第2条、第3条、第4条数据,就可以这样写:
List<Book> books = LitePal.limit(3).offset(1).find(Book.class);

由于limit(3)查询到的是前3条数据,这里我们再加上offset(1)进行一个位置的偏移,就能实现查询第2条、第3条、第4条数据的功能了。limit()和offset()方法共同对应了SQL当中的limit关键字。当然,你还可以对这5个方法进行任意的连缀组合,来完成一个比较复杂的查询操作:

List<Book> books = LitePal.select("name","author","pages")
    .where("pages > ?"."400")
    .order("pages")
    .limit(10)
    .offset(10)
    .find(Book.class);

这段代码就表示,查询Book表中第11~20条满足页数大于400这个条件的name、author和pages这3列数据,并将查询结果按照页数升序排列。

LitePal的查询功能非常强大,并且代码明显更加简洁。我们需要用到一个方法的时候直接连缀一下就可以了,不需要的话就可以不写,而不是像之前的query()方法,不管需不需要用到,都必须要传固定的参数进去才行。

当前,如果你实在有一些特殊需求,上述的API都满足不了你的时候,LitePal仍然支持使用原生的SQL来进行查询:

Cursor c = LitePal.findBySQL("select * from Book where pages > ? and price < ? ","400","20");

调用DataSupport.findBySQL()方法来进行原生查询,其中第一个参数用于指定SQL语句,后面的参数用于指定占位符的值。注意findBySQL()方法返回的是一个Cursor对象,接下来你还需要通过之前所学的老方式将数据一一取出才行。

6.6 小结与点评

本章主要是对Android常用的数据持久化方式进行了详细的讲解,包括文件存储、SharedPreferences存储以及数据库存储。其中文件适用于存储一些简单的文本数据或者二进制数据,SharedPreferences适用于存储一些键值对,而数据库则适用于存储那些复杂的关系型数据。虽然目前你已经掌握了这3种数据持久化方式的用法,但是能够根据项目的实际需求来选择最合适的方式也是你未来需要继续探索的。

posted @ 2022-05-11 11:45  江河湖泊  阅读(827)  评论(0编辑  收藏  举报