Android学习记录——6.持久化

1.持久化技术简介

数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或电脑关机的情况下,这些数据仍然不会丢失。保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的,持久化技术则提供了一种机制可以让数据在瞬时状态和持久状态之间进行转换。
持久化技术被广泛应用于各种程序设计的领域当中,而本篇博客中要探讨的自然是Android中的数据持久化技术。Android系统中主要提供了3种方式用于简单地实现数据持久化功能,即文件存储、SharedPreference存储以及数据库存储。当然,除了这3种方式之外,你还可以将数据保存在手机的SD卡中,不过使用文件、SharedPreference或数据库来保存数据会相对更简单一些, 而且比起将数据保存在SD卡中会更加地安全。
那么下面就将对这3种数据持久化的方式一一进行详细的讲解。

2.文件存储

文件存储是Android中最基本的一种数据存储方式,它不对存储的内容进行任何的格式化处理,所有数据都是原封不动地保存到文件当中的,因而它比较适合用于存储一些简单的文本数据或二进制数据。如果你想使用文件存储的方式来保存一些较为复杂的文本数据,就需要定义一套自己的格式规范,这样可以方便之后将数据从文件中重新解析岀来。
那么首先我们就来看一看,Android中是如何通过文件来保存数据的。

2.1 将数据存储到文件中

Context类中提供了一个openFileOutput()方法,可以用于将数据存储到指定的文件中。 这个方法接收两个参数,第一个参数是文件名,在文件创建的时候使用的就是这个名称,注意这里指定的文件名不可以包含路径,因为所有的文件都是默认存储到/data/data//files/目录下的。第二个参数是文件的操作模式,主要有两种模式可选,MODE_PRIVATE 和M0DE_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(new OutputStreamWriter(out)); 
        writer.write(data);
    } catch (IOException e) {
        e.printStackTrace();
    } finally { 
        try { 
            if (writer != null) { 
                writer.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
            }
        }
}

如果你已经比较熟悉Java流了,理解上面的代码一定轻而易举吧。这里通过openFileOutput()方法能够得到一个FileOutputStream对象,然后再借助它构建出一个OutputStreamWriter 对象,接着再使用 OutputStreamWriter 构建出一个 BufferedWriter 对象,这样你就可以通过BufferedWriter来将文本内容写入到文件中了。
下面我们就编写一个完整的例子,借此学习一下如何在Android项目中使用文件存储的技术。 首先创建一个新项目,并修改activity main.xml中的代油,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <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.mxt.filepersistencetest;

import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
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(intputText)
    }

    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。 save()方法中的代码和之前的示例基本相同,这里就不再做解释了。现在重新运行一下程序, 并在EditText中输入一些内容,如图所示:
在这里插入图片描述
打开文件资管管理器,如图所示:
在这里插入图片描述
进入/data/data/《你的项目名》/files,就可以看到生成了一个Data文件,如图所示:
在这里插入图片描述
这样就证实了,在EditText中输入的内容确实已经成功保存到文件中了。
不过只是成功将数据保存下来还不够,我们还需要想办法在下次启动程序的时候让这些数据能够还原到EditText中,因此接下来我们就要学习一下如何从文件中读取数据。

2.2 从文件中读取数据

类似于将数据存储到文件中,Context类中还提供了一个openFilelnput ()方法,用于从文件中读取数据。这个方法要比openFileOutput()简单一些,它只接收一个参数,即要读取的文件名,然后系统会自动到/data/data/vpackage name>/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.mxt.filepersistencetest;

import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
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();
        }
    }

    @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();
        }
    }
    }

    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();
    }
}

可以看到,这里的思路非常简单,在onCreate()方法中调用load()方法来读取文件中存储的文本内容,如果读到的内容不为null,就调用EditText的setText ()方法将内容填充到EditText 里,并调用setSelection()方法将输入光标移动到文本的末尾位置以便于继续输入,然后弹出一句还原成功的提示。load()方法中的细节我们在前面已经讲过,这里就不再赘述了。
注意,上述代码在对字符串进行非空判断的时候使用了 TextUtils.isEmpty()方法,这是一个非常好用的方法,它可以一次性进行两种空值的判断。当传入的字符串等于null或者等于空字符串的时候,这个方法都会返回true,从而使得我们不需要先单独判断这两种空值再使用逻辑运算符连接起来了。
现在重新运行一下程序,刚才保存的Content字符串肯定会被填充到EditText中,然后编写一点其他的内容,比如在EditText中输入Hello,接着按下Back键退出程序,再重新启动程序,这时刚才输入的内容并不会丢失,而是还原到了 EditText中,如图所示:
在这里插入图片描述
这样我们就已经把文件存储方面的知识学习完了,其实所用到的核心技术就是Context类中提供的openFilelnput()和openFileOutput()方法,之后就是利用Java的各种流来进行读写操作。
不过正如我前面所说,文件存储的方式并不适合用于保存一些较为复杂的文本数据,因此,下面我们就来学习一下Android中另一种数据持久化的方式,它比文件存储更加简单易用,而且可以很方便地对某一指定的数据进行读写操作。

3.SharedPreferences存储

不同于文件的存储方式,SharedPreferences是使用键值对的方式来存储数据的。也就是说,当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过这个键把相应的值取出来。而且SharedPreferences还支持多种不同的数据类型存储,如果存储的数据类型是整型,那么读取岀来的数据也是整型的;如果存储的数据是一个字符串,那么读取岀来的数据仍然是字符串。
这样你应该就能明显地感觉到,使用SharedPreferences来进行数据持久化要比使用文件方便很多,下面我们就来看一下它的具体用法吧。

3.1 将数据存储到SharedPreferences中

要想使用SharedPreferences来存储数据,首先要获取到SharePreferences。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模式是在Android 6.0版本中被废弃的。
  2. Activity 类中的 getPreferences()方法
    这个方法和Context中的getSharedPreferences()方法很相似,不过它只接收一个操作模式参数,因为使用这个方法时会自动将当前活动的类名作为SharedPreferences的文件名。
  3. PreferenceManager 类中的 getDefaultSharedPreferences()方法
    这是一个静态方法,它接收一个Context参数,并自动使用当前应用程序的包名作为前缀来命名SharedPreferences文件。
    得到了 SharedPreferences对象之后,就可以开始向SharedPreferences文件中存储数据了,主要可以分为3步实现。
    1. 调用 SharedPreferences 对象的edit()方法来获取一个 SharedPreferences. Editor 对象。
    2. 向SharedPreferences.Editor对象中添加数据,比如添加一个布尔型数据就使用 putBoolean()方法,添加一个字符串则使用putString()方法,以此类推。
    3. 调用apply()方法将添加的数据提交,从而完成数据存储操作。

不知不觉中已经将理论知识介绍得挺多了,那我们就赶快通过一个例子来体验一下 SharedPreferences 存储的用法吧。新建一个项目,然后修改 activity_ main.xml 中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/sava_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" 
        android:text="Sava data"/>

</LinearLayout>

这里我们不做任何复杂的功能,只是简单地放置了一个按钮,用于将一些数据存储到 SharedPreferences文件当中。然后修改MainActivity中的代码,如下所示:

package com.mxt.filepersistencetest;

import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
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 savaData = (Button)findViewById(R.id.sava_data);
        savaData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SharedPreferences.Editor editor = getSharedPreferences("data",MODE_PRIVATE).edit();
                editor.putString("name","mxt");
                editor.putInt("age",21);
                editor.putBoolean("married",false);
                editor.apply();
            }
        });
    }
    
}

可以看到,这里首先给按钮注册了一个点击事件,然后在点击事件中通过getSharedPreferences ()方法指定 SharedPreferences 的文件名为 data,并得到了 SharedPreferences.Editor对象。接着向这个对象中添加了3条不同类型的数据,最后调用apply ()方法进行提交, 从而完成了数据存储的操作。
很简单吧?现在就可以运行一下程序了,进入程序的主界面后,点击一下Save data按钮。 这时的数据应该已经保存成功了,不过为了证实一下,我们还是要借助File Explorer来进行查看。 打开 Android Device Monitor,并点击 File Explorer 标签页,然后进入到/data/data/《你的项目名》/sharedprefs/ 目录下,可以看到生成 了一个 data.xml 文件。
可以看到,我们刚刚在按钮的点击事件中添加的所有数据都已经成功保存下来了,并且 SharedPreferences文件是使用XML格式来对数据进行管理的。
那么接下来我们自然要看一看,如何从SharedPreferences文件中去读取这些数据了。

3.2 从SharedPreferences中读取数据

你应该已经感觉到了,使用SharedPreferences来存储数据是非常简单的,不过下面还有更好的消息,其实从SharedPreferences文件中读取数据会更加地简单。SharedPreferences对象中提供了一系列的get方法,用于对存储的数据进行读取,每种get方法都对应了 SharedPreferences .Editor中的一种put方法,比如读取一个布尔型数据就使用getBoolean ()方法, 读取一个字符串就使用getString()方法。这些get方法都接收两个参数,第一个参数是键, 传入存储数据时使用的键就可以得到相应的值了;第二个参数是默认值,即表示当传入的键找不 到对应的值时会以什么样的默认值进行返回。
我们还是通过例子来实际体验一下吧,仍然是在之前项目的基础上继续开发,修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <Button
        android:id="@+id/sava_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Sava data"/>

    <Button
        android:id="@+id/restore_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Restore data"/>

</LinearLayout>

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

package com.mxt.filepersistencetest;

import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
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 savaData = (Button)findViewById(R.id.sava_data);
        Button restoreData = (Button)findViewById(R.id.restore_data);
        savaData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SharedPreferences.Editor editor = getSharedPreferences("data",MODE_PRIVATE).edit();
                editor.putString("name","mxt");
                editor.putInt("age",21);
                editor.putBoolean("married",false);
                editor.apply();
            }
        });
        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(), getlnt ()和 getBoolean() 方法,去获取前面所存储的姓名、年龄和是否已婚,如果没有找到相应的值,就会使用方法中传 入的默认值来代替,最后通过Log将这些值打印出来。
现在重新运行一下程序,并点击界面上的Restore data按钮,然后查看logcat中的打印信息,,如图所示:
在这里插入图片描述
所有之前存储的数据都成功读取出来了!通过这个例子,我们就把SharedPreferences存储的知识也学习完了。相比之下,SharedPreferences存储确实要比文本存储简单方便了许多,应用场景也多了不少,比如很多应用程序中的偏好设置功能其实都使用到了 SharedPreferences技术。 那么下面我们就来编写一个记住密码的功能,相信通过这个例子能够加深你对SharedPreferences 的理解。

3.3 实现记住密码功能

既然是实现记住密码的功能,那么我们就不需要从头去写了,因为在上一篇博客中的最佳实践部分已经编写过一个登录界面了,有可以重用的代码为什么不用呢?那就首先打开之前的项目,来编辑一下登录界面的布局。修改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="60dp">
        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="账号:"/>
        <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="密码:"/>
        <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="记住密码"/>
    </LinearLayout>
    <Button
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:text="登录"/>
</LinearLayout>

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

package com.mxt.broadtest;

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;

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 v){
                String account = accountEdit.getText().toString();
                String password = passwordEdit.getText().toString();
                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, "账号或者密码有错误!", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }
}

可以看到,这里首先在onCreate()方法中获取到了 SharedPreferences对象,然后调用 它的getBoolean()方法去获取remember password这个键对应的值。一开始当然不存在对应的值了,所以会使用默认值false,这样就什么都不会发生。接着在登录成功之后,会调用 CheckBox的isChecked()方法来检查复选框是否被选中,如果被选中了,则表示用户想要记住 密码,这时将remember password设置为true,然后把account和password对应的值都存 入到SharedPreferences文件当中并提交。如果没有被选中,就简单地调用一下clear()方法,将 SharedPreferences文件中的数据全部清除掉。
当用户选中了记住密码复选框,并成功登录一次之后,remember_password键对应的值就是true 了,这个时候如果再重新启动登录界面,就会从SharedPreferences文件中将保存的账号 和密码都读取出来,并填充到文本输入框中,然后把记住密码复选框选中,这样就完成记住密码 的功能了。
现在重新运行一下程序,效果如图所示:
在这里插入图片描述
这样我们就使用SharedPreferences技术将记住密码功能成功实现了,你是不是对SharedPreferences理解得更加深刻了呢?
不过需要注意,这里实现的记住密码功能仍然只是个简单的示例,并不能在实际的项目中直接使用。因为将密码以明文的形式存储在SharedPreferences文件中是非常不安全的,很容易就会被别人盗取,因此在正式的项目里还需要结合一定的加密算法来对密码进行保护才行。
好了,关于SharedPreferences的内容就讲到这里,接下来我们要学习一下本篇博客的重头戏——Android中的数据库技术。

4.SQLite数据库存储

SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资 源很少,通常只需要几百KB的内存就足够了,因而特别适合在移动设备上使用。SQLite不仅支 持标准的SQL语法,还遵循了数据库的ACID事务,所以只要你以前使用过其他的关系型数据 库,就可以很快地上手SQLite。而SQLite又比一般的数据库要简单得多,它甚至不用设置用户 名和密码就可以使用。Android正是把这个功能极为强大的数据库嵌入到了系统当中,使得本地 持久化的功能有了一次质的飞跃。
前面我们所学的文件存储和SharedPreferences存储毕竟只适用于保存一些简单的数据和键值对,当需要存储大量复杂的关系型数据的时候,你就会发现以上两种存储方式很难应付得了。比如我们手机的短信程序中可能会有很多个会话,每个会话中又包含了很多条信息内容,并且大部分会话还可能各自对应了电话簿中的某个联系人。很难想象如何用文件或者SharedPreferences来存储这些数据量大、结构性复杂的数据吧?但是使用数据库就可以做得到。那么我们就赶快来看一看,Android中的SQLite数据库到底是如何使用的。

4.1 创建数据库

Android为了让我们能够更加方便地管理数据库,专门提供了一个SQLiteOpenHelper帮助类,,借助这个类就可以非常简单地对数据库进行创建和升级。既然有好东西可以直接使用,那我们自然要尝试一下了,下面我就对SQLiteOpenHelper的基本用法进行介绍。
首先你要知道SQLiteOpenHelper是一个抽象类,这意味着如果我们想要使用它的话,就需要创建一个自己的帮助类去继承它。SQLiteOpenHelper中有两个抽象方法,分别是onCreate()和onUpgrade(),我们必须在自己的帮助类里面重写这两个方法,然后分别在这两个方法中去实现创建、升级数据库的逻辑。
SQLiteOpenHelper中还有两个非常重要的实例方法:getReadableDatabase()和getWritableDatabase()。这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。不同的是, 当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase()方法返回的对象将以只读的方式去打开数据库,而getWritableDatabase()方法则将出现异常。
SQLiteOpenHelper中有两个构造方法可供重写,一般使用参数少一点的那个构造方法即可。 这个构造方法中接收4个参数,第一个参数是Context,这个没什么好说的,必须要有它才能对数据库进行操作。第二个参数是数据库名,创建数据库时使用的就是这里指定的名称。第三个参数允许我们在查询数据的时候返回一个自定义的Cursor,,一般都是传入null。第四个参数表示当前数据库的版本号,可用于对数据库进行升级操作。构建出SQLiteOpenHelper的实例之后, 再调用它的getReadableDatabase()或getWritableDatabase()方法就能够创建数据库了, 数据库文件会存放在/data/data//databases/目录下。此时,重写的onCreate()方法 也会得到执行,所以通常会在这里去处理一些创建表的逻辑。
接下来还是让我们通过例子的方式来更加直观地体会SQLiteOpenHelper的用法吧,首先 建一个 项目。
这里我们希望创建一个名为BookStore.db的数据库,然后在这个数据库中新建一张Book表, 表中有id (主键)、作者、价格、页数和书名等列。创建数据库表当然还是需要用建表语句的, 这里也是要考验一下你的SQL基本功了,Book表的建表语句如下所示:

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

只要你对SQL方面的知识稍微有一些了解,上面的建表语句对你来说应该都不难吧。SQLite 不像其他的数据库拥有众多繁杂的数据类型,它的数据类型很简单,integer表示整型,real 表示浮点型,text表示文本类型,blob表示二进制类型。另外,上述建表语句中我们还使用了 primary key将id列设为主键,并用autoincrement关键字表示id列是自增长的。
然后需要在代码中去执行这条SQL语句,才能完成创建表的操作。新建MyDatabaseHelper 类继承自SQLiteOpenHelper,代码如下所示:

package com.mxt.filepersistencetest;

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

/**
 * Created by Administrator on 2018-10-02.
 */

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(Context context, String name, SQLiteDatabase.CursorFactory factory, int version){
        super(context,name,factory,version);
        mContext = context;
    }
    @Override
    public void onCreate(SQLiteDatabase db){
        db.execSQL(CREATE_BOOK);
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
    }
}

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

package com.mxt.filepersistencetest;

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

/**
 * Created by Administrator on 2018-10-02.
 */

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(Context context, String name, SQLiteDatabase.CursorFactory factory, int version){
        super(context,name,factory,version);
        mContext = context;
    }
    @Override
    public void onCreate(SQLiteDatabase db){
        db.execSQL(CREATE_BOOK);
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, 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"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <Button
        android:id="@+id/create_database"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Sava data"/>

</LinearLayout>

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

package com.mxt.filepersistencetest;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
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);
        Button createDatabase = (Button)findViewById(R.id.create_database);
        dbHelper = new MyDatabaseHelper(this,"BookStore.db",null,1);
        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表也就得到了创建,再次点 击Create database按钮时,会发现此时已经存在BookStore.db数据库了,因此不会再创建一次。
现在就可以运行一下代码了,在程序主界面点击Create database按钮,结果如图所示:
在这里插入图片描述
此时BookStore.db数据库和Book表应该都已经创建成功了,。可是又回到了之前的那个老问题,怎样才能证实它们的确创建成功了?如果还是使用File Explorer,那么最多你只能看到databases目录下出现了一个 ookStore.db文件,Book表是无法通过File Explorer看到的。因此这次我们准备换一种查看方式, 使用adb shell来对数据库和表的创建情况进行检查。
详细过程可以参考《第一行代码》原书,这里不再详述,输入相关命令后,得到如下结果:
在这里插入图片描述
由此证明,BookStore.db数据库和Book表确实已经创建成功了。之后键入.exit或.quit 命令可以退出数据库的编辑,再键入exit命令就可以退出设备控制台了。

4.2 升级数据库

如果你足够细心,一定会发现MyDatabaseHelper中还有一个空方法呢!没错,onUpgrade() 方法是用于对数据库进行升级的,它在整个数据库的管理工作当中起着非常重要的作用,可千万不能忽视它哟。
目前项目中已经有一张Book表用于存放书的各种详细数据,如果我们想再添加一张Category表用于记录图书的分类,该怎么做呢?
比如Category表中有id (主键)、分类名和分类代码这几个列,那么建表语句就可以写成:

create table Category (
id integer primary key autoincrement,
categoryname text,
categorycode integer)

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

package com.mxt.filepersistencetest;

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

/**
 * Created by Administrator on 2018-10-02.
 */

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(Context context, String name, SQLiteDatabase.CursorFactory factory, int version){
        super(context,name,factory,version);
        mContext = context;
    }
    @Override
    public void onCreate(SQLiteDatabase db){
        db.execSQL(CREATE_BOOK);
        db.execSQL(CREATE_CATEGORY);
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
    }
}

看上去好像都挺对的吧?现在我们重新运行一下程序,并点击Create database按钮,咦?竟然没有弹岀创建成功的提示。当然,你也可以通过adb工具到数据库中再去检查一下,这样你会更加地确认Category表没有创建成功!
其实没有创建成功的原因不难思考,因为此时BookStore.db数据库已经存在了,之后不管我们怎样点击Create database按钮,MyDatabaseHelper中的onCreate()方法都不会再次执行,因 此新添加的表也就无法得到创建了。
解决这个问题的办法也相当简单,只需要先将程序卸载掉,然后重新运行,这时BookStore.db 数据库已经不存在了,如果再点击Create database按钮,MyDatabaseHelper中的onCreate()方 法就会执行,这时Category表就可以创建成功了。
不过,通过卸载程序的方式来新增一张表毫无疑问是很极端的做法,其实我们只需要巧妙地 运用SQLiteOpenHelper的升级功能就可以很轻松地解决这个问题。修改MyDatabaseHelper中的代码,如下所示:

package com.mxt.filepersistencetest;

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

/**
 * Created by Administrator on 2018-10-02.
 */

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(Context context, String name, SQLiteDatabase.CursorFactory factory, int version){
        super(context,name,factory,version);
        mContext = context;
    }
    @Override
    public void onCreate(SQLiteDatabase db){
        db.execSQL(CREATE_BOOK);
        db.execSQL(CREATE_CATEGORY);
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
        db.execSQL("drop table if exists Book");
        db.execSQL("drop table if exists Category");
        onCreate(db);
    }
}

可以看到,我们在onUpgrade()方法中执行了两条DROP语句,如果发现数据库中已经存在Book表或Category表了,就将这两张表删除掉,然后再调用onCreate()方法重新创建。这里先将已经存在的表删除掉,因为如果在创建表时发现这张表已经存在了,就会直接报错。
接下来的问题就是如何让onUpgrade()方法能够执行了,还记得SQLiteOpenHelper的构造方法里接收的第四个参数吗?它表示当前数据库的版本号,之前我们传入的是1,现在只要传入一个比1大的数,就可以让onllpgrade()方法得到执行了。修改MainActivity中的代码,如下所示:

package com.mxt.filepersistencetest;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
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);
        Button createDatabase = (Button)findViewById(R.id.create_database);
        dbHelper = new MyDatabaseHelper(this,"BookStore.db",null,2);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dbHelper.getWritableDatabase();
            }
        });
    }

}

这里将数据库版本号指定为2,表示我们对数据库进行升级了。现在重新运行程序,并点击 Create database按钮,这时就会再次弹出创建成功的提示。为了验证一下Category表是不是已经 创建成功了,我们在adb shell中打开BookStore.db数据库,然后键入.table命令,结果如图所示:
在这里插入图片描述
由此可以看出,Category表已经创建成功了,同时也说明我们的升级功能的确起到了作用。

4.3 添加数据

现在你已经掌握了创建和升级数据库的方法,接下来就该学习一下如何对表中的数据进行操作了。其实我们可以对数据进行的操作无非有4种,即CRUD。其中C代表添加(Create),,R代表查询(Retrieve),,U代表更新(Update ),D代表删除(Delete)。每一种操作又各自对应了一 种SQL命令,如果你比较熟悉SQL语言的话,一定会知道添加数据时使用insert,查询数据时使用select,更新数据时使用update,删除数据时使用delete。但是开发者的水平总会是参差不齐的,未必每一个人都能非常熟悉地使用SQL语言,因此Android也提供了一系列的辅助性方法,使得在Android中即使不去编写SQL语句,也能轻松完成所有的CRUD操作。
前面我们已经知道,调用 SQLiteOpenHclper 的 getReadableDatabase()或 getWritableDatabaseO方法是可以用于创建和升级数据库的,不仅如此,这两个方法还都会返回一个 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"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <Button
        android:id="@+id/create_database"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Sava data"/>

    <Button
        android:id="@+id/add_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Add data"/>

</LinearLayout>

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

package com.mxt.filepersistencetest;

import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
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);
        Button createDatabase = (Button)findViewById(R.id.create_database);
        Button addData = (Button)findViewById(R.id.add_data);
        dbHelper = new MyDatabaseHelper(this,"BookStore.db",null,2);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dbHelper.getWritableDatabase();
            }
        });
        addData.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()方法。
好了,现在可以重新运行一下程序了,界面如图所示:
在这里插入图片描述
点击一下Add data按钮,此时两条数据应该都已经添加成功了,不过为了证实一下,我们还是 打开BookStore.db数据库瞧一瞧。输入SQL查询语句select * from Book,结果如图所示:
在这里插入图片描述
由此可以看出,我们刚刚组装的两条数据都已经准确无误地添加到Book表中了。

4.4 更新数据

学习完了如何向表中添加数据,接下来我们看看怎样才能修改表中已有的数据。SQLiteDatabase中也提供了一个非常好用的update()方法,用于对数据进行更新,这个方法接收4个参数,第一个参数和insert()方法一样,也是表名,在这里指定去更新哪张表里的数据。第二个参数是Contentvalues对象,要把更新数据在这里组装进去。第三、第四个参数用于约束更新某一行或某几行中的数据,不指定的话默认就是更新所有行。
那么接下来我们仍然是在原t项目的基础上修改,看一下更新数据的具体用法。比如说刚才添加到数据库里的第一本书,由于过了畅销季,卖得不是很火了,现在需要通过降低价格的方式来吸引更多的顾客,我们应该怎么操作呢?首先修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <Button
        android:id="@+id/create_database"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Sava data"/>

    <Button
        android:id="@+id/add_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Add data"/>

    <Button
        android:id="@+id/update_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Update data"/>

</LinearLayout>

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

package com.mxt.filepersistencetest;

import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
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);
        Button createDatabase = (Button)findViewById(R.id.create_database);
        Button addData = (Button)findViewById(R.id.add_data);
        Button updateData = (Button)findViewById(R.id.update_data);
        dbHelper = new MyDatabaseHelper(this,"BookStore.db",null,2);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dbHelper.getWritableDatabase();
            }
        });
        addData.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);
            }
        });
        updateData.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v){
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("price",10.99);
                db.update("Book",values,"name = ?",new String[]{"The Da Vinci Code"});
            }
        } );
    }

}

这里在更新数据按钮的点击事件里面构建了一个Contentvalues对象,并且只给它指定了 一组数据,说明我们只是想把价格这一列的数据更新成10.99。然后调用了 SQLiteDatabase的 update()方法去执行具体的更新操作,可以看到,这里使用了第三、第四个参数来指定具体更新哪几行。第三个参数对应的是SQL语句的where部分,表示更新所有name等于?的行,而? 是一个占位符,可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应的内容。因此上述代码想表达的意图是将名字是The Da Vinci Code的这本书的价格改成10.99。
现在重新运行一下程序,界面如图所示:
在这里插入图片描述
点击一下Update data按钮后,再次输入查询语句查看表中的数据情况,结果如图所示:
在这里插入图片描述
可以看到,The DaVinci Code这本书的价格已经被成功改为10.99了。

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"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <Button
        android:id="@+id/create_database"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Sava data"/>

    <Button
        android:id="@+id/add_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Add data"/>

    <Button
        android:id="@+id/update_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Update data"/>

    <Button
        android:id="@+id/delete_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Delete data"/>

</LinearLayout>

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

package com.mxt.filepersistencetest;

import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
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);
        Button createDatabase = (Button)findViewById(R.id.create_database);
        Button addData = (Button)findViewById(R.id.add_data);
        Button updateData = (Button)findViewById(R.id.update_data);
        Button deleteData = (Button)findViewById(R.id.delete_data); 
        dbHelper = new MyDatabaseHelper(this,"BookStore.db",null,2);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dbHelper.getWritableDatabase();
            }
        });
        addData.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);
            }
        });
        updateData.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v){
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("price",10.99);
                db.update("Book",values,"name = ?",new String[]{"The Da Vinci Code"});
            }
        } );
        deleteData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                db.delete("Book","pages > ?",new String[]{"500"});
            }
        });
    }

}

可以看到,我们在删除按钮的点击事件里指明去删除Book表中的数据,并且通过第二、第三个参数来指定仅删除那些页数超过500页的书。当然这个需求很奇怪,这里也仅仅是为了做个测试。你可以先查看一下当前Book表里的数据,其中The Lost Symbol这本书的页数超过了 500 页,也就是说当我们点击删除按钮时,这条记录应该会被删除掉。
现在重新运行一下程序,界面如图所示:
在这里插入图片描述
点击一下Delete data按钮后,再次输入查询语句查看表中的数据情况,结果如图所示:
在这里插入图片描述

4.6 查询数据

终于到了最后一种操作了,掌握了查询数据的方法之后,你就将数据库的CRUD操作全部学完了。不过千万不要因此而放松,因为查询数据是CRUD中最复杂的一种操作。
我们都知道SQL的全称是Structured Query Language,翻译成中文就是结构化查询语言。它的大部功能都体现在“查”这个字上的,而“增删改”只是其中的一小部分功能。由于SQL查询涉及的内容实在是太多了,因此在这里我不准备对它展开来讲解,而是只会介绍Android上的查询功能。如果你对SQL语言非常感兴趣,可以找一本专门介绍SQL的书进行学习。
相信你已经猜到了,SQLiteDatabase中还提供了一个query()方法用于对数据进行查询。这个方法的参数非常复杂,最短的一个方法重载也需要传入7个参数。那我们就先来看一下这7个参数各自的含义吧。第一个参数不用说,当然还是表名,表示我们希望从哪张表中查询数据。第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列。第三、第四个参数用于约束查询某一行或某几行的数据,不指定则默认查询所有行的数据。第五个参数用于指定需要去group by 的列,不指定则表示不对查询结果进行group by操作。第六个参数用于对group by之后的数据进行进一步的过滤,不指定则表示不进行过滤。第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。更多详细的内容可以参考下表。其他几个query()方法的重载其实 也大同小异,你可以自己去研究一下,这里就不再进行介绍了。

在这里插入图片描述

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

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <Button
        android:id="@+id/create_database"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Sava data"/>

    <Button
        android:id="@+id/add_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Add data"/>

    <Button
        android:id="@+id/update_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Update data"/>

    <Button
        android:id="@+id/delete_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Delete data"/>

    <Button
        android:id="@+id/query_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Query data"/>

</LinearLayout>

这个已经没什么好说的了,添加了一个按钮用于查询数据。然后修改MainActivity中的代码,如下所示:

package com.mxt.filepersistencetest;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
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);
        Button createDatabase = (Button)findViewById(R.id.create_database);
        Button addData = (Button)findViewById(R.id.add_data);
        Button updateData = (Button)findViewById(R.id.update_data);
        Button deleteData = (Button)findViewById(R.id.delete_data);
        Button queryData = (Button)findViewById(R.id.query_data); 
        dbHelper = new MyDatabaseHelper(this,"BookStore.db",null,2);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dbHelper.getWritableDatabase();
            }
        });
        addData.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);
            }
        });
        updateData.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v){
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("price",10.99);
                db.update("Book",values,"name = ?",new String[]{"The Da Vinci Code"});
            }
        } );
        deleteData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                db.delete("Book","pages > ?",new String[]{"500"});
            }
        });
        queryData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                Cursor cursor = db.query("Book",null,null,null,null,null,null);
                if(cursor.moveToFirst()){
                    do{
                        String name = cursor.getString(cursor.getColumnIndex("name"));
                        String author = cursor.getString(cursor.getColumnIndex("author"));
                        int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                        double price = cursor.getDouble(cursor.getColumnIndex("price"));
                        Log.d("MainActivity","书名是" + name);
                        Log.d("MainActivity","书的作者是" + author);
                        Log.d("MainActivity","书的页数是" + pages);
                        Log.d("MainActivity","书的价格是" + price);
                    }while(cursor.moveToNext());
                }
                cursor.close();
            }
        });
    }

}

可以看到,我们首先在查询按钮的点击事件里面调用了 SQLiteDatabase的query ()方法去查 询数据。这里的query ()方法非常简单,只是使用了第一个参数指明去查询Book表,后面的参数全部为null。这就表示希望查询这张表中的所有数据,虽然这张表中目前只剩下一条数据了。 查询完之后就得到了一个Cursor对象,接着我们调用它的moveToFirst ()方法将数据的指针移 动到第一行的位置,然后进入了一个循环当中,去遍历查询到的每一行数据。在这个循环中可以 通过Cursor的getCoIumnlndex()方法获取到某一列在表中对应的位置索引,然后将这个索引 传入到相应的取值方法中,就可以得到从数据库中读取到的数据了。接着我们使用Log的方式将取岀的数据打印岀来,借此来检查一下读取工作有没有成功完成。最后别忘了调用close()方法 来关闭Cursoro
好了,现在再次重新运行程序,界面如图所示:
在这里插入图片描述
点击一下Query data按钮后,查看logcat的打印内容,结果如图所示。
在这里插入图片描述
可以看到,这里已经将Book表中唯一的一条数据成功地读取出来了。
当然这个例子只是对查询数据的用法进行了最简单的示范,在真正的项目中你可能会遇到比这要复杂得多的查询功能,更多高级的用法还需要你自己去慢慢摸索,毕竟query()方法中还有 那么多的参数我们都还没用到呢。

4.7 使用SQL操作数据库

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

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

可以看到,除了查询数据的时候调用的是SQLiteDatabase的rawQuery()方法,其他的操作都是调用的execSQL()方法。以上演示的几种方式,执行结果会和前面几小节中我们学习的 CRUD操作的结果完全相同,选择使用哪一种方式就看你个人的喜好了。

5.使用LitePal操作数据库

上一节中我们学习了使用SQLiteDatabase来操作SQLite数据库的方法,你觉得好用吗?每 个人的回答可能会不一样。但我相信,等学完了本节的内容之后,你将再也不想去碰 SQLiteDatabase了。到底是什么东西这么神奇?新建一个项目,然后开始我们本节的学习之旅吧。

5.1 LitePal简介

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

5.2 配置LitePal

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

compile 'org.litepal.android:core:1.4.1'

添加的这一行声明中,前面部分是固定的,最后的1.4.1是版本号的意思,最新的版本号可以到LitePal的项目主页上去查看。
这样我们就把LitePal成功引入到当前项目中了,接下来需要配置litepaLxml文件。右击 app/src/main目录—>New—>Directory,创建一个assets目录,然后在assets目录下再新建一个litepal.xml文件,接着编辑litepal.xml文件中的内容,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value="BookStore"></dbname>
    
    <version value="1"></version>
    
    <list>
    </list>
</litepal>

其中,<dbname>标签用于指定数据库名,<version>标签用于指定数据库版本号,<list> 标签用于指定所有的映射模型,我们稍后就会用到。
最后还需要再配置一下LitePalApplication,修改AndroidManifest.xml中的代码,如下所示:

<application
    android:name="org.litepal.LitePalApplication"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

这里我们将项目的application配置为org.litepal.LitePalApplication,这样才能 让LitePal的所有功能都可以正常工作。关于application的作用,我们之前并没有进行过详细的讲解,现在你只需要知道必须这么写就行了,我们将会在之后学习application的更多内容。
现在LitePal的配置工作已经全部结束了,下面我们开始正式使用它吧。

5.3 创建和升级数据库

我们之前创建数据库是通过自定义一个类继承自SQLiteOpenHelper,然后在onCreate()方 法中编写建表语句来实现的,而使用LitePal就不用再这么麻烦了。本节中我们会使用LitePal来逐一完成上一节中所学的所有功能,以此来对比它们之间的差距,那么为了方便测试,我们先将 activity_main.xml布局文件从DatabaseTest项目复制到LitePalTest项目中来。
刚才在介绍的时候已经说过,LitePal采取的是对象关系映射(ORM)的模式,那么什么是对象关系映射呢?简单点说,我们使用的编程语言是面向对象语言,而使用的数据库则是关系型数据库,那么将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是对象关系映射了。
不过你可千万不要小看对象关系映射模式,它赋予了我们一个强大的功能,就是可以用面向对象的思维来操作数据库,而不用再和SQL语句打交道了,不信的话我们现在就来体验一下。 比如在4.1小节中,为了创建一张Book需要先分析表中应该包含哪些列,然后再编写出一条建表语句,最后在自定义的SQLiteOpenHelper中去执行这条建表语句。但是使用LitePal,你就可以用面向对象的思维来实现同样的功能了,定义一个Book类,代码如下所示:

package com.mxt.litepaltest;

import org.litepal.crud.DataSupport;

/**
 * Created by Administrator on 2018-10-03.
 */

public class Book extends DataSupport {
    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"></dbname>

    <version value="1"></version>

    <list>
        <mapping class="com.mxt.litepaltest.Book"/>
    </list>
</litepal>

这里使用标签来声明我们要配置的映射模型类,注意一定要使用完整的类名。不管有多少模型类需要映射,都使用同样的方式配置在标签下即可。
没错,这样就已经把所有工作都完成了,现在只要进行任意一次数据库的操作,BookStore.db 数据库应该就会自动创建出来。那么我们修改MainActivity中的代码,如下所示:

package com.mxt.litepaltest;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
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();
            }
        });
    }
}

其中,调用Connector.getDatabase()方法就是一次最简单的数据库操作,只要点击一下按钮,数据库就会自动创建完成了。运行一下程序,然后点击Create database按钮,接着通过adb shell查看一下数据库创建情况,如图所示:
在这里插入图片描述
非常棒!数据库文件已经创建成功了。接下来我们使用sqlite3命令打开BookStore.db文 件,然后再使用.schema命令来查看建表语句,如图所示:
在这里插入图片描述
怎么样,是不是很神奇?但不用太吃惊,因为更加神奇的还在后面呢。4.2节中我们体验了使用SQLiteOpenHelper来升级数据库的方式,虽说功能是实现了,但你有没有发现一个问题, 就是升级数据库的时候我们需要先把之前的表drop掉,然后再重新创建才行。这其实是一个非常严重的问题,因为这样会造成数据丢失,每当升级一次数据库,之前表中的数据就全没了。
当然如果你是非常有经验的程序员,也可以通过复杂的逻辑控制来避免这种情况,但是维护成本很高。而有了 LitePal,这些就都不再是问题了,使用LitePal来升级数据库非常非常简单, 你完全不用思考任何的逻辑,只需要改你想改的任何内容,然后将版本号加1就行了。
比如我们想要向Book表中添加一个press (出版社)列,直接修改Book类中的代码,添加一个press字段即可,如下所示:

package com.mxt.litepaltest;

import org.litepal.crud.DataSupport;

/**
 * Created by Administrator on 2018-10-03.
 */

public class Book extends DataSupport {
    private int id;
    private String author;
    private double price;
    private int pages;
    private String name;
    private String press;
    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;
    }

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

    public String getPress() {
        return press;
    }

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

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

package com.mxt.litepaltest;

/**
 * Created by Administrator on 2018-10-04.
 */

public class Category {
    private int id;
    private String categoryName;
    private int categoryCode;
    public int getId(){
        return id;
    }
    public void setId(int id){
        this.id = id;
    }

    public String getCategoryName(){
        return categoryName;
    }
    public void setCategoryName(String categoryName){
        this.categoryName = categoryName;
    }

    public int getCategoryCode(){
        return categoryCode;
    }
    public void setCategoryCode(int categoryCode){
        this.categoryCode = categoryCode;
    }
}

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

<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value="BookStore"></dbname>

    <version value="2"></version>

    <list>
        <mapping class="com.mxt.litepaltest.Book"/>
        <mapping class="com.mxt.litepaltest.Category"/>
    </list>
</litepal>

现在重新运行一下程序,然后点击Create database按钮,再查看一下最新的建表语句,结果如图所示:
在这里插入图片描述
可以看到,book表中新增了一个press列,category表也创建成功了,当然LitePal还自动帮我们做了一项非常重要的工作,就是保留之前表中的所有数据,这样就再也不用担心数据丢失的问题了。

5.4 使用LitePal添加数据

体验了使用LitePal来创建和升级数据库,是不是感觉已经有一些小震撼了呢?不过LitePal所提供的强大功能还远不止于此,接下来我们就学习一下如何使用它来向数据库的表中添加数据吧。
首先回顾一下之前添加数据的方法,我们需要创建出一个Contentvalues对象,然后将所有要添加的数据put到这个Contentvalues对象当中,最后再调用SQLiteDatabase的insert() 方法将数据添加到数据库表当中。
而使用LitePal来添加数据,这些操作可以简单到让你惊叹!我们只需要创建出模型类的实例,再将所有要存储的数据设置好,最后调用一下save()方法就可以了。
下面开始来动手实现,观察现有的模型类,你会发现它们都是没有继承结构的。没错,因为LitePal进行表管理操作时不需要模型类有任何的继承结构,但是进行CRUD操作时就不行了,必须要继承自DataSupport类才行,因此这里我们需要先把继承结构给加上。修改Book类中的代码,如下所示:

public class Book extends DataSupport {
    ...
}

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

package com.mxt.litepaltest;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
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);
        Button addData = (Button)findViewById(R.id.add_data); 
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
               LitePal.getDatabase();
            }
        });
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setName("The Da Vinci Code");
                book.setauthor("Dan Brown");
                book.setPages(454);
                book.setPrice(16.96);
                book.setPress("UnKnow");
                book.save();
            }
        });
    }
}

这段代码非常神奇,我们来仔细阅读一下。在添加数据按钮的点击事件里面,首先是创建出了一个Book的实例,然后调用Book类中的各种set方法对数据进行设置,最后再调用 book.save()方法就能完成数据添加操作了。那么这个save()方法是从哪儿来的呢?当然是从DataSupport类中继承而来的了。除了 save()方法之外,DataSupport类还给我们提供了丰富的CRUD方法,这些我们在后面都会学到。
现在重新行程序,点击一下Add data按钮,此时数据应该已经添加成功了,我们打开BookStore.db数据库瞧一瞧。输入SQL查询语句select * from Book,结果如图所示:
在这里插入图片描述
可以看到,作者、书名、页数、价格、出版社,这些数据全部精确无误地添加成功了。

5.5 使用LitePal更新数据

学习完了如何使用LitePal添加数据,接下来我们看看怎样使用LitePal更新数据。更新数据要比添加数据稍微复杂一点,因为它的API接口比较多,这里我们只介绍最常用的几种更新方式。
首先,最简单的一种更新方式就是对已存储的对象重新设值,然后重新调用save()方法即可。那么这里我们就要了解一个概念,什么是已存储的对象?
对于LitePal来说,对象是否已存储就是根据调用model.isSaved()方法的结果来判断的, 返回true就表示已存储,返回false就表示未存储。那么接下来的问题就是,什么情况下会返回true,什么情况下会返回false呢?
实际上只有在两种情况下model.isSave()方法才会返回true, 一种情况是已经调用过model. save()方法去添加数据了,此时model会被认为是已存储的对象。另一种情况是model对象是通过LitePal提供的查询API查岀来的,由于是从数据库中查到的对象,因此也会被认为是已存储的对象。
由于查询API我们暂时还没学到,因此只能先通过第一种情况来进行验证。修改MainActivity 中的代码,如下所示:

package com.mxt.litepaltest;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
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);
        Button addData = (Button)findViewById(R.id.add_data);
        Button updateData = (Button)findViewById(R.id.update_data);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
               LitePal.getDatabase();
            }
        });
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setName("The Da Vinci Code");
                book.setauthor("Dan Brown");
                book.setPages(454);
                book.setPrice(16.96);
                book.setPress("UnKnow");
                book.save();
            }
        });
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setName("The Lost Symbol");
                book.setAuthor("Dan Brown");
                book.setPages(510);
                book.setPrice(19.95);
                book.setPress("UnKnow");
                book.save();
                book.setPrice(10.99);
                book.save();
            }
        });
    }
}

在更新数据按钮的点击事件里面,我们先是通过上一小节中学习的知识添加了一条Book数据,然后调用setPrice()方法将这本书的价格进行了修改,之后再次调用了 save()方法。此时 LitePal会发现当前的Book对象是已存储的,因此不会再向数据库中去添加一条新数据,而是会直接更新当前的数据。
现在重新运行一下程序,然后点击Update data按钮,我们再次输入查询语句查看表中的数据情况,结果如图所示:
在这里插入图片描述
可以看到,Book表中新增了一条书的数据,但这本书的价格并不是一开始设置的19.95,而是10.99,说明我们的更新操作确实生效了。
但是这种更新方式只能对已存储的对象进行操作,限制性比较大,接下来我们学习另外一种 更加灵巧的更新方式。修改MainActivity中的代码,如下所示:

package com.mxt.litepaltest;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
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);
        Button addData = (Button)findViewById(R.id.add_data);
        Button updateData = (Button)findViewById(R.id.update_data);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
               LitePal.getDatabase();
            }
        });
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setName("The Da Vinci Code");
                book.setauthor("Dan Brown");
                book.setPages(454);
                book.setPrice(16.96);
                book.setPress("UnKnow");
                book.save();
            }
        });
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setPrice(14.95);
                book.setPress("Anchor");
                book.updateAll("name = ? and author = ?","The Lost Symbol","Dan Brown");
            }
        });
    }
}

可以看到,这里我们首先new出了一个Book的实例,然后直接调用setPrice()和 setPress()方法来设置要更新的数据,最后再调用updateAll()方法去执行更新操作。注意 updateAll()方法中可以指定一个条件约束,和SQLiteDatabase中update()方法的where参数部分有点类似,但更加简洁,如果不指定条件语句的话,就表示更新所有数据。这里我们指定将所有书名是The Lost Symbol并且作者是Dan Brown的书价格更新为14.95,出版社更新为Anchor。
现在重新运行程序并点击Update data按钮,我们再次查询一下表中的数据情况,结果如图所示:
在这里插入图片描述
意料之中,第二本书的价格被更新成了 14.95,出版社被更新成了 Anchor。怎么样? 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()方法中没有指定约束条件,因此更新操作对所有数据都生效了。

5.6 使用LitePal删除数据

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

package com.mxt.litepaltest;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import org.litepal.LitePal;
import org.litepal.crud.DataSupport;

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);
        Button addData = (Button)findViewById(R.id.add_data);
        Button updateData = (Button)findViewById(R.id.update_data);
        Button deleteButton = (Button)findViewById(R.id.delete_data); 
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
               LitePal.getDatabase();
            }
        });
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setName("The Da Vinci Code");
                book.setauthor("Dan Brown");
                book.setPages(454);
                book.setPrice(16.96);
                book.setPress("UnKnow");
                book.save();
            }
        });
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setPrice(14.95);
                book.setPress("Anchor");
                book.updateAll("name = ? and author = ?","The Lost Symbol","Dan Brown");
            }
        });
        deleteButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                DataSupport.deleteAll(Book.class,"price < ?","15");
            }
        });
    }
}

这里调用了 DataSupport.deleteAll()方法来删除数据,其中deleteAll ()方法的第一个参数用于指定删除哪张表中的数据,Book.class就意味着删除Book表中的数据,后面的参数用于指定约束条件,应该不难理解。那么这行代码的意思就是,删除Book表中价格低于15的书, 正好目前Book表中有两本书,一本价格是16.96, 一本价格是14.95,刚好可以看出效果。
现在重新运行程序,并点击一下Delete data按钮,然后查询表中的数据情况,如图所示:

在这里插入图片描述
可以看到,价格低于15的那本书已经被删除掉了。
另外,deleteAll ()方法如果不指定约束条件,就意味着你要删除表中的所有数据,这一点和updateAll()方法是比较相似的。

5.7 使用LitePal查询数据

终于又到了最复杂的查询数据部分了,不过这个“最复杂”只是相对于过去而言,因为使用 LitePal来查询数据一点都不复杂。我一直都认为LitePal在查询API方面的设计极为人性化,想想之前我们所使用的query ()方法,冗长的参数列表让人看得头疼,即使多数参数都是用不到的, 也不得不传入null,如下所示:

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

像这样的代码恐怕是没人会喜欢的。为此LitePal在查询API方面做了非常多的优化,基本上可以满足绝大多数场景的查询需求,并且代码十分整洁,下面我们就来一起学习一下。
首先分析一下上述代码,query()方法中使用了第一个参数指明去查询Book表,后面的参数全部为null,这就表示希望查询这张表中的所有数据。那么使用LitePal如何完成同样的功能 呢?非常简单,只需要这样写:

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

怎么样,代码是不是简单易懂多了?没有冗长的参数列表,只需要调用一下findAll()方法, 然后通过Book.class参数指定查询Book表就可以。另外,findAll()方法的返回值是一个Book 类型的List集合,也就是说,我们不用像之前那样再通过Cursor对象一行行去取值了,LitePal 已经自动帮我们完成了赋值操作。
下面通过一个完整的例子来实践一下吧,修改MainActivity中的代码,如下所示:

package com.mxt.litepaltest;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;
import org.litepal.crud.DataSupport;

import java.util.List;

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);
        Button addData = (Button)findViewById(R.id.add_data);
        Button updateData = (Button)findViewById(R.id.update_data);
        Button deleteButton = (Button)findViewById(R.id.delete_data);
        Button queryData = (Button)findViewById(R.id.query_data);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
               LitePal.getDatabase();
            }
        });
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setName("The Da Vinci Code");
                book.setauthor("Dan Brown");
                book.setPages(454);
                book.setPrice(16.96);
                book.setPress("UnKnow");
                book.save();
            }
        });
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setPrice(14.95);
                book.setPress("Anchor");
                book.updateAll("name = ? and author = ?","The Lost Symbol","Dan Brown");
            }
        });
        deleteButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                DataSupport.deleteAll(Book.class,"price < ?","15");
            }
        });
        queryData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                List<Book> books = DataSupport.findAll(Book.class);
                for (Book book : books){
                    Log.d("MainActivity","书名是"+book.getName());
                    Log.d("MainActivity","书的作者是"+book.getAuthor());
                    Log.d("MainActivity","书的页数是"+book.getPages());
                    Log.d("MainActivity","书的价格是"+book.getPrice());
                    Log.d("MainActivity","书的出版状态是"+book.getPress());
                }
            }
        });
    }
}

查询的那段代码刚刚已经解释过了,接下来就是遍历List集合中的Book对象,并将其中的信息全部打印出来。现在重新运行一下程序,点击Query data按钮,然后查看logcat的打印内容, 结果如图所示:
在这里插入图片描述
Book表中只剩下一条数据,由此可见,我们已经将这条数据成功查询出来了。
除了 findAll()方法之外,LitePal还提供了很多其他非常有用的查询API。比如我们想要查 询Book表中的第一条数据就可以这样写:

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

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

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

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

  1. select ()方法用于指定查询哪几列的数据,对应了 SQL当中的select关键字。比如只 查name和author这两列的数据,就可以这样写:
List<Book> books = DataSupport.select("name", "author").find(Book.class);
  1. where()方法用于指定查询的约束条件,对应了 SQL当中的where关键字。比如只查页数大于400的数据,就可以这样写:
List<Book> books = DataSupport.where("pages > ?", "400").find(Book.class);
  1. order()方法用于指定结果的排序方式,对应了 SQL当中的order by关键字。比如将 查询结果按照书价从高到低排序,就可以这样写:
List<Book> books = DataSupport.order("price desc").find(Book.class);
其中desc表示降序排列,asc或者不写表示升序排列。
  1. limit ()方法用于指定查询结果的数量,比如只查表中的前3条数据,就可以这样写:
List<Book> books = DataSupport.limit(3).find(Book.class);
  1. offset()方法用于指定查询结果的偏移量,比如查询表中的第2条、第3条、第4条数 据,就可以这样写:
List<Book> books = DataSupport.limit(3).offset(1).find(Book.class);

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

List<Book> books = DataSupport.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()方法, 不管需不需要用到,都必须要传固定的参数进去才行。
关于LitePal的查询API差不多就介绍到这里,这些API已经足够我们应对绝大多数场景的查询需求了。当前,如果你实在有一些特殊需求,上述的API都满足不了你的时候,LitePal仍然支持使用原生的SQL来进行查询:

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赈川

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值