65.9K
CodeProject 正在变化。 阅读更多。
Home

Android 数据存储

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.57/5 (9投票s)

2014 年 9 月 14 日

CPOL

49分钟阅读

viewsIcon

49701

downloadIcon

1452

了解如何管理数据。

目录

引言

数据存储是你想构建应用程序时应该学习的重要内容。在 Android 平台上,你可以将数据存储在设备存储中的文件、数据库或共享偏好设置里,还可以通过 Dropbox、Parse 等外部 API 将数据存储在云端。

背景

当你创建应用程序时,你会发现你需要存储一些数据,比如用户信息、图片等,这正是我们在本文中将要学习的主题。要学习本文,你需要了解 Android 应用程序开发的基础知识。

开始吧

如今,数据是你应用程序前进的最强大的燃料,你需要知道如何管理数据?如何存储它们?以便提供最佳的用户体验。有许多选项可以存储数据,例如文件、数据库、共享偏好设置等。

文件

文件是最简单的数据存储方式。它非常灵活,你可以设计自己的结构,或使用标准格式,如 XML、JSON 等,你也可以直接将实例序列化到文件中。Android 允许你在内部存储和外部存储(可能是可移动的或不可移动的)中保存文件。

文件

当你想要写入文件时,你需要创建一个新文件或打开一个现有文件,然后写入你想要的数据,使用完毕后关闭它。Java 提供了 File 类来表示文件或目录路径名,你可以用它来创建新文件、打开现有文件、删除文件、创建目录等。

Java 中 File 操作的示例

File file = new File("Sample.txt");
try {
    System.out.println("exists: "+file.exists());
    file.createNewFile();
    System.out.println("exists: "+file.exists());
    System.out.println("canRead: "+file.canRead());
    System.out.println("canWrite: "+file.canWrite());
    System.out.println("getPath: "+file.getPath());
    System.out.println("getAbsolutePath: "+file.getAbsolutePath());
    System.out.println("getCanonicalPath: "+file.getCanonicalPath());
    System.out.println("getParent: "+file.getParent());
} catch (IOException e) {
    e.printStackTrace();
}

以上代码的运行结果

exists: false
exists: true
canRead: true
canWrite: true
getPath: Sample.txt
getAbsolutePath: E:\java\DataStorage\Sample.txt
getCanonicalPath: E:\java\DataStorage\Sample.txt
getParent: null

你会看到我实例化了一个 File 对象,路径名为 "Sample.txt",然后我检查它的存在性,Java 告诉我文件尚不存在。我创建了它并再次检查,Java 告诉我存在一个我调用的文件,之后我检查读写权限。getPath 方法返回我在构造函数中给它的路径,getAbsolutePath 方法返回文件在文件系统根目录下的完整路径,而 getCanonicalPath 返回被格式化为系统依赖形式的绝对路径。我尝试获取此文件的父级,但返回了 null 值,因为我在构造对象时提供的路径名中没有父路径。

注意:操作 File 时,可能会抛出 IOException。所以你需要使用 Try/Catch Block 来处理它,或者使用 throws 关键字将其抛出。

规范路径与绝对路径

规范路径是绝对路径,它以系统依赖的方式映射到其唯一形式,并移除冗余名称,例如".""..".

例如

Sample.txt 是一个相对路径

.\Sample.txt 是一个相对路径

E:\java\DataStorage\..\DataStorage\Sample.txt 是一个绝对路径,但不是一个规范路径

E:\java\DataStorage\Sample.txt 是一个绝对路径,并且也是一个规范路径

注意:Windows 操作系统不区分大小写,而 *NIX 系统区分(区分大小写)。

File 类的一些有趣方法

  • boolean canRead() - 如果应用程序可以读取文件,则返回 true。
  • boolean canWrite() - 如果应用程序可以向文件写入数据,则返回 true。
  • boolean createNewFile() - 如果文件尚不存在,则创建文件。
  • static File createTempFile() - 用于创建临时文件。
  • boolean delete() - 删除文件或目录。
  • void deleteOnExit() - 在应用程序关闭时删除文件或目录。
  • boolean exists() - 检查文件或目录是否存在。
  • File getAbsoluteFile() - 获取使用绝对路径名实例化的新 File 对象。
  • String getAbsolutePath() - 获取文件或目录的绝对路径名。
  • File getCanonicalFile() - 获取使用规范路径名实例化的新 File 对象。
  • String getCanonicalPath() - 获取文件或目录的规范路径名。
  • String getName() - 获取文件或目录的名称。
  • String getParent() - 获取此路径名的父路径名。如果不存在父路径名,则返回 null。
  • File getParentFile() - 获取路径名的父级 File 对象。
  • String getPath() - 获取路径名字符串。
  • boolean isAbsolute() - 如果路径名是绝对路径名,则返回 true。
  • boolean isDirectory() - 如果路径名是普通文件,则返回 true。(此处原文有误,应为 isFile())
  • boolean isHidden() - 如果路径名是隐藏文件,则返回 true。
  • long lastModified() - 返回文件最后修改的时间。
  • long length() - 返回文件的大小。
  • String[] list() - 返回目录中文件和目录名称的字符串数组。
  • File[] listFiles() - 返回目录中文件和目录的 File 对象数组。
  • boolean mkdir() - 创建目录。
  • boolean mkdirs() - 创建目录,包括任何必要的父目录。
  • boolean renameTo() - 重命名文件。
  • URI toURI() - 返回路径名的 URI 对象。

在文件部分,你会发现没有直接读写文件的方法。在编程中,当你想要读写内存、存储、网络等中的数据时,你需要通过 I/O 流来完成。流表示一系列数据。

I/O 流只允许单向数据流,这意味着你不能使用同一个流进行读写。应用程序使用输入流一次只从一个源读取数据,而应用程序使用输出流一次只将数据写入一个目的地。流支持许多不同类型的数据,如字节、原始数据类型、对象等。

虽然 Android 使用 Java 作为应用程序开发语言,但我们将学习 Java 中的基本 I/O 流,然后尝试在 Android 应用程序中使用它们。


字节流

字节流用于以原始字节的形式读写数据。Java 提供了抽象类 InputStreamOutputStream 以及它们的许多派生类,如 FileInputStream、ObjectInputStream 等。在本文中,我们将重点关注 FileInputStreamFileOutputStream

从输入流读取数据

要从源读取数据,你可以使用 InputStream 抽象类提供的 read() 方法。

 int read()  - 以 int (0-255) 的形式返回输入数据,如果检测到“流结束”则返回 -1,或在 I/O 错误时抛出 IOException

示例

我有一个名为 a.txt 的文件,内容如下

HELLO

然后我使用 FileInputStream 读取此文件并将结果打印到控制台

        File sourceFile = new File("a.txt");
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream(sourceFile);
            
            int read;
            while((read = fileInputStream.read())!=-1){
                System.out.printf("%s -> %d -> %c\n",
                        Integer.toBinaryString(read),
                        read,
                        (char)read);
            }
        }
        finally{
            if(fileInputStream!=null)
                fileInputStream.close();
        }

我使用 while 循环逐字节读取文件,然后以二进制格式、十进制整数和字符的形式打印数据。这是我得到的结果

1001000 -> 72 -> H
1000101 -> 69 -> E
1001100 -> 76 -> L
1001100 -> 76 -> L
1001111 -> 79 -> O

 

int read(byte[] bytes) - 将数据作为字节数组读取。返回读取的字节数,如果检测到“流结束”则返回 -1。

此方法与不带参数的 read() 方法有所不同。此方法需要一个字节数组作为参数,因为它会将结果存储在该数组中,并返回数据长度作为方法结果。

示例

使用上面示例中的相同数据源文件,我创建了一个长度为 10 的字节数组并将其作为 read() 方法的参数传递

        File sourceFile = new File("a.txt");
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream(sourceFile);
            
            int length;
            byte[] bytes = new byte[10];
            while((length = fileInputStream.read(bytes))!=-1){
                byte[] readBytes = Arrays.copyOfRange(bytes, 0, length);
                StringBuilder builder = new StringBuilder();
                for(int i=0;i<readBytes.length;i++){
                    builder.append(Integer.toBinaryString(readBytes[i] & 255 | 256).substring(1));
                    builder.append(String.format("(%c)", (char)readBytes[i]));
                    builder.append(" ");
                }
                System.out.printf("%d bytes read: %s \n",length,builder.toString());
            }
        }
        finally{
            if(fileInputStream!=null)
                fileInputStream.close();
        }

读取的每个字节都会存储在你提供的数组中。在循环的每次迭代中,数组将从索引 0 开始填充到 read(bytes) 方法返回的数据长度。然后我复制数组的一部分,只复制 read(bytes) 方法返回的数据长度,并在屏幕上打印数据长度、二进制数据及其字符值。这是我得到的结果

5 bytes read: 01001000(H) 01000101(E) 01001100(L) 01001100(L) 01001111(O) 

返回了 5 个字节。

在循环的每次迭代中,我将相同的数组作为 read(bytes) 方法的参数,上一轮的数据将被新一轮的数据覆盖。例如,你有一个文本文件,内容是 "HELLO WORLD"。你将一个**长度为 5 的字节数组**传递给 read(bytes),第一轮你得到的结果是 [H,E,L,L,O],返回 **5**;第二轮你会得到 [ ,W,O,R,L],返回 **5**;最后一轮你会得到 [D,W,O,R,L],返回 **1**。你会看到 W,O,R,L 是第二轮的结果仍然存储在数组中。

 

int read(byte[] bytes, int offset, int length) - 将数据读取到指定偏移量和指定长度的字节数组中。返回读取的字节数,如果检测到“流结束”则返回 -1。

此重载方法的工作方式与 read(bytes) 方法类似,但你可以指定要开始填充数据的数组索引以及要读取的长度。

示例

使用上面示例中的相同数据源文件。我创建了一个名为 totalLength 的新变量,用于存储读取的总长度。在循环的每次迭代中,我将数组的起始索引移动到 totalLength,并只读取 3 个字节。

        File sourceFile = new File("a.txt");
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream(sourceFile);
            
            int totalLength = 0;
            int length;
            byte[] bytes = new byte[10];
            while((length = fileInputStream.read(bytes,totalLength,3))!=-1){
                byte[] readBytes = Arrays.copyOfRange(bytes, totalLength, totalLength+length);
                StringBuilder builder = new StringBuilder();
                for(int i=0;i<readBytes.length;i++){
                    builder.append(Integer.toBinaryString(readBytes[i] & 255 | 256).substring(1));
                    builder.append(String.format("(%c)", (char)readBytes[i]));
                    builder.append(" ");
                }
                System.out.printf("%d bytes read: %s \n",length,builder.toString());
                totalLength += length;
            }
            
            byte[] resultBytes = Arrays.copyOfRange(bytes, 0, totalLength);
            
            StringBuilder builder = new StringBuilder();
            for(int i=0;i<resultBytes.length;i++){
                builder.append(Integer.toBinaryString(resultBytes[i] & 255 | 256).substring(1));
                builder.append(String.format("(%c)", (char)resultBytes[i]));
                builder.append(" ");
            }
            System.out.printf("All data read: %s \n",builder.toString());
            
        }
        finally{
            if(fileInputStream!=null)
                fileInputStream.close();
        }

这是结果

3 bytes read: 01001000(H) 01000101(E) 01001100(L)  
2 bytes read: 01001100(L) 01001111(O)  
All data read: 01001000(H) 01000101(E) 01001100(L) 01001100(L) 01001111(O) 

你会看到程序读取了文件 2 次,第一轮得到了 3 个字节的数据,第二轮得到了 2 个字节的数据,因为我将每次读取的数据长度限制为 3。然后我打印了从文件 [H,E,L,L,O] 中接收到的所有数据。

将数据写入输出流

要将数据写入目标源,你可以使用 OutputStream 抽象类提供的 write() 方法。

write(int) - 将单个字节数据写入文件。

在写入之前,你需要将数据转换为整数。

示例

我想将单词 "HELLO" 写入文件 c.txt。我将单词转换为整数数组 [72,69,76,76,79],然后使用 write(int) 将每个字符写入文件。

        File destinationFile = new File("c.txt");
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(destinationFile);
            
            int[] data = new int[]{72,69,76,76,79};
            for(int i=0;i<data.length;i++){
                fileOutputStream.write(data[i]);
            }
        }
        finally{
            if(fileOutputStream!=null)
                fileOutputStream.close();
        }

运行程序后,我打开文件 c.txt,看到如下结果

HELLO

看起来很麻烦?你可以使用 String 的 toCharArray() 方法将 String 转换为字符数组

        File destinationFile = new File("c.txt");
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(destinationFile);
            String helloString = "HELLO";
            char[] data = helloString.toCharArray();
            for(int i=0;i<data.length;i++){
                fileOutputStream.write((int)data[i]);
            }
        }
        finally{
            if(fileOutputStream!=null)
                fileOutputStream.close();
        }

这是结果(c.txt

HELLO

 

write(byte[] bytes) - 将字节数组写入文件。

示例

与上一个示例一样,我想将单词 "HELLO" 写入文件 c.txt,但我将使用 write(bytes) 替代。

        File destinationFile = new File("c.txt");
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(destinationFile);
            
            String helloString = "HELLO";
            byte[] data = helloString.getBytes();
            fileOutputStream.write(data);
        }
        finally{
            if(fileOutputStream!=null)
                fileOutputStream.close();
        }

我通过使用 getBytes() 方法将 String 转换为 byte[]。然后我得到了与上一个示例相同的结果

HELLO

 

write(byte[] bytes, int offset, int length) - 你也可以写入指定起始索引和长度的字节数组。

示例

我将编辑上一个示例代码,只写入字符串 "ELL" 到文件。字母 "E" 的索引是 1,"ELL" 的长度是 3,因此我将使用 write(data,1,3) 来写入文件。

      File destinationFile = new File("c.txt");
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(destinationFile);
            
            String helloString = "HELLO";
            byte[] data = helloString.getBytes();
            fileOutputStream.write(data,1,3);
        }
        finally{
            if(fileOutputStream!=null)
                fileOutputStream.close();
        }

这是结果(c.txt

ELL


注意:如果你想在文件末尾追加数据,你可以使用构造函数 FileOutputStream(String fileName, boolean append) 或 FileOutputStream(Filefile, boolean append) 来构造 FileOutputStream

 

关闭流

使用完流后,你应该使用 close() 方法将其关闭。

        if(fileOutputStream!=null)
                fileOutputStream.close();

注意:关闭不再需要的流非常重要,以避免资源泄露。建议在 try-catch 块的 finally 子句中关闭流,以确保流在使用后会被关闭。

现在,你已经了解了字节流,你会发现你需要进行一些额外的工作来转换数据,因为这些流只支持整数和字节数组。不建议将字节流与字符数据一起使用,而应该使用字符流。

 

字符流

字符流用于从源读取/写入字符数据,Java 提供了 ReaderWriter 抽象类来处理字符数据流。在本文中,我们将重点关注 FileReaderFileWriter 类。Java 内部以 16 位字符集存储字符,但外部数据源可能使用其他字符集,字符流将帮助你将数据从 Java 内部字符集转换为本地字符集。

从 Reader 读取数据

与 InputStream 类似,这里也有 read() 方法用于读取字符数据。

int read() - 返回单个字符,其值为 0 到 65535 之间的整数,或在检测到“流结束”时返回 -1。

示例

我使用与 InputStream 示例相同的源文件 (a.txt)。

        File sourceFile = new File("a.txt");
        FileReader fileReader = null;
        try {
            fileReader = new FileReader(sourceFile);
            int read;
            while((read = fileReader.read())!=-1)
            {
                System.out.printf("%s -> %d -> %c\n",
                        Integer.toBinaryString(read),
                        read,
                        (char)read);
            }
        }
        finally{
            if(fileReader!=null)
                fileReader.close();
        }

代码与 InputStream 示例代码类似,但使用 FileInputReader 而不是 FileInputStream。这是结果

1001000 -> 72 -> H
1000101 -> 69 -> E
1001100 -> 76 -> L
1001100 -> 76 -> L
1001111 -> 79 -> O

与 InputStream 示例结果没有区别。

我将尝试读取包含非 ASCII 字符的文件。我创建了一个名为 d.txt 的文件,其内容如下

สวัสดี HELLO

该文件包含泰语字符和英文字符。然后我尝试使用 FileInputReader 读取它。

这是我得到的结果

111000101010 -> 3626 -> ส
111000100111 -> 3623 -> ว
111000110001 -> 3633 -> ั
111000101010 -> 3626 -> ส
111000010100 -> 3604 -> ด
111000110101 -> 3637 -> ี
100000 -> 32 ->  
1001000 -> 72 -> H
1000101 -> 69 -> E
1001100 -> 76 -> L
1001100 -> 76 -> L
1001111 -> 79 -> O

结果是正确的!!现在我想知道如果我使用 FileInputStream 会得到什么结果。

11001010 -> 202 -> ?
11000111 -> 199 -> ?
11010001 -> 209 -> ?
11001010 -> 202 -> ?
10110100 -> 180 -> ?
11010101 -> 213 -> ?
100000 -> 32 ->  
1001000 -> 72 -> H
1000101 -> 69 -> E
1001100 -> 76 -> L
1001100 -> 76 -> L
1001111 -> 79 -> O

哇,结果是错误的!!

 

int read(char[] chars) - 将字符数据作为字符数组读取。

示例

我将读取 b.txt 文件作为长度为 10 的字符数组。

    public static void readAsCharArray() throws IOException, FileNotFoundException
    {
        File sourceFile = new File("d.txt");
        FileReader fileReader = null;
        try {
            fileReader = new FileReader(sourceFile);
            
            int length;
            char[] chars = new char[10];
            while((length = fileReader.read(chars))!=-1)
            {
                char[] readChars = Arrays.copyOfRange(chars, 0, length);
                System.out.printf("%d characters read: %s \n",length,new String(readChars));
            }
        }
        finally{
            if(fileReader!=null)
                fileReader.close();
        }
    }

注意:你可以通过使用 char[] 作为参数构造新的 String 来将 char[] 转换为 String。

这是我得到的结果

10 characters read: สวัสดี HEL
2 characters read: LO

 

int read(char[] chars, int offset, int length) - 以指定的偏移量和长度将字符数据作为字符数组读取。

示例

        File sourceFile = new File("d.txt");
        FileReader fileReader = null;
        try {
            fileReader = new FileReader(sourceFile);
            
            int totalLength = 0;
            int length;
            char[] chars = new char[20];
            while((length = fileReader.read(chars,totalLength,3))!=-1)
            {
                char[] readChars = Arrays.copyOfRange(chars, totalLength, totalLength+length);
                System.out.printf("%d characters read: %s \n",length,new String(readChars));
                totalLength += length;
            }
            
            char[] readChars = Arrays.copyOfRange(chars, 0, totalLength);
            System.out.printf("All characters read: %s \n",new String(readChars));

        }
        finally{
            if(fileReader!=null)
                fileReader.close();
        }

这是结果

3 characters read: สวั
3 characters read: สดี
3 characters read:  HE
3 characters read: LLO
All characters read: สวัสดี HELLO

 

将数据写入 Writer

Writer 抽象类提供了 write() 方法,用于将数据写入文件。

write(int character) - 将单个字符写入文件。

示例

我想将字符串 "สวัสดี HELLO" 写入文件 e.txt。只需创建 FileWriter,然后创建一个循环将 String 中的每个字符写入文件。

        File destinationFile = new File("e.txt");
        FileWriter fileWriter = null;
        try {
            fileWriter = new FileWriter(destinationFile);
            String helloString = "สวัสดี HELLO";
            char[] helloChars = helloString.toCharArray();
            for(int i=0;i<helloChars.length;i++){
                fileWriter.write((int)helloChars[i]);
            }
        }
        finally{
            if(fileWriter!=null)
                fileWriter.close();
        }

这是我得到的结果(e.txt

สวัสดี HELLO

 

write(char[] chars) - 将数组中的所有字符写入文件。

示例

从上一个示例中,我们可以通过将 char[] 作为 write() 方法的参数来更简单地将字符串写入文件。

        File destinationFile = new File("e.txt");
        FileWriter fileWriter = null;
        try {
            fileWriter = new FileWriter(destinationFile);
            String helloString = "สวัสดี HELLO";
            char[] helloChars = helloString.toCharArray();
            fileWriter.write(helloChars);
        }
        finally{
            if(fileWriter!=null)
                fileWriter.close();
        }

这是结果(e.txt

สวัสดี HELLO

 

write(char[] chars, int offset, int length) - 将字符数组写入文件,指定起始索引和长度。

示例

与上一个示例一样,我想只写入单词 "HELLO" 而不是完整的字符串 "สวัสดี HELLO"。我需要将参数起始索引和长度传递给 write() 方法。

        File destinationFile = new File("e.txt");
        FileWriter fileWriter = null;
        try {
            fileWriter = new FileWriter(destinationFile);
            String helloString = "สวัสดี HELLO";
            char[] helloChars = helloString.toCharArray();
            fileWriter.write(helloChars,7,5);
        }
        finally{
            if(fileWriter!=null)
                fileWriter.close();
        }

这是我得到的结果(e.txt

HELLO

注意:如果你想在文件末尾追加数据,你可以使用构造函数 FileWriter(String fileName, boolean append) 或 FileWriter(Filefile, boolean append) 来构造 FileWriter

字符流会自动将 Java 内部格式与本地字符集相互转换,但不能保证结果正确。坏消息是,你应该避免使用 FileReader/FileWriter,因为**你无法控制文件编码的字符集**。

 

缓冲流

缓冲流旨在通过缓冲输入/输出数据来降低本地 API 调用的成本。当你使用缓冲流读取数据时,它将从缓冲区读取数据,只有当缓冲区为空时才会调用本地输入 API。另一方面,当你将数据写入缓冲流时,它会将数据放入缓冲区,只有当缓冲区满时才会调用本地写入 API。

Java 提供了 BufferedInputStream/BufferedOutputStream 类来创建缓冲字节流,以及 BufferedReader/BufferedWriter 类来创建缓冲字符流。

要在 Java 中使用缓冲流,你需要另一个非缓冲流并将其与缓冲流链接起来。

示例

你可以用 BufferedInputStream 包装 FileInputStream

        File sourceFile = new File("a.txt");
        FileInputStream fileInputStream = null;
        BufferedInputStream bufferedInputStream = null;
        try {
            fileInputStream = new FileInputStream(sourceFile);
            bufferedInputStream = new BufferedInputStream(fileInputStream);
            
            int read;
            while((read = bufferedInputStream.read())!=-1)
            {
                System.out.printf("%s -> %d -> %c\n",
                        Integer.toBinaryString(read),
                        read,
                        (char)read);
            }
        }
        finally{
            if(bufferedInputStream!=null)
                bufferedInputStream.close();
            if(fileInputStream!=null)
                fileInputStream.close();
        }

设置缓冲区大小

如果你想设置缓冲区大小,你可以通过将大小作为 int 值传递给构造函数来设置。

示例

我想将 BufferedInputStream 的缓冲区大小设置为 512 字节。

bufferedInputStream = new BufferedInputStream(fileInputStream,512);

 

刷新输出流

当你使用缓冲输出流时,有时你需要强制输出流写出缓冲区而无需等待其填满。这被称为刷新缓冲区。Java 提供了 flush() 方法来完成此操作。

示例

我想在每次向流写入内容时刷新缓冲区。

        File sourceFile = new File("a.txt");
        File destinationFile = new File("b.txt");
        FileInputStream fileInputStream = null;
        BufferedInputStream bufferedInputStream = null;
        FileOutputStream fileOutputStream = null;
        BufferedOutputStream bufferedOutputStream = null;
        try {
            fileInputStream = new FileInputStream(sourceFile);
            bufferedInputStream = new BufferedInputStream(fileInputStream);
            fileOutputStream = new FileOutputStream(destinationFile);
            bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
            
            int read;
            while((read = bufferedInputStream.read())!=-1)
            {
                fileOutputStream.write(read);
                fileOutputStream.flush();
            }
        }
        finally{
            if(bufferedInputStream!=null)
                bufferedInputStream.close();
            if(bufferedOutputStream!=null)
                bufferedOutputStream.close();
            if(fileInputStream!=null)
                fileInputStream.close();
            if(fileOutputStream!=null)
                fileOutputStream.close();
        }

注意:不建议像示例那样每次写入数据时都刷新缓冲区。

某些缓冲流(如 PrintWriter)支持自动刷新,如果你启用自动刷新功能,PrintWriter 将在你每次调用 println()format() 方法时刷新缓冲区。

 

流的链式调用

在缓冲流的示例代码中,我将 BufferedInputStream 与 FileInputStream 连接起来以缓冲输入。这被称为流的链式调用或分层。

何时链式调用流?我们链式调用流是为了特定的目的,如转换数据、缓冲、过滤等。当你想要为流添加一些行为时,你应该链式调用它们。

 

数据流

想读写原始数据?数据流使其成为可能,使用数据流,你可以读写所有原始类型值和 String 值。所有数据流都派生自 DataInput 接口或 DataOutput 接口。在本文中,我们将重点关注 DataInputStreamDataOutputStream 类。

示例

我想将一些原始数据写入文件,然后尝试读取它并打印到控制台。

        File file = new File("primitive_data.txt");
        FileOutputStream filOutputStream = null;
        DataOutputStream dataOutputStream = null;
        FileInputStream fileInputStream = null;
        DataInputStream dataInputStream = null;
        
        try {
            filOutputStream = new FileOutputStream(file);
            dataOutputStream = new DataOutputStream(filOutputStream);
            
            fileInputStream = new FileInputStream(file);
            dataInputStream = new DataInputStream(fileInputStream);
            
            String itemName = "Nexus 5";
            double itemPrice = 299.99;
            int itemCount = 10;
            
            dataOutputStream.writeUTF(itemName);
            dataOutputStream.writeDouble(itemPrice);
            dataOutputStream.writeInt(itemCount);
            
            System.out.println(dataInputStream.readUTF());
            System.out.println(dataInputStream.readDouble());
            System.out.println(dataInputStream.readInt());
    
        }
        finally{
            if(dataOutputStream!=null)
                dataOutputStream.close();
            if(fileInputStream!=null)
                fileInputStream.close();
        }

这是控制台上的结果

Nexus 5
299.99
10

注意:如果读取顺序不正确,你将获得不正确的数据。

注意:有很多方法可以读写原始数据,你可以在 IDE 的建议框中找到它们。readUTF 和 writeUTF 用于以 UTF-8 字符集读写字符串。

 

控制流的字符集

Java 使用 UCS-2 字符集存储字符,但数据源可能使用其他字符集。FileReader/FileWriter 可能无法正确格式化数据,InputStreamReader 和 OutputStreamWriter 是你需要的。

InputStreamReader/OutputStreamWriter 是字节流到字符流的桥梁,它们允许你指定用于解码/编码数据的字符集。

这些是 InputStreamReader 的构造函数

  • InputStreamReader(InputStream in) - 使用默认字符集解码数据。
  • InputStreamReader(InputStream in, Charset cs) - 使用指定字符集编码数据。
  • InputStreamReader(InputStream in, CharsetDecoder dec) - 使用指定字符集编码数据。
  • InputStreamReader(InputStream in, String charsetName) - 使用指定字符集编码数据。

这些是 OutputStreamWriter 的构造函数

  • OutputStreamWriter(OutputStream out) - 使用默认字符集编码数据。
  • OutputStreamWriter(OutputStream out, Charset cs) - 使用指定字符集编码数据。
  • OutputStreamWriter(OutputStream out, CharsetEncoder enc) - 使用指定字符集编码数据。
  • OutputStreamWriter(OutputStream out, String charsetName) - 使用指定字符集编码数据。

示例

我想以 UTF-8 作为字符集将数据写入文件。

        File file = new File("utf8output.txt");
        FileOutputStream filOutputStream = null;
        OutputStreamWriter outputStreamWriter = null;
        
        try {
            filOutputStream = new FileOutputStream(file);
            outputStreamWriter = new OutputStreamWriter(filOutputStream,"UTF-8");
            
            outputStreamWriter.append("ทอสอบภาษาไทย Charset UTF-8");

    
        }
        finally{
            if(outputStreamWriter!=null)
                outputStreamWriter.close();
            if(filOutputStream!=null)
                filOutputStream.close();
        }

这是结果(utf8output.txt

ทอสอบภาษาไทย Charset UTF-8

注意:你可以从官方 Android 参考资料中查看 Android 支持的字符集。

 

对象流

我将在本文中描述的最后一种流类型。当 Data Streams 允许你读写原始数据值时,Object Streams 允许你读写复杂数据类型(对象)。

序列化是将对象转换为序列化比特流的过程,该比特流可以被写出或重新构造为对象。

什么类型的 Object 可以序列化?大多数标准类都可以序列化,并且你可以通过实现 Serializable 标记接口来使你的类可序列化。

示例

我创建了一个 Car 数据的类,包含 brand 和 model 属性。然后添加了 implements 关键字来标记该类可序列化。

    public class Car implements Serializable{
        public String brand;
        public String model;
        @Override
        public String toString() {
            return brand+" "+model;
        }
    }

接下来,我将使用 writeObject(Object) 方法将 Car 对象写入文件 "car.obj"

        FileOutputStream fileOutputStream = null;
        ObjectOutputStream objectOutputStream = null;
        try{
            fileOutputStream = new FileOutputStream("car.obj");
            objectOutputStream = new ObjectOutputStream(fileOutputStream);
            
            Car car = new Car();
            car.brand = "Toyota";
            car.model = "Camry";
            
            objectOutputStream.writeObject(car);
            
        }finally{
            if(fileOutputStream!=null)
                fileOutputStream.close();
            if(objectOutputStream!=null)
                objectOutputStream.close();
        }

这是 car.obj 中的结果

aced 0005 7372 0003 4361 72a5 d324 0f05
4b47 f002 0002 4c00 0562 7261 6e64 7400
124c 6a61 7661 2f6c 616e 672f 5374 7269
6e67 3b4c 0005 6d6f 6465 6c71 007e 0001
7870 7400 0654 6f79 6f74 6174 0005 4361
6d72 79

现在,我想反序列化 Car 对象。我可以通过使用 readObject() 方法然后将其转换为 Car 来实现。

        FileInputStream fileInputStream = null;
        ObjectInputStream objectInputStream = null;
        try{
            fileInputStream = new FileInputStream("car.obj");
            objectInputStream = new ObjectInputStream(fileInputStream);
            
            Car car = (Car)objectInputStream.readObject();
            
            System.out.println(car);
        }finally{
            if(fileInputStream!=null)
                fileInputStream.close();
            if(objectInputStream!=null)
                objectInputStream.close();
        }

当我打印结果时,我得到

Toyota Camry

当你使用 readObject() 方法时,如果需要但没有该对象的类定义,可能会抛出 ClassNotFoundException。你需要处理它。

注意:如果你的类包含对另一个类的引用,你需要确保你引用的所有对象都可以序列化。

 

Android 和文件

Android 允许你像 Java 一样操作文件,你可以使用 Java 的所有流类。大多数 Android 设备提供内置存储(内部存储),以及可移动存储(外部存储),你需要知道哪种数据应该写入哪种存储。

内部存储

内部存储无法从设备中移除,这意味着内部存储上的数据始终可用。默认情况下,此处保存的文件只能被你的应用程序访问,并且当你卸载应用程序时,数据也会被移除。

注意:Android 设备存储挂载路径取决于制造商的配置。你应该避免使用硬编码的路径名。

要使用内部存储,Android 在 Context 类中提供了用于获取内部存储路径名 File 对象的方法。

方法

  • boolean deleteFile(String name) - 删除指定的私有文件,参数是文件名,但不能包含路径分隔符。
  • String[] fileList() - 获取私有文件的名称字符串数组。
  • File getCacheDir() - 获取缓存目录的 File 对象。
  • File getFileStreamPath(String name) - 获取由 openFileInput(String name) 创建的文件的 File 对象。
  • File getFilesDir() - 获取由 openFileInput(String name) 创建的文件目录的 File 对象。
  • FileInputStream openFileInput(String name) - 获取指定私有文件的 FileInputStream。参数是文件名,但不能包含路径分隔符。
  • FileOutputStream openFileOutput(String name, int mode) - 获取指定私有文件的 FileOutputStream,你也可以设置创建模式。参数是文件名,但不能包含路径分隔符。

openFileOutput 模式

  • MODE_APPEND - 将数据追加到现有文件的末尾。
  • MODE_PRIVATE - 默认模式,防止其他应用程序访问文件。
  • MODE_WORLD_READABLE - 允许其他应用程序读取文件。此模式已弃用,应避免使用,因为它非常危险。
  • MODE_WORLD_WRITEABLE - 允许其他应用程序写入文件。此模式已弃用,应避免使用,因为它非常危险。
外部存储

外部存储是可选存储,如 micro SD 卡,它可能是可移动的。这种存储可能不可用,因为用户可以卸载、挂载为 USB 存储或从设备中移除它。存储在外部存储上的数据可以被任何应用程序读写,你无法控制它。当你卸载应用程序时,如果数据是从 getExternalFilesDir() 方法保存的,系统将删除你的数据。

要读写外部存储中的数据,你需要请求 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE 权限。你可以像这样将它们添加到 Manifest 文件中的 manifest 元素:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="..." >
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>

Context 类中有一些方法用于获取外部存储上的文件

  • File getExternalCacheDir() - 获取主要外部存储上的缓存目录的 File 对象。
  • File[] getExternalCacheDirs() - 获取所有外部存储上的缓存目录的 File 对象。
  • File getExternalFilesDir(String type) - 获取主要外部存储上的文件目录的 File 对象。
  • File[] getExternalFilesDirs(String type) - 获取所有外部存储上的文件目录的 File 对象。

你还可以使用 Environment 类来管理外部存储上的文件。

  • static File getExternalStorageDirectory() - 获取主外部存储目录。
  • static File getExternalStoragePublicDirectory(String type) - 获取特定文件类型的顶级公共外部存储目录。
  • static String getExternalStorageState() - 获取主外部存储的状态。
  • static File getRootDirectory() - 获取 Android 的根目录。
  • static String getStorageState(File path) - 获取提供给定路径的存储设备的状态。
  • static boolean isExternalStorageEmulated() - 如果外部存储是模拟的,则返回 true。
  • static boolean isExternalStorageRemovable() - 如果外部存储是可移动的,则返回 true。

公共外部存储文件类型(用作 getExternalStoragePublicDirectory() 的参数)

  • DIRECTORY_ALARMS - 用于放置应该在闹钟列表中的音频文件的目录。
  • DIRECTORY_DCIM - 用于放置用户在将设备挂载到 PC 作为相机时会看到的图片和视频的目录。
  • DIRECTORY_DOCUMENTS - 用于放置用户创建的文档的目录。
  • DIRECTORY_DOWNLOADS - 用于放置用户下载的文件的目录。
  • DIRECTORY_MOVIES - 用于放置用户可用的电影的目录。
  • DIRECTORY_MUSIC - 用于放置任何音乐音频文件的目录。
  • DIRECTORY_NOTIFICATIONS - 用于放置任何通知音频文件的目录。
  • DIRECTORY_PICTURES - 用于放置图片文件的目录。
  • DIRECTORY_PODCASTS - 用于放置任何播客音频文件的目录。
  • DIRECTORY_RINGTONES - 用于放置任何播客铃声文件的目录。

当外部存储可能可移动时,你需要先检查其状态再读取。

public boolean isExternalStorageWritable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;
    }
    return false;
}

public boolean isExternalStorageReadable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }
    return false;
}

开始编码

现在,你已经了解了文件、流和 Android 存储。是时候编写代码了,利用你所知道的一切来解决问题。

任务 1创建一个简单的备忘录应用程序,它只包含一个大的 EditText 和一个保存按钮,你需要完成保存功能。在用户按下保存按钮时保存 EditText 的内容,并在用户再次回到此页面时恢复数据。

建议:使用适合读写文本的流。

示例布局屏幕(请尽量使其美观)

我决定将备忘录数据存储在内部存储中作为文本文件。这是我的代码

布局 XML

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="me.vable.android.datastorage.Task1MemoApp">

    <EditText
        android:id="@+id/edittext_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentLeft="true"
        android:layout_above="@+id/button_save"
        android:gravity="top|left"
        android:layout_margin="8dp"
        />
    <Button
        android:id="@+id/button_save"
        android:text="SAVE"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentLeft="true"
        android:layout_margin="8dp"
        />

</RelativeLayout>

我创建了一个 Activity,并将 EditText 和 Button 的实例作为类变量存储,

    EditText contextEditText;
    Button saveButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_task1_memo_app);

        if(savedInstanceState == null) {
            contextEditText = (EditText) findViewById(R.id.edittext_content);
            saveButton = (Button) findViewById(R.id.button_save);
        }
    }

然后实现按钮的 onClickListener。

public class Task1MemoApp extends ActionBarActivity implements View.OnClickListener {
    ....
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ....
        if(savedInstanceState == null) {
            ....
            saveButton.setOnClickListener(this);
        }
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.button_save:{
               //TODO
            }break;
        }
    }
}

之后,我创建了用于将数据写入文件的该方法。我使用了 DataOutputStream 将文本写入 FileOutputStream。

    private static final String FILE_NAME = "memo.txt";

    ....

    public void saveTheDataToFile() throws IOException{
        FileOutputStream fileOutputStream = null;
        DataOutputStream dataOutputStream = null;

        try {
            fileOutputStream = openFileOutput(FILE_NAME, MODE_PRIVATE);
            dataOutputStream = new DataOutputStream(fileOutputStream);

            String data = contextEditText.getText().toString();
            dataOutputStream.writeUTF(data);

            Toast.makeText(this,"Saved",Toast.LENGTH_SHORT).show();

        }finally {
            if(dataOutputStream!=null)
                dataOutputStream.close();
            if(fileOutputStream!=null)
                fileOutputStream.close();
        }
    }

然后将其添加到 onClick() 方法中。

    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.button_save:{
                try {
                    saveTheDataToFile();
                }catch (IOException e){
                    e.printStackTrace();
                }
            }break;
        }
    }

接下来,我创建了一个用于恢复已保存数据的方法。我在读取数据之前检查了文件是否存在。

    public void restoreTheDataFromFile() throws IOException{

        if(!getFileStreamPath(FILE_NAME).exists())
            return;

        FileInputStream fileInputStream = null;
        DataInputStream dataInputStream = null;

        try {
            fileInputStream = openFileInput(FILE_NAME);
            dataInputStream = new DataInputStream(fileInputStream);

            String data = dataInputStream.readUTF();
            contextEditText.setText(data);

            Toast.makeText(this,"Loaded",Toast.LENGTH_SHORT).show();

        }finally {
            if(dataInputStream!=null)
                dataInputStream.close();
            if(fileInputStream!=null)
                fileInputStream.close();
        }
    }

最后,我将 restoreTheDataFromFile() 语句添加到了 onCreate() 方法中;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_task1_memo_app);

        if(savedInstanceState == null) {
            contextEditText = (EditText) findViewById(R.id.edittext_content);
            saveButton = (Button) findViewById(R.id.button_save);
            saveButton.setOnClickListener(this);

            try {
                restoreTheDataFromFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

尝试运行和测试!!

 

数据库

当你想保存大量具有相同结构/模式的数据时,数据库就是你所需要的。Android 提供对 SQLite 数据库的全面支持。要使用 SQLite,你需要了解一些基本的 SQL 命令知识,你可以在 w3schools.com 查看更多。

注意:Android 有许多替代数据库,如 db4oCouchbasemcobject 等,如果你不喜欢 SQLite,不必担心。

要使用 SQLite,你需要定义一个数据模式并创建派生自 SQLiteOpenHelper 的类。

任务 2:创建一个简单的购物清单应用程序。此应用程序的重要功能是创建新项、标记为已购买和删除。在本任务中,我们将一起学习如何构建它 :)

所需功能

- 添加新项目。

- 标记为已购买。

- 从列表中删除已购买的项目(仅标记为删除)。

- 永久删除数据库中的所有项目。

定义 Schema

当你想要使用 RDBMS 时,Schema 是你应该首先考虑的事情。在 Android 中,你应该创建一个包含定义 URI、表和列名称的常量的契约类。

注意:如果想实现 Content Provider,契约类应该派生自 BaseColumns 接口。BaseColumns 提供了 _ID 属性,该属性由 Content Provider 使用,你也可以选择将 _ID 作为主键。

在购物清单应用程序中,你需要创建一个类来存储项目数据,如下所示

public class Item {

    private int id;
    private String title;
    private boolean bought;
    private boolean deleted;
    private Date createDate = new Date();

    public int getId() {
        return id;
    }

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

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public boolean isBought() {
        return bought;
    }

    public void setBought(boolean bought) {
        this.bought = bought;
    }


    public boolean isDeleted() {
        return deleted;
    }

    public void setDeleted(boolean deleted) {
        this.deleted = deleted;
    }

    public Date getCreateDate() {
        return createDate;
    }

    public void setCreateDate(Date createDate) {
        this.createDate = createDate;
    }

    @Override
    public String toString() {
        return "Item{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", bought=" + bought +
                ", deleted=" + deleted +
                ", createDate=" + createDate +
                '}';
    }
}

这个类有 id、title、bought、deleted 和 createDate 属性。createDate 属性将在该类对象构造时自动创建。

注意:我使用 java.util.Date 作为 createDate 属性,因为它易于格式化。

然后创建购物应用程序的契约类

public final class ShoppingListContract {
    public ShoppingListContract() {}

    public static abstract class ItemEntry implements BaseColumns {
        public static final String TABLE_NAME = "item";
        public static final String COLUMN_NAME_TITLE = "title";
        public static final String COLUMN_NAME_BOUGHT = "bought";
        public static final String COLUMN_NAME_DELETED = "deleted";
        public static final String COLUMN_NAME_CREATED_DATE = "create_date";
    }
}

从上面的代码中,有一个 ShoppingListContract 类,其中包含 ItemEntry 内部类。ItemEntry 类称为契约类,它定义了表名和列名。所有契约类的属性都是静态和最终的,你可以在不创建契约类实例的情况下访问它们,并且运行时无需更改属性值。

注意:契约类应该被声明为抽象类,因为不需要创建它的实例。

创建数据库

要创建数据库,你需要创建一个派生自 SQLiteOpenHelper 的类并实现其方法。

public class ShoppingListDatabaseHelper extends SQLiteOpenHelper {

    public static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "ShoppingList.db";

    public ShoppingListDatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {

    }

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

    }
}

数据库文件将存储在私有存储中,其他应用程序无法访问。接下来,创建用于创建和删除数据库表的 SQL 语句。

注意:SQLite 版本 3 有一些数据类型限制,你可以从 SQLite 官方文档中查看支持的数据类型。

    private static final String SQL_CREATE_ITEM_TABLE =
            "CREATE TABLE " + ShoppingListContract.ItemEntry.TABLE_NAME + " (" +
                    ShoppingListContract.ItemEntry._ID + INTEGER_TYPE+ " PRIMARY KEY AUTOINCREMENT," +
                    ShoppingListContract.ItemEntry.COLUMN_NAME_TITLE + TEXT_TYPE + COMMA_SEP +
                    ShoppingListContract.ItemEntry.COLUMN_NAME_BOUGHT + INTEGER_TYPE +" DEFAULT 0"+ COMMA_SEP +
                    ShoppingListContract.ItemEntry.COLUMN_NAME_DELETED + INTEGER_TYPE +" DEFAULT 0"+ COMMA_SEP +
                    ShoppingListContract.ItemEntry.COLUMN_NAME_CREATED_DATE + TEXT_TYPE +
            " )";

    private static final String SQL_DROP_ITEM_TABLE =
            "DROP TABLE IF EXISTS " +  ShoppingListContract.ItemEntry.TABLE_NAME;

_ID 列被设置为 INTEGER,并在插入数据时自动增加值。我为 bought 和 deleted 属性的列使用了 INTEGER 类型,因为 SQLite 中没有布尔类型。

注意:每个列的默认值是 NULL,如果你没有设置。

然后将其添加到 onCreate()onUpgrade() 方法中。

onCreate() - 该方法仅在数据库创建后调用一次。它用于创建数据库表。

onUpgrade() - 当数据库版本更改时,该方法会调用。它用于更新表结构。

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        sqLiteDatabase.execSQL(SQL_CREATE_ITEM_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        sqLiteDatabase.execSQL(SQL_DROP_ITEM_TABLE);
        onCreate(sqLiteDatabase);
    }

execSQL() 方法用于执行原始 SQL 命令,如 create table、drop table、alter table。

我在 onUpgrade() 方法中简单地删除了并重新创建了表,但在实际应用程序中,当数据库结构更改时,你应该改为 alter table。

写入数据

要将数据写入数据库,你可以使用原始 SQL 命令,或者使用 Android 提供的简单方法。

这是向数据库插入数据的原始 SQL

INSERT INTO table_name VALUES (value1,value2,value3,...);

或者

INSERT INTO table_name (column1,column2,column3,...) VALUES (value1,value2,value3,...);

注意:当 id 列的值是自动递增时,不需要插入 id。

你可以在 Java 代码中为 Item 对象创建原始插入命令,如下所示

    String insertCommand =
                String.format("INSERT INTO %s (%s,%s,%s,%s) VALUES ('%s',%s,%s,'%s')",
                        ShoppingListContract.ItemEntry.TABLE_NAME,
                        ShoppingListContract.ItemEntry.COLUMN_NAME_TITLE,
                        ShoppingListContract.ItemEntry.COLUMN_NAME_BOUGHT,
                        ShoppingListContract.ItemEntry.COLUMN_NAME_DELETED,
                        ShoppingListContract.ItemEntry.COLUMN_NAME_CREATED_DATE,
                        item.getTitle(),
                        item.isBought()? 1 : 0,
                        item.isDeleted()? 1 : 0,
                        dateFormat.format(item.getCreateDate())
                );

你会看到我已经格式化了 SQL 字符串,当你想要插入布尔值时,你需要将其转换为 int,我建议你使用 if-else 语句的快捷方式。

 

if-else 语句快捷方式

当你想要比较某事并执行操作时,你可以使用 if-else 语句,如下所示

if(condition)
   foo();
else
    bar();

但是,如果条件和操作非常简单,你可以改用快捷方式。

condition?foo():bar();

日期格式

当你想要插入 Date 值时,你需要将其转换为格式为 "yyyy-MM-dd HH:mm:ss" 的字符串。你可以使用 SimpleDateFormat 类将 Date 对象格式化为字符串,如下所示

SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
String createDateString = dateFormat.format(item.getCreateDate());

你也可以通过使用 parse() 方法将其转换回 Date 对象。

Date createDate = dateFormat.parse(createDateString );

 

格式化 SQL 字符串后,你将得到如下字符串

INSERT INTO item (title,bought,deleted,create_date) VALUES ('Test Item',0,0,'2014-09-10 19:07:35')

当我们考虑我们的应用程序时,你会发现不需要插入 bought 和 deleted 属性,因为用户无法创建带有已删除标记或已购买标记的新 Item。你可以这样重写 SQL

String insertCommand =
                String.format("INSERT INTO %s (%s,%s) VALUES ('%s','%s')",
                        ShoppingListContract.ItemEntry.TABLE_NAME,
                        ShoppingListContract.ItemEntry.COLUMN_NAME_TITLE,
                        ShoppingListContract.ItemEntry.COLUMN_NAME_CREATED_DATE,
                        item.getTitle(),
                        dateFormat.format(item.getCreateDate())
                );

结果字符串是

INSERT INTO item (title,create_date) VALUES ('Test Item','2014-09-10 19:22:26')

然后我们将执行第三条命令。你需要获取可写数据库并使用其 execSQL() 方法执行该命令。

    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
    public void putItemWithRawSQL(Item item)throws SQLException{
        SQLiteDatabase db = getWritableDatabase();
        String insertCommand =
                String.format("INSERT INTO %s (%s,%s) VALUES ('%s','%s')",
                        ShoppingListContract.ItemEntry.TABLE_NAME,
                        ShoppingListContract.ItemEntry.COLUMN_NAME_TITLE,
                        ShoppingListContract.ItemEntry.COLUMN_NAME_CREATED_DATE,
                        item.getTitle(),
                        dateFormat.format(item.getCreateDate())
                );
        Log.d("insert statement",insertCommand);
        db.execSQL(insertCommand);
        db.close();
    }

我使用了 Log.d() 来打印插入语句,以便于调试 SQL。

注意:如果 SQL 语句无效,execSQL() 方法会抛出 SQLException。你应该将 SQLException 抛给方法的调用者,并尽可能处理它。

现在,我们将创建插入语句,使用 Android 提供的该方法。

long insert(String table, String nullColumnHack, ContentValues values)

  • String table - 你要插入数据的表名。
  • String nullColumnHack - 无法插入完全空白的行。如果提供的 values 为空,Android 将使用此列来创建插入语句。
  • ContentValues values - 你要插入的值。

你需要创建 insert() 方法所需的 ContentValues 实例,ContentValues 是用于存储键值对的类。所以,我们将使用它来存储列-值对,如下所示

ContentValues contentValues = new ContentValues();
contentValues.put(ShoppingListContract.ItemEntry.COLUMN_NAME_TITLE,item.getTitle());
contentValues.put(ShoppingListContract.ItemEntry.COLUMN_NAME_CREATED_DATE,dateFormat.format(item.getCreateDate()));

将所有需要插入的数据放入 ContentValues 实例,然后将其传递给 insert() 语句。

    public long putItemWithInsertMethod(Item item){
        SQLiteDatabase db = getWritableDatabase();

        ContentValues contentValues = new ContentValues();
        contentValues.put(ShoppingListContract.ItemEntry.COLUMN_NAME_TITLE,item.getTitle());
        contentValues.put(ShoppingListContract.ItemEntry.COLUMN_NAME_CREATED_DATE,dateFormat.format(item.getCreateDate()));

        long result = db.insert(ShoppingListContract.ItemEntry.TABLE_NAME,null,contentValues);
        db.close();
        return result;
    }

我将 nullColumnHack 设置为 null,因为如果 values 为 null,我不想插入项。insert() 方法将返回行 ID (long) 作为结果,如果发生错误则返回 -1。

现在,我们将尝试使用此数据库并在我们的应用程序中测试 put 方法。创建 Activity(如果尚未创建),然后查看 Activity 的 onCreate() 方法并初始化数据库助手常量。

ShoppingListDatabaseHelper shoppingListDatabaseHelper = new ShoppingListDatabaseHelper(this);

然后创建 Item 实例,设置其 title。

Item item = new Item();
item.setTitle("Test Item")

之后,我们将尝试使用 putItemWithRawSQL() 方法将 Item 数据插入数据库。

try {
    shoppingListDatabaseHelper.putItemWithRawSQL(item);
    Toast.makeText(this,"The Data is inserted",Toast.LENGTH_SHORT).show();
}catch(SQLException e){
    Toast.makeText(this,e.getStackTrace().toString(),Toast.LENGTH_SHORT).show();
}

putItemWithRawSQL() 方法可能抛出 SQLException 时,你需要处理它。

尝试运行应用程序!!

如果没有错误,该项将插入数据库。如果你收到 SQLException,请检查 putItemWithRawSQL() 方法中的 SQL 语句。

接下来,我们将尝试使用 putItemWithInsertMethod() 方法。

long result = shoppingListDatabaseHelper.putItemWithInsertMethod(item);
Toast.makeText(this,"result: "+String.valueOf(result),Toast.LENGTH_SHORT).show();

然后再次运行应用程序。如果结果不等于 -1,则表示你的数据已插入。

 

接下来,我们将创建 UI 用于将数据添加到数据库。

首先,在布局中添加 EditTextButton

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="me.vable.android.datastorage.Task2ShoppingList">

    <LinearLayout
        android:id="@+id/linearlayout_add_new_item"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentBottom="true"
        >

        <EditText
            android:id="@+id/edittext_add_new_item"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"/>

        <Button
            android:id="@+id/button_add_new_item"
            android:text="ADD"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            />

    </LinearLayout>

</RelativeLayout>

然后,获取 EditTextButton 的实例。

EditText addNewItemEditText;
Button addNewItemButton;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_task2_shopping_list);
    addNewItemEditText = (EditText) findViewById(R.id.edittext_add_new_item);
    addNewItemButton = (Button) findViewById(R.id.button_add_new_item);
}

之后,创建 ShoppingListDatabaseHelper 的实例并实现 addNewItemButtonOnClickListener

....
ShoppingListDatabaseHelper databaseHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
    ....
    databaseHelper = new ShoppingListDatabaseHelper(this);
    addNewItemButton.setOnClickListener(onAddButtonClickListener);
}


View.OnClickListener onAddButtonClickListener = new View.OnClickListener() {
    @Override
    public void onClick(View view) {

    }
};

onAddButtonClickListener 中创建 item 对象并添加到数据库。

View.OnClickListener onAddButtonClickListener = new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        String newItemTitle = addNewItemEditText.getText().toString().trim();
        if(!newItemTitle.isEmpty())
        {
            Item newItem = new Item();
            newItem.setTitle(newItemTitle);
            long result = databaseHelper.putItemWithInsertMethod(newItem);
            if(result > -1)
            {
                Toast.makeText(Task2ShoppingListActivity.this,"New item added",Toast.LENGTH_SHORT).show();
                addNewItemEditText.setText("");
            }
        }
    }
};

然后,你可以通过在 EditText 中输入标题并点击 Button 来添加新项目。

读取数据

要从数据库读取数据,你可以使用 query() 方法,如果你偏好原始 SQL,也可以使用 rawQuery() 方法。

我们将从原始 SQL 查询开始,当你想要从数据库中获取数据时,你需要创建 SELECT 语句。

SELECT * FROM table_name;

此语句用于从特定表中选择所有列和所有行。如果你想限制输出列,可以在 SELECT 和 FROM 之间添加列名,如下所示

SELECT column_name,column_name FROM table_name;

你可以使用 rawQuery() 方法来执行原始 SQL 查询语句。

Cursor rawQuery(String sql, String[] selectionArgs) - 用于查询数据,如果你愿意,可以放入条件参数(需要在查询中编写条件,并使用问号 (?) 作为参数,例如 SELECT * FROM student where score > ?)。

所有查询方法都返回 Cursor 对象作为结果。Cursor 对象是查询的结果集,它将数据存储在缓冲区/缓存中的某个位置,并且你一次只能获取一行数据。

    public Cursor getItemsWithRawQuery()
    {
        SQLiteDatabase db = getReadableDatabase();
        String queryCommand = String.format("SELECT * FROM %s", ShoppingListContract.ItemEntry.TABLE_NAME);
        Cursor cursor = db.rawQuery(queryCommand,null);
        return cursor;
    }

警告:在你不想访问 Cursor 对象中的数据之前,不要关闭 SQLiteDatabase 对象。

我在 ShoppingListDatabaseHelper 中创建了一个新方法,该方法用于检索数据库中的所有 Items。它返回一个标准的 Cursor 对象。接下来,我们将创建与 getItemsWithRawQuery() 方法功能相同的方法,但改为使用 SQLiteDatabase 提供的 query() 方法。

    public Cursor getItemsWithQueryMethod()
    {
        SQLiteDatabase db = getReadableDatabase();
        Cursor cursor = db.query(ShoppingListContract.ItemEntry.TABLE_NAME,null,null,null,null,null,null);
        return cursor;
    }

SQLiteDatabase 的 query() 方法用于从数据库查询数据,你也可以在此方法中加入条件、分组和排序。query() 方法有很多重载,但我们将使用一个简单的。

Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)

  • table - 你要查询的表名。
  • columns - 你要获取的列名数组,如果想要所有列,则传递 null 值。
  • selection - 如果要选择满足条件的某些行,请在此处输入 SQL 语句的 WHERE 子句。
  • selectionArgs - 你可以在 selection 中使用问号 (?),每个 ? 将被 selectionArgs 中的值替换。
  • groupBy - 如果要对数据进行分组,请在此处输入 SQL GROUP BY 子句。
  • having - 如果需要,请输入 SQL HAVING 子句。
  • orderBy - 如果要对数据进行排序,请在此处输入 SQL ORDER BY 子句。
  • limit - 如果要限制结果数量,可以在此处输入 LIMIT 子句。

获取 Cursor 对象后,你需要迭代它以获取每一行的数据。

Cursor cursor = shoppingListDatabaseHelper.getItemsWithRawQuery();
while(cursor.moveToNext()){
    Log.d("result",  cursor.getString(cursor.getColumnIndex(ShoppingListContract.ItemEntry.COLUMN_NAME_TITLE))+"");
}
cursor.close();

上面的代码将在 logcat 中打印 item title,如下所示

09-13 10:49:41.762      370-370/me.vable.android.datastorage D/result﹕ Title
09-13 10:49:41.762      370-370/me.vable.android.datastorage D/result﹕ Title

你会看到 Cursor 有一个 moveToNext() 方法,用于将游标移动到下一个项,然后你可以读取数据。

注意:Cursor 对象的第一位置没有数据,在使用之前需要先移动它;

  • boolean move(int offset) - 从当前位置相对移动 Cursor,如果移动成功则返回 true。
  • boolean moveToFirst() - 将 Cursor 移动到第一个有数据的行,如果移动成功则返回 true。
  • boolean moveToLast() - 将 Cursor 移动到最后一个有数据的行,如果移动成功则返回 true。
  • boolean moveToNext() - 将 Cursor 移动到下一行,如果移动成功则返回 true。
  • boolean moveToPosition(int position) - 将 Cursor 移动到指定行,如果移动成功则返回 true。
  • boolean moveToPrevious() - 将 Cursor 移动到上一行,如果移动成功则返回 true。
  • byte[] getBlob(int columnIndex) - 将特定列的数据作为字节数组获取。
  • int getColumnCount() - 获取列的总数。
  • int getColumnIndex(String columnName) - 获取列的索引。如果列不存在,则返回 -1。
  • String getColumnName(int columnIndex) - 获取指定索引的列名。
  • String[] getColumnNames() - 获取所有列名。
  • int getCount() - 获取 Cursor 中的行数。
  • int getPosition() - 获取 Cursor 的当前位置。
  • double getDouble(int columnIndex) - 将特定列的值作为 double 获取。
  • float getFloat(int columnIndex) - 将特定列的值作为 float 获取。
  • int getInt(int columnIndex) - 将特定列的值作为 int 获取。
  • long getLong(int columnIndex) - 将特定列的值作为 long 获取。
  • short getShort(int columnIndex) - 将特定列的值作为 short 获取。
  • String getString(int columnIndex) - 将特定列的值作为 string 获取。
  • int getType(int columnIndex) - 获取特定列中数据的类型。

你会看到,所有用于获取特定列值的get方法都需要一个索引作为参数。索引在哪里获取?索引是你为 SQL 的 SELECTION 子句提供的列的顺序,如果你使用 *,列的顺序将与表定义相同。当列索引不严格时,你应该使用 getColumnIndex() 方法来获取它。

尝试将 Cursor 中的数据传递给 Item 对象。

cursor.moveToFirst();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
Item item = new Item();
int id = cursor.getInt(cursor.getColumnIndex(ShoppingListContract.ItemEntry._ID));
item.setId(id);
String title = cursor.getString(cursor.getColumnIndex(ShoppingListContract.ItemEntry.COLUMN_NAME_TITLE));
item.setTitle(title);
int deleted = cursor.getInt(cursor.getColumnIndex(ShoppingListContract.ItemEntry.COLUMN_NAME_DELETED));
item.setDeleted(deleted==1);
int bought = cursor.getInt(cursor.getColumnIndex(ShoppingListContract.ItemEntry.COLUMN_NAME_BOUGHT));
item.setBought(bought == 1);
String createDateString = cursor.getString(cursor.getColumnIndex(ShoppingListContract.ItemEntry.COLUMN_NAME_CREATED_DATE));
try{
    Date createDate = dateFormat.parse(createDateString);
    item.setCreateDate(createDate);
}catch (ParseException e)
{
    e.printStackTrace();
}
Log.d("item",  item.toString());

这是结果

671  11937-11937/me.vable.android.datastorage D/item﹕ Item{id=28, title='Title', bought=false, deleted=false, createDate=Sat Sep 13 11:25:57 GMT+07:00 2014}

在示例中,你会看到我使用了 == 运算符将 int 值转换为 boolean 值,并使用 SimpleDateFormatcreateDate 转换回来。

你也可以将 Cursor 中的所有数据转换为 Item 对象列表,但不推荐这样做,因为对象列表会消耗大量内存,而 Cursor 则不会。

 

接下来,我们将学习如何在 ListView 中显示查询结果。

首先,你必须将 ListView 添加到 Activity 的布局中,如下所示。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="me.vable.android.datastorage.Task2ShoppingListActivity">

    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_above="@+id/linearlayout_add_new_item"/>

    <LinearLayout
        android:id="@+id/linearlayout_add_new_item"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentBottom="true"
        >

        <EditText
            android:id="@+id/edittext_add_new_item"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="1"/>

        <Button
            android:id="@+id/button_add_new_item"
            android:text="ADD"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            />

    </LinearLayout>

</RelativeLayout>

然后打开 Activity 文件,获取 ListView 的实例。

ListView listView;
@Override
protected void onCreate(Bundle savedInstanceState) {
    ....
    listView = (ListView) findViewById(android.R.id.list);
}

之后,从数据库查询所有 Items。

Cursor itemCursor = databaseHelper.getItemsWithQueryMethod();

现在,你需要为 ListView 创建一个适配器。最简单的方法是使用 SimpleCursorAdapter,如下所示。

SimpleCursorAdapter cursorAdapter=
        new SimpleCursorAdapter(
                this,
                android.R.layout.simple_list_item_1,
                itemCursor,
                new String[]{ShoppingListContract.ItemEntry.COLUMN_NAME_TITLE},
                new int[]{android.R.id.text1});
listView.setAdapter(cursorAdapter);

我创建了一个 SimpleCursorAdapter,它在布局 android.R.layout.simple_list_item_1 中显示 item title。

SimpleCursorAdapter(Context context, int layoutId, Cursor cursor, String[] columns, int[] viewIds)

  • layoutId - 你可以使用 Android 提供的布局,或创建自己的布局。
  • columns - 你想显示的列名数组。
  • viewIds - 你想用于显示每列数据的视图的 id。(与 columns 顺序相同)

运行应用程序时,你将看到项目列表。

你也可以创建自己的布局并将其应用于 SimpleCursorAdapter

这是我的新列表项布局。

<?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"
    android:padding="16dp">

    <TextView
        android:id="@+id/textview_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        />

    <TextView
        android:id="@+id/textview_create_date"
        android:textColor="@android:color/darker_gray"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceSmall"
        />
</LinearLayout>

然后更改 SimpleCursorAdaptor 的构造函数参数。

SimpleCursorAdapter cursorAdapter =
        new SimpleCursorAdapter(
                this,
                R.layout.listitem_shopping_item,
                itemCursor,
                new String[]{ShoppingListContract.ItemEntry.COLUMN_NAME_TITLE, ShoppingListContract.ItemEntry.COLUMN_NAME_CREATED_DATE},
                new int[]{R.id.textview_title, R.id.textview_create_date});

然后将其移到一个新方法中,如下所示。

SimpleCursorAdapter cursorAdapter;
private void reloadData()
{
    Cursor itemCursor = databaseHelper.getItemsWithQueryMethod();
    if(cursorAdapter==null) {
        cursorAdapter=
                new SimpleCursorAdapter(
                        this,
                        R.layout.listitem_shopping_item,
                        itemCursor,
                        new String[]{ShoppingListContract.ItemEntry.COLUMN_NAME_TITLE, ShoppingListContract.ItemEntry.COLUMN_NAME_CREATED_DATE},
                        new int[]{R.id.textview_title, R.id.textview_create_date});
        listView.setAdapter(cursorAdapter);
    }
    else
    {
        cursorAdapter.swapCursor(itemCursor);
    }
}

swapCursor() 方法用于将适配器中的 Cursor 交换为新的 Cursor。

reloadData() 语句添加到 onCreate() 方法和 onAddButtonClickListener

这是我的完整源代码。

public class Task2ShoppingListActivity extends ActionBarActivity {

    ListView listView;
    EditText addNewItemEditText;
    Button addNewItemButton;
    ShoppingListDatabaseHelper databaseHelper;
    SimpleCursorAdapter cursorAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_task2_shopping_list);

        addNewItemEditText = (EditText) findViewById(R.id.edittext_add_new_item);
        addNewItemButton = (Button) findViewById(R.id.button_add_new_item);

        databaseHelper = new ShoppingListDatabaseHelper(this);
        addNewItemButton.setOnClickListener(onAddButtonClickListener);

        listView = (ListView) findViewById(android.R.id.list);
        reloadData();
    }

    private void reloadData()
    {
        Cursor itemCursor = databaseHelper.getNonDeletedItem();
        if(cursorAdapter==null) {
            cursorAdapter=
                    new SimpleCursorAdapter(
                            this,
                            R.layout.listitem_shopping_item,
                            itemCursor,
                            new String[]{ShoppingListContract.ItemEntry.COLUMN_NAME_TITLE, ShoppingListContract.ItemEntry.COLUMN_NAME_CREATED_DATE},
                            new int[]{R.id.textview_title, R.id.textview_create_date});
            listView.setAdapter(cursorAdapter);
        }
        else
        {
            cursorAdapter.swapCursor(itemCursor);
        }
    }

    View.OnClickListener onAddButtonClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            String newItemTitle = addNewItemEditText.getText().toString().trim();
            if(!newItemTitle.isEmpty())
            {
                Item newItem = new Item();
                newItem.setTitle(newItemTitle);
                long result = databaseHelper.putItemWithInsertMethod(newItem);
                if(result > -1)
                {
                    Toast.makeText(Task2ShoppingListActivity.this,"New item added",Toast.LENGTH_SHORT).show();
                    addNewItemEditText.setText("");
                    reloadData();
                }
            }
        }
    };
}

最后,尝试运行应用程序!!

带条件的查询

应用程序中使用的大多数数据查询都有一些条件,例如日期范围、数据状态等。现在,我们将学习如何向查询添加条件。

SELECT <*,[column_names]> FROM table_name WHERE condition1 <AND|OR> condition2 .... ;

这是用于从数据库中选择未标记为已删除的项目的 SELECT 语句。

SELECT * FROM Item WHERE deleted = 0;

你可以使用 AND 或 OR 作为连接词添加多个条件。

SELECT * FROM Item WHERE deleted = 0 AND bought=0;

当你执行上述语句时,数据库将过滤数据并只返回未标记为已删除或已购买的行。

我建议使用 query() 方法,并在条件中使用 ?,然后将真实值作为字符串数组传递给第四个参数。

public Cursor getNonDeletedItem()
{
    SQLiteDatabase db = getReadableDatabase();
    Cursor cursor = db.query(
            ShoppingListContract.ItemEntry.TABLE_NAME,
            null,
            ShoppingListContract.ItemEntry.COLUMN_NAME_DELETED+"=?",
            new String[]{"0"},
            null,
            null,
            null);
    return cursor;
}

在 Activity 中,将 getItemsWithQueryMethod() 替换为 x()

更新数据

当数据更改时,需要使用 SQL UPDATE 语句将更新写入数据库。

UPDATE table_name
SET column1=value1,column2=value2,...
WHERE some_column=some_value;

与其他语句一样,你可以使用原始查询或 SQLiteDatabase 提供的该方法。

对于原始查询,你可以使用 execSQL() 方法来执行 SQL 语句,如下所示。

db.execSQL("UPDATE " +
        ShoppingListContract.ItemEntry.TABLE_NAME +
        " SET " +
        ShoppingListContract.ItemEntry.COLUMN_NAME_BOUGHT +
        "=1 " +
        " WHERE "+
        ShoppingListContract.ItemEntry._ID+"="+id );

Android 提供了 update() 方法来创建和执行 UPDATE 语句。

int update (String table, ContentValues values, String whereClause, String[] whereArgs)

  • values - 你要更新的列名-值的映射。
  • whereClause - 行选择条件,所有选定的行都将被更新。
  • whereArgs - 将替换 whereClause 中的 ? 的值。

此方法将返回一个整数值,即受影响的行数。

public int markAsBought(int id)
{
    SQLiteDatabase db = getWritableDatabase();

    ContentValues contentValues = new ContentValues();
    contentValues.put(ShoppingListContract.ItemEntry.COLUMN_NAME_BOUGHT,1);

    int result = db.update(
            ShoppingListContract.ItemEntry.TABLE_NAME,
            contentValues,
            ShoppingListContract.ItemEntry._ID+"=?",
            new String[]{String.valueOf(id)});

    return result;
}

注意:更新数据需要可写数据库实例。

建议:建议使用 UPDATE 语句来标记数据为已删除,因为删除数据很危险,可能会影响与已删除数据相关的其他数据。

现在,我们将实现滑动标记为已购买的功能。

使用 SimpleCursorAdapter,你无法创建动态列表项布局。要完成滑动标记为已购买的功能,我们需要创建一个自定义 CursorAdapter

首先,创建一个派生自 CursorAdapter 的新类,命名为 ShoppingListCursorAdapter

public class ShoppingListCursorAdapter extends CursorAdapter{

}

实现构造函数、newView() 和 bindView() 方法。

public class ShoppingListCursorAdapter extends CursorAdapter implements View.OnTouchListener {
    public ShoppingListCursorAdapter(Context context, Cursor c) {
        super(context, c, false);
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
        return null;
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {

    }
}

然后创建一个 LayoutInflator 实例。

private LayoutInflater layoutInflater;
public ShoppingListCursorAdapter(Context context, Cursor c) {
    super(context, c, false);
    layoutInflater = LayoutInflater.from(context);
}

之后,在 newView() 方法中,inflater 布局文件。

@Override
public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
    View view = layoutInflater.inflate(R.layout.listitem_shopping_item, viewGroup, false);
    return view;
}

newView() 方法用于构造 View,类似于 BaseAdaptergetView(),但你无法在此方法中访问项实例。

然后完成 bindView() 方法,bindView() 方法用于将数据应用于 View

    @Override
    public void bindView(View view, Context context, Cursor cursor) {

        String title = cursor.getString(cursor.getColumnIndex(ShoppingListContract.ItemEntry.COLUMN_NAME_TITLE));
        int bought = cursor.getInt(cursor.getColumnIndex(ShoppingListContract.ItemEntry.COLUMN_NAME_BOUGHT));
        String createDateString = cursor.getString(cursor.getColumnIndex(ShoppingListContract.ItemEntry.COLUMN_NAME_CREATED_DATE));

        TextView titleTextView = (TextView) view.findViewById(R.id.textview_title);
        TextView createDateTextView = (TextView) view.findViewById(R.id.textview_create_date);

        titleTextView.setText(title);
        createDateTextView.setText(createDateString);

        if(bought == 1){
            titleTextView.setPaintFlags(Paint.STRIKE_THRU_TEXT_FLAG);
        }else{
            titleTextView.setPaintFlags(Paint.LINEAR_TEXT_FLAG);
        }

    }

我检查了 bought 状态,如果项目已购买,则在项目标题上添加删除线。

然后回到类签名,添加 implements View.OnTouchListener 并覆盖 onTouch() 方法,使用此代码片段。

    private float mLastX;
    @Override
    public boolean onTouch(View view, MotionEvent event) {
        float currentX = event.getX();
        TextView titleTextView = (TextView) view.findViewById(R.id.textview_title);
        switch(event.getAction()) {
            case  MotionEvent.ACTION_DOWN:
                mLastX = currentX;
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL:
                if (currentX > mLastX + view.getWidth() / 5) {
                    titleTextView.setPaintFlags(titleTextView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
                }
                break;
        }
        return true;
    }

OnTouchListener 用于检测特定视图上的触摸事件,我们将使用它来检测列表项上的滑动以将项目标记为已购买。

OnTouchListener 应用于视图。

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
        View view = layoutInflater.inflate(R.layout.listitem_shopping_item, viewGroup, false);
        view.setOnTouchListener(this);
        return view;
    }

现在,我们的 CursorAdapter 已完成。接下来,转到 Activity 并将 SimpleCursorAdapter 替换为 ShoppingListCursorAdapter

    private void reloadData()
    {
        Cursor itemCursor = databaseHelper.getNonDeletedItem();
        if(cursorAdapter ==null) {
            cursorAdapter =
                    new ShoppingListCursorAdapter(this,itemCursor);
            listView.setAdapter(cursorAdapter);
        }
        else
        {
            cursorAdapter.swapCursor(itemCursor);
        }
    }

运行应用程序并尝试滑动项目!!

 

似乎有效!!但是我们需要更改项目状态并更新数据库。

ShoppingListCursorAdapter 中创建 ShoppingListDatabaseHelper 实例。

    private ShoppingListDatabaseHelper databaseHelper;
    public ShoppingListCursorAdapter(Context context, Cursor c) {
        ...
        databaseHelper = new ShoppingListDatabaseHelper(context);
    }

之后,转到 bindView() 方法,获取项目的 id 并将其设置为 titleTextView 的 tag。

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        ....
        int id = cursor.getInt(cursor.getColumnIndex(ShoppingListContract.ItemEntry._ID));
        titleTextView.setTag(id);
    }

接下来,转到 onTouch() 方法的 case MotionEvent.ACTION_UP。添加用于标记项目为已购买的代码。

case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL:
        if (currentX > mLastX + view.getWidth() / 5) {
            titleTextView.setPaintFlags(titleTextView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
            databaseHelper.markAsBought((Integer)titleTextView.getTag());
        }
        break;

在将更新保存到数据库后,ListView 不会自动刷新。你需要重新查询数据。Cursor 对象中有 requery() 方法,但它自 API 级别 11 起已弃用,你需要手动查询数据并使用 swapCursor() 来更改适配器的 Cursor。

case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL:
        if (currentX > mLastX + view.getWidth() / 5) {
            titleTextView.setPaintFlags(titleTextView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
            databaseHelper.markAsBought((Integer)titleTextView.getTag());
            Cursor itemCursor = databaseHelper.getNonDeletedItem();
            swapCursor(itemCursor);
        }
        break;

现在,再次尝试运行应用程序!!

你会发现,在你滑动项目后,已购买状态已保存。

接下来,我们将实现删除项目功能。当用户在选项菜单中选择删除已购买的项目选项时,我们将仅将项目标记为已删除。

如果你还没有选项菜单,现在就创建一个,然后添加删除已购买的项目选项。

<?xml version="1.0" encoding="utf-8"?>

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/menu_delete_bought"
        android:title="DELETE BOUGHT ITEMS"
        />
</menu>

将选项菜单添加到 Activity。

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.activity_task2_shopping_list,menu);
    return super.onCreateOptionsMenu(menu);
}

然后创建数据库帮助类中的删除已购买项目函数。

public int markBoughtItemsAsDeleted()
{
    SQLiteDatabase db = getWritableDatabase();
    ContentValues contentValues = new ContentValues();
    contentValues.put(ShoppingListContract.ItemEntry.COLUMN_NAME_DELETED,1);
    int result = db.update(
            ShoppingListContract.ItemEntry.TABLE_NAME,
            contentValues,
            ShoppingListContract.ItemEntry.COLUMN_NAME_BOUGHT+"=?",
            new String[]{String.valueOf(1)});
    return result;
}

在按下菜单后调用此函数,然后重新加载数据。

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId())
    {
        case R.id.menu_delete_bought :{
            if(databaseHelper != null)
            {
                databaseHelper.markBoughtItemsAsDeleted();
                reloadData();
            }
        }
    }
    return super.onOptionsItemSelected(item);
}

运行应用程序!!

删除数据

你也可以通过使用 SQL DELETE 语句永久从数据库中删除数据,但如果非必要,不建议删除数据。

DELETE FROM table_name [WHERE some_column=some_value;]

SQL DELETE 语句非常简单,只需要表名和条件。

警告:你可以不提供条件而使用 DELETE 语句,但表中的所有数据都将被删除。

与其他 SQL 语句一样,你可以使用 execSQL() 方法来执行原始查询,或使用 delete() 方法,这是 SQLiteDatabase 提供的。

我们应用程序的最后一个功能是删除数据库中的所有项目,我们将使用 SQL DELETE 语句来构建此功能。

DELETE FROM Item;

你可以使用 SQL DELETE 语句而不带任何条件,因为我们想删除所有项目。

delete() 方法

int delete (String table, String whereClause, String[] whereArgs)

delete() 方法需要 3 个参数:table、whereClausewhereArgs。如果你不想使用条件,可以将 null 值作为 whereClausewhereArgs 参数传递。返回受影响的行数或 -1。

回到应用程序,创建用于删除所有项目的函数。

public int deleteAllItems()
{
    SQLiteDatabase db = getWritableDatabase();
    int result = db.delete(ShoppingListContract.ItemEntry.TABLE_NAME,null,null);
    return result;
}

然后创建删除所有项目的菜单。

<item
    android:id="@+id/menu_delete_all"
    android:title="DELETE ALL"
    />

之后,返回 Activity 并实现删除所有功能。

case R.id.menu_delete_all :{
        if(databaseHelper != null)
        {
            databaseHelper.deleteAllItems();
            reloadData();
        }
    }

再次运行应用程序!!

完成!!现在购物清单应用程序已完成,你可以自定义样式并在商店中发布它!

警告:删除前应询问用户确认。

共享偏好设置

Android 提供了一个称为 SharedPreferences 的键值对存储。SharedPreferences 允许你保存和检索原始数据类型的键值对。你可以创建任意数量的 SharedPreferences 文件,并且像其他文件一样,你可以将其设为私有或共享。

要检索 SharedPreferences 对象,你可以使用 Context 类中的 getSharedPreferences()getPreferences() 方法。

  • SharedPreferences getSharedPreferences (String name, int mode) - 通过传递名称和创建模式来获取特定的共享偏好设置。
  • SharedPreferences getPreferences (int mode) - 使用特定模式获取默认共享偏好设置,类似于 getSharedPreferences() 方法,但将 Activity 的类名作为偏好设置的名称传递。

SharedPreferences 模式

  • MODE_PRIVATE - 创建只能被此应用程序访问的文件。
  • MODE_WORLD_READABLE - 创建任何应用程序都可以读取的文件。
  • MODE_WORLD_WRITEABLE - 创建任何应用程序都可以写入的文件。
  • MODE_MULTI_PROCESS - 创建允许应用程序的多个进程同时读写共享偏好设置文件的文件。

写入数据

要将数据写入共享偏好设置文件,你需要通过调用 SharedPreferences 对象的 edit() 方法来获取 SharedPreferences.Editor

之后,你可以使用 Editor 提供的 putString()、putDouble() 等方法将任何数据放入 Editor。

然后,你需要调用 Editor 实例上的 commit() 方法来将值提交写入共享偏好设置。

SharedPreferences sharedPreference = getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreference.edit();
editor.putInt("app_usage_count", count);
editor.commit();

读取数据

要从共享偏好设置文件读取数据,你可以使用 SharedPreferences 对象上的 getInt()、getString() 等方法。你还可以提供默认值,如果键不存在,则返回默认值。

SharedPreferences sharedPreference = getPreferences(Context.MODE_PRIVATE);
int defaultValue = 0;
int appUsageCount = sharedPreference.getInt("app_usage_count", defaultValue);

任务 3

创建一个空应用程序,其机制是在用户使用 7 次后提醒用户评价该应用程序。

要完成此应用程序,你需要计算用户进入应用程序的次数,并在每次创建 Activity 时增加该数字。

首先,创建一个新应用程序或新 Activity,并从共享偏好设置中读取 "app_usage_count"。

SharedPreferences sharedPreference = getPreferences(Context.MODE_PRIVATE);

int appUsageCount = sharedPreference.getInt("app_usage_count",0);

如果键未找到,将返回 0 值。然后增加该值并将其保存回共享首选项

appUsageCount++;
SharedPreferences.Editor editor = sharedPreference.edit();
editor.putInt("app_usage_count", appUsageCount);
editor.commit();

之后,检查 appUsageCount 的值,如果大于或等于 7,则提醒用户对应用进行评分

if(appUsageCount>=7)
{
    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setTitle("Rate this app");
    builder.setMessage("Please rate our app if you do love it :)");
    builder.setPositiveButton("Rate the app",new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialogInterface, int i) {
            final Uri uri = Uri.parse("market://details?id=" + getApplicationContext().getPackageName());
            final Intent rateAppIntent = new Intent(Intent.ACTION_VIEW, uri);
            startActivity(rateAppIntent);
        }
    });
    builder.setNegativeButton("No, thanks", null);
    builder.show();
}

现在,尝试运行该应用,关闭并重新打开 7 次

您会发现,在 7 次之后您仍然会收到警报对话框,因为您还没有检查评分状态。尝试添加更多数据来检查这一点

SharedPreferences sharedPreference = getPreferences(Context.MODE_PRIVATE);

int appUsageCount = sharedPreference.getInt("app_usage_count",0);
int remindLaterCount = sharedPreference.getInt("app_rate_remind_later_count", 0);
boolean rated = sharedPreference.getBoolean("app_rated", false);

appUsageCount++;
remindLaterCount++;
SharedPreferences.Editor editor = sharedPreference.edit();
editor.putInt("app_usage_count", appUsageCount);
editor.putInt("app_rate_remind_later_count", remindLaterCount);
editor.commit();

if(!rated && remindLaterCount>=7)
{
    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setTitle("Rate this app");
    builder.setMessage("Please rate our app if you do love it :)");
    builder.setPositiveButton("Rate the app",new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialogInterface, int i) {
            SharedPreferences sharedPreference = getPreferences(Context.MODE_PRIVATE);
            SharedPreferences.Editor editor = sharedPreference.edit();
            editor.putBoolean("app_rated", true);
            editor.commit();

            final Uri uri = Uri.parse("market://details?id=" + getApplicationContext().getPackageName());
            final Intent rateAppIntent = new Intent(Intent.ACTION_VIEW, uri);
            startActivity(rateAppIntent);
        }
    });
    builder.setNegativeButton("No, thanks", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialogInterface, int i) {
            SharedPreferences sharedPreference = getPreferences(Context.MODE_PRIVATE);
            SharedPreferences.Editor editor = sharedPreference.edit();
            editor.putInt("app_rate_remined_later_count", 0);
            editor.commit();
        }
    });
    builder.show();
}

我在共享首选项中添加了 2 个键值对,用于检测评分状态以及在用户拒绝时重新计数。

现在,您将此机制添加到您的应用程序中,以提高您的应用评分和评论。

Web 服务

大多数现代应用程序都从互联网消耗数据并将一些数据发送回服务器。服务器端的数据服务称为 Web 服务,它可能是 SOAP、WCF、REST 或移动设备可以与之交互的任何其他服务。在本文中,我们将重点介绍简单的 REST/HTTP 请求,因为它是移动应用程序流行的服务。

要连接互联网,您需要在应用程序清单中添加 android.permission.INTERNET uses-permission。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="me.vable.android.datastorage" >
    <uses-permission android:name="android.permission.INTERNET" />
    ....
</manifest>

注意: Android 不允许在主线程(UI 线程)中连接互联网,您需要创建一个新线程来完成这项工作。

URLConnection

URLConnection 是从互联网读取/写入数据的最简单方法,您只需传递服务的 URL 即可通过输入流读取数据。

例如

URL url = new URL("http://download.finance.yahoo.com/d/quotes.csv?s=%40%5EDJI,GOOG&f=nsl1op&e=.csv");
URLConnection urlConnection = url.openConnection();
InputStream inputStream = new BufferedInputStream(urlConnection.getInputStream());

在示例中,我创建了 URLConnection 来消耗来自雅虎财经 API 的数据。

HttpURLConnection

HttpURLConnection 源自 URLConnection,它旨在用于从 http 服务发送和接收数据。

要获取 HttpURLConnection 实例,您可以调用 url.openConnection() 并将其转换为 HttpURLConnection,然后您还可以添加请求头和请求体。将响应数据读取为流并在使用后关闭连接。

例如

URL url = new URL("http://www.android.com/");
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
try {
    InputStream in = new BufferedInputStream(urlConnection.getInputStream());
    readStream(in);
    finally {
        urlConnection.disconnect();
    }
}

任务 4

通过消耗来自 openweathermap.org 的数据来创建天气预报应用程序。唯一需要的功能是显示您家乡的 7 天天气预报。

服务 URL 是

http://api.openweathermap.org/data/2.5/forecast/daily?q=YOUR_HOME_TOWN&mode=json&units=metric&cnt=7

只需将 YOUR_HOME_TOWN 替换为您家乡的名称,例如泰国曼谷桐。这是我的

http://api.openweathermap.org/data/2.5/forecast/daily?q=Bangbuathong,Thailand&mode=jsonl&units=metric&cnt=7

尝试在浏览器中打开 URL,您将看到 JSON 字符串,使用 jsonformatter 格式化它,您将获得更易读的 JSON,如下所示

{
   "cod":"200",
   "message":0.5364,
   "city":{
      "id":"1620917",
      "name":"Bang Bua Thong",
      "coord":{
         "lon":100.424,
         "lat":13.9095
      },
      "country":"Thailand",
      "population":0
   },
   "cnt":7,
   "list":[
      {
         "dt":1410670800,
         "temp":{
            "day":32.01,
            "min":23.69,
            "max":32.8,
            "night":24.22,
            "eve":23.9,
            "morn":32.01
         },
         "pressure":1018.01,
         "humidity":71,
         "weather":[
            {
               "id":501,
               "main":"Rain",
               "description":"moderate rain",
               "icon":"10d"
            }
         ],
         "speed":3.11,
         "deg":254,
         "clouds":20,
         "rain":4.5
      },
      {
         "dt":1410757200,
         "temp":{
            "day":33.72,
            "min":24.33,
            "max":33.83,
            "night":25.1,
            "eve":27.17,
            "morn":24.33
         },
         "pressure":1016.29,
         "humidity":67,
         "weather":[
            {
               "id":500,
               "main":"Rain",
               "description":"light rain",
               "icon":"10d"
            }
         ],
         "speed":2.21,
         "deg":262,
         "clouds":12,
         "rain":2
      },
      {
         "dt":1410843600,
         "temp":{
            "day":33.5,
            "min":24.95,
            "max":33.5,
            "night":25.79,
            "eve":26.65,
            "morn":24.95
         },
         "pressure":1014.44,
         "humidity":65,
         "weather":[
            {
               "id":501,
               "main":"Rain",
               "description":"moderate rain",
               "icon":"10d"
            }
         ],
         "speed":3.03,
         "deg":235,
         "clouds":0,
         "rain":4
      },
      {
         "dt":1410930000,
         "temp":{
            "day":32.52,
            "min":25.17,
            "max":32.52,
            "night":25.35,
            "eve":26.45,
            "morn":25.17
         },
         "pressure":1013.75,
         "humidity":68,
         "weather":[
            {
               "id":500,
               "main":"Rain",
               "description":"light rain",
               "icon":"10d"
            }
         ],
         "speed":3.21,
         "deg":217,
         "clouds":36,
         "rain":1
      },
      {
         "dt":1411016400,
         "temp":{
            "day":29.05,
            "min":25.16,
            "max":29.05,
            "night":25.16,
            "eve":27.2,
            "morn":26.59
         },
         "pressure":1010.73,
         "humidity":0,
         "weather":[
            {
               "id":501,
               "main":"Rain",
               "description":"moderate rain",
               "icon":"10d"
            }
         ],
         "speed":6.82,
         "deg":185,
         "clouds":55,
         "rain":9.03
      },
      {
         "dt":1411102800,
         "temp":{
            "day":30.66,
            "min":24.82,
            "max":30.66,
            "night":25.05,
            "eve":27.16,
            "morn":24.82
         },
         "pressure":1012.78,
         "humidity":0,
         "weather":[
            {
               "id":501,
               "main":"Rain",
               "description":"moderate rain",
               "icon":"10d"
            }
         ],
         "speed":5,
         "deg":170,
         "clouds":28,
         "rain":11.31
      },
      {
         "dt":1411189200,
         "temp":{
            "day":31.31,
            "min":25.07,
            "max":31.31,
            "night":25.19,
            "eve":28.05,
            "morn":25.07
         },
         "pressure":1013.02,
         "humidity":0,
         "weather":[
            {
               "id":501,
               "main":"Rain",
               "description":"moderate rain",
               "icon":"10d"
            }
         ],
         "speed":2.22,
         "deg":200,
         "clouds":73,
         "rain":3.45
      }
   ]
}

我们将重点关注结果列表(list JSON 数组),我们需要获取每天的 dt(日期)、weather.main、weather.icon 和 temp.day 属性。

要解析 JSON,您可以使用 JSONObject 和 JSONArray 类,JSONObject 包含许多用于从 JSON 获取数据的方法,例如 gteBoolean()、getInt()、getJSONArray() 等。

例如

JSONObject forecastJson = new JSONObject(forecastJsonStr);
JSONArray weatherArray = forecastJson.getJSONArray("list")
for (int i = 0; i < weatherArray.length(); i++) {
    JSONObject dayForecast = weatherArray.getJSONObject(i);
    long dateTime = dayForecast.getLong("dt");
}

对于天气图标,在解析结果后,您只会获得图标的名称。您可以从以下 URL 获取图标文件

http://openweathermap.org/img/w/ICON_NAME_HERE.png

例如

http://openweathermap.org/img/w/10d.png

 

AsyncTask

AsyncTask 允许您在后台线程中执行某些操作,然后将结果和进度发布到 UI 线程,而无需手动处理线程和/或 Handlers。当您想使用 AsyncTask 时,您需要创建一个派生自它的类。

例如

private class GetWeatherDataTask extends AsyncTask<URL, Integer, Long> {
    @Override
    protected void onPreExecute() {
        //UI thread operations
    }

    @Override
    protected Long doInBackground(URL... urls) {
        //Background thread operations
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        //UI thread operations
    }

    @Override
    protected void onPostExecute(Long result) {
        //UI thread operations
    }
}

在示例中,您将看到 GetWeatherDataTask 派生自 AsyncTask,并带有 3 个类型参数。

AsyncTask<inputType, progressType, outputType>

  • inputType - 当创建实例时,您将传递给 doInBackground() 方法的参数类型。
  • progressType - 将传递给 onProgressUpdate() 方法的进度类型。
  • outputType - onPostExecute 接受的参数类型,以及 doInBackground() 方法返回的值类型。

AsyncTask 的方法

  • onPreExecute() - 在执行后台任务之前要执行的一些操作。
  • doInBackground() - 要执行的后台任务。
  • onProgressUpdate() - 由 doInBackground() 调用,以将后台任务的进度通知 UI 线程。
  • onPostExecute() - 在执行后台任务之后要执行的一些操作。(UI 线程)

何时使用 AsynTask?

每次需要执行长时间操作时,例如从磁盘或网络读取/写入数据,以避免 UI 阻塞。特别是网络操作,您应该在 AsyncTask 中执行,因为 Android 不允许在主/UI 线程中执行任何网络操作。

现在,我们准备开始构建应用程序了!!

首先,创建您想要的应用程序/活动名称,然后创建一个类来存储天气数据

public class Weather {
    private Date date;
    private String weather;
    private String icon;
    private float temperature;

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public String getWeather() {
        return weather;
    }

    public void setWeather(String weather) {
        this.weather = weather;
    }

    public String getIcon() {
        return icon;
    }

    public void setIcon(String icon) {
        this.icon = icon;
    }

    public float getTemperature() {
        return temperature;
    }

    public void setTemperature(float temperature) {
        this.temperature = temperature;
    }
}

接下来,在活动文件的同一文件中创建 AsyncTask,用于下载数据并更新 UI

private class GetWeatherDataTask extends AsyncTask<String, Void, List<Weather>> {

    @Override
    protected List<Weather> doInBackground(String... strings) {
        return null;
    }

    @Override
    protected void onPostExecute(List<Weather> weathers) {
        super.onPostExecute(weathers);
    }
}

我创建了 GetWeatherDataTask,它派生自 AsyncTask<String, Void, List<Weather>>。为什么我使用 List<Weather> 作为 outputType?因为它易于添加到 ListView 的 Adapter 中。然后我覆盖了 doInBackground()onPostExecute() 方法。

接下来,我们将在 doInBackground() 方法中从服务获取数据

@Override
protected List<Weather> doInBackground(String... strings) {
    URL url = null;
    InputStream inputStream;
    BufferedReader bufferedReader;
    List<Weather> weathers = new ArrayList<Weather>(7);
    try {
        url = new URL("http://api.openweathermap.org/data/2.5/forecast/daily?q=Bangbuathong,Thailand&mode=jsonl&units=metric&cnt=7");
        URLConnection urlConnection = url.openConnection();
        inputStream = new BufferedInputStream(urlConnection.getInputStream());
        bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                

    } catch (MalformedURLException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return weathers;
}

我已将 Input stream 与 InputStreamReaderBufferedReader 链接起来,以便将数据作为字符串读取。

注意:您需要处理 MalformedURLExceptionIOException,请注意,您不能在后台线程中抛出任何异常。

注意:您可以将整数作为 ArrayList 构造函数的参数传递,以定义数组的预期大小。

接下来,我们将读取所有数据并将其连接成一个字符串

StringBuffer stringBuffer = new StringBuffer();
String line = null;
while((line = bufferedReader.readLine()) != null)
{
    stringBuffer.append(line);
}
String data = stringBuffer.toString();
bufferedReader.close();         
inputStream.close();

之后,我们将数据解析为 JSON 并将其转换为 Weather 对象列表

SimpleDateFormat format = new SimpleDateFormat("E, MMM d");
JSONObject forecastJson = new JSONObject(data);
JSONArray weatherArray = forecastJson.getJSONArray("list");

for (int i = 0; i < weatherArray.length(); i++) {
    JSONObject dayForecast = weatherArray.getJSONObject(i);

    long dateTime = dayForecast.getLong("dt");
    Date date = new Date(dateTime * 1000);

    float temperature = (float) dayForecast.getJSONObject("temp").getDouble("day");
                
    String weatherString = dayForecast.getJSONArray("weather").getJSONObject(0).getString("main");

    String icon = dayForecast.getJSONArray("weather").getJSONObject(0).getString("icon");
                    
    Weather weather = new Weather();
    weather.setDate(date);
    weather.setTemperature(temperature);
    weather.setWeather(weatherString);
    weather.setIcon(icon);
                
    weathers.add(weather);
}

JSON 返回的日期是 long 值,您需要将其解析为 Date 对象。

注意:不要忘记处理 JSONException

之后,我们将 ListView 放入布局中

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="me.vable.android.datastorage.Task4WeatherForecastActivity">

    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</RelativeLayout>

然后创建列表项布局

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:padding="16dp">

    <ImageView
        android:id="@+id/imageview_icon"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:src="@drawable/ic_rain"
        android:layout_centerVertical="true"
        />

    <LinearLayout
        android:id="@+id/layout_weather"
        android:layout_marginLeft="16dp"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_toRightOf="@+id/imageview_icon"
        android:gravity="center"
        android:orientation="vertical">

        <TextView
            android:id="@+id/textview_date"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="15 Sep 14"
            android:textSize="24dp"/>

        <TextView
            android:id="@+id/textview_weather"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Rain"
            android:textSize="28dp"/>
    </LinearLayout>

    <TextView
        android:id="@+id/textview_temperature"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:layout_centerVertical="true"
        android:gravity="center"
        android:text="33°C"
        android:textSize="36dp"/>

</RelativeLayout>

之后,为 ListView 创建 Adapter

public class WeatherListAdapter extends BaseAdapter {

    SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMM yy");
    List<Weather> weatherList = new ArrayList<Weather>(7);
    Context context;
    public WeatherListAdapter(Context context)
    {
        this.context = context;
    }

    public void replaceData(List<Weather> weatherList)
    {
        this.weatherList.clear();
        notifyDataSetChanged();
        this.weatherList.addAll(weatherList);
        notifyDataSetChanged();
    }

    @Override
    public int getCount() {
        return weatherList.size();
    }

    @Override
    public Weather getItem(int i) {
        return weatherList.get(i);
    }

    @Override
    public long getItemId(int i) {
        return 0;
    }

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        if(view == null)
        {
            view = LayoutInflater.from(context).inflate(R.layout.listitem_weather,null);
        }

        Weather weather = getItem(i);

        TextView dateTextView = (TextView) view.findViewById(R.id.textview_date);
        TextView weatherTextView = (TextView) view.findViewById(R.id.textview_weather);
        TextView temperatureTextView = (TextView) view.findViewById(R.id.textview_temperature);
        ImageView iconImageView = (ImageView) view.findViewById(R.id.imageview_icon);

        dateTextView.setText(dateFormat.format(weather.getDate()));
        weatherTextView.setText(weather.getWeather());
        temperatureTextView.setText(String.format("%.0f°C",weather.getTemperature()));

        return view;
    }
}

在 adapter 中,我提供了一个 replaceData() 方法,用于替换数据集。此 adapter 几乎完成,但需要实现图标图像。

接下来,转到 activity。获取 ListView 的实例,创建 WeatherListAdapter 实例并将其应用于 ListView

WeatherListAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_task4_weather_forecast);

    ListView listView = (ListView) findViewById(android.R.id.list);
    adapter = new WeatherListAdapter(this);
    listView.setAdapter(adapter);
}

然后转到 AsyncTaskonPostExecute,添加语句将数据设置到 ListView

@Override
protected void onPostExecute(List<Weather> weathers) {
    super.onPostExecute(weathers);
    adapter.replaceData(weathers);
}

转到 onCreate() 方法执行 GetWeatherDataTask

protected void onCreate(Bundle savedInstanceState) {
    ....
    new GetWeatherDataTask().execute();
}

运行应用程序!!

显示来自互联网的图片

在 Android 中,您无法将 ImageView 的源设置为互联网 URL,您需要读取它并解析为位图,然后将其设置为 ImageView

您也可以使用 URLConnectionAsyncTask 下载图像,并使用 BitmapFactory 将其转换为 Bimap 对象。

例如

class ImageDownloadTask extends AsyncTask<String,Void,Bitmap>{

    ImageView imageView;

    public ImageDownloadTask(ImageView imageView)
    {
        this.imageView = imageView;
    }

    @Override
    protected Bitmap doInBackground(String... strings) {
        Bitmap bitmap = null;
        String urlString = strings[0];
        URL url = null;
        try {
            url = new URL(urlString);
            bitmap = BitmapFactory.decodeStream(url.openConnection().getInputStream());
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);
        //set the image to imageview
        imageView.setImageBitmap(bitmap);
    }
}

此任务将从给定 URL 下载图像并将其设置到 ImageView。

消耗互联网图像的列表项

坏消息是 ListView 总是会回收不再显示的 View,这可能会导致图像显示无效,例如雨天图标但该行天气是晴天:(

对解决这个问题有什么想法吗?我认为 View tag 在这种情况下可能很有用。将 image view 的 tag 设置为图像的 url,下载后,如果 tag 仍然等于图像 url,则将图像设置到 image view。

我已经修改了 ImageDownloadTask 以接受构造函数中的 ImageView

class ImageDownloadTask extends AsyncTask<String,Void,Bitmap>{

    ImageView imageView;
    String urlString;
    public ImageDownloadTask(ImageView imageView)
    {
        this.imageView = imageView;
    }

    @Override
    protected Bitmap doInBackground(String... strings) {
        Bitmap bitmap = null;
        urlString = strings[0];
        URL url = null;
        try {
            url = new URL(urlString);
            bitmap = BitmapFactory.decodeStream(url.openConnection().getInputStream());
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);
        if(imageView.getTag().equals(urlString)) {
            imageView.setImageBitmap(bitmap);
        }
    }
}

尝试在 adapter 中使用它

public class WeatherListAdapter extends BaseAdapter {
    ....

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        ....

        String iconUrl = String.format("http://openweathermap.org/img/w/%s.png", weather.getIcon());
        iconImageView.setTag(iconUrl);
        new ImageDownloadTask(iconImageView)
                .execute(iconUrl);

        return view;
    }

    class ImageDownloadTask extends AsyncTask<String,Void,Bitmap>{

        ....
    }
}

再次运行应用程序!!

它起作用了吗?是的,但当您的 ListView 消耗大量项目的数据时,它会使您的应用程序变慢或崩溃,因为它每次都会重新加载图像,并且在不再需要该图像时无法取消请求。

Picasso

Picasso 是解决所有与图像相关问题的答案,它将帮助您从互联网、存储和资源加载图像。它还帮助您管理图像缓存。

要在 Android Studio 中使用 Picasso 库,您需要在 build.gradle 中添加 compile 依赖项。

打开 build.gradle 文件(在 app 目录中)

接下来,查看依赖项并添加此语句

compile 'com.squareup.picasso:picasso:+'

单击编辑器右上角的 Sync Now 按钮

现在,您可以使用 Picasso。您可以在可以获取 Context 对象的任何地方使用 Picasso :)

Picasso.with(context).load(url).into(view);

这是使用 Picasso 库的简单方法,如果您想了解更多,可以查看 官方 Picasso Github 页面

转到 adapter 并删除 AsyncTask,然后像这样编辑 getView() 方法

@Override
public View getView(int i, View view, ViewGroup viewGroup) {
    if(view == null)
    {
        view = LayoutInflater.from(context).inflate(R.layout.listitem_weather,null);
    }

    Weather weather = getItem(i);

    TextView dateTextView = (TextView) view.findViewById(R.id.textview_date);
    TextView weatherTextView = (TextView) view.findViewById(R.id.textview_weather);
    TextView temperatureTextView = (TextView) view.findViewById(R.id.textview_temperature);
    ImageView iconImageView = (ImageView) view.findViewById(R.id.imageview_icon);

    dateTextView.setText(dateFormat.format(weather.getDate()));
    weatherTextView.setText(weather.getWeather());
    temperatureTextView.setText(String.format("%.0f°C",weather.getTemperature()));

    String iconUrl = String.format("http://openweathermap.org/img/w/%s.png", weather.getIcon());
    Picasso.with(context).load(iconUrl).into(iconImageView);

    return view;
}

现在运行应用程序!!

现在,我们的应用程序已完成。但是,您应该不断学习,以便将来构建更出色的应用程序。

关注点

在本文中,您创建了 4 个应用程序,每个应用程序都将帮助您理解每种存储类型的概念。有时您应该考虑第三方库,它将帮助您简化复杂的问题。

希望您享受您的 Android 应用程序开发生涯。

历史

- 2014/08/14 初次发布

© . All rights reserved.