Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Hosted-services / storage

Android Data Storage

4.57/5 (9 votes)
14 Sep 2014CPOL49 min read 48.4K   1.4K  
Lean about how to manage the data

Index

Introduction

Data Storage is the important thing that you should to learn when you want to buil an application. On the Android platform you can store the data in files, databases, or shared preferences in the device storage and you can also store the data in the cloud by using the external APIs such as Dropbox, Parse, etc.

Background

When you create the application you will found that you need to store some data such as user data, images, etc. that is the topic that we will learn in this article. To learn this article you need to know the basic of Android Application Development.

Let's Start

Today the data is the most powerful fuel that drive your application ahead, you need to know that how can you manage the data? How to store them? to provide the best user experiences. There are many options that you can store the data such as files, databases, shared preferences, etc.

The Files

Files is the simplest way to store the data. It's very flexible, you can design your own structure or using the standard format such as XML, JSON, etc. and you can also serialize the instance into the file directly. Android allow you to save the file on both internal storage and external storage (maybe removable or not).

Files

When you want to write something to the file you need to create a new one or open an existing one then write the data that you want and close it after use. Java provides File class that represent the file or directory pathname, you can use it to create a new one, open the existing one, remove the file, create the directory, etc.

Image 1

Sample of File operate in Java:

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

The results of the code above:

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

You will see that I have instantiate a File with the pathname "Sample.txt" then I check for the existence of it and Java tell me that file is not yet exist. I created it and check again and Java tell me there is a file that I call after that I check for read write permission. The getPath method returns me a path that I give it on construct, the getAbsolutePath method returns me a full part of file from the file system roots, and the getCanonicalPath return the absolute path that format into the system-dependent form. I have try to get the Parent of this file but the null value is returns because there is no parent path in the pathname that I give the object on construct.

Note: When you operate a File, the IOException may throws. So you need to control it by using the Try/Catch Block or throws it away.

Canonical Path vs Absolute Path

Canonical path is an absolute path that mapped to its unique form in a system-dependent way and removed redundant names such as "." and "..".

For example:

Sample.txt is a relative path

.\Sample.txt is a relative path

E:\java\DataStorage\..\DataStorage\Sample.txt is an absolute path but not a canonical path

E:\java\DataStorage\Sample.txt is absolute path is both an absolute path and a canonical path

Note: The Windows OS don't care about the letter case but the *NIX cares (Case Sensitive).

Interesting methods of the File class:

  • boolean canRead() - Returns true if the application can read the file.
  • boolean canWrite() - Returns true if the application can write the data to the file.
  • boolean createNewFile() - Create a file if not yet exist.
  • static File createTempFile() - Used to create the temporary file.
  • boolean delete() - Deletes the file or directory.
  • void deleteOnExit() - Delete the file or directory when application closed.
  • boolean exists() - Check for the existence of file or directory.
  • File getAbsoluteFile() - Get the new File instance that instantiate with absolute pathname.
  • String getAbsolutePath() - Get the absolute pathname of file or directory.
  • File getCanonicalFile() - Get the new File instance that instantiate with canonical pathname.
  • String getCanonicalPath() - Get the canonical pathname of file or directory.
  • String getName() - Get the name of file or directory.
  • String getParent() - Get the pathname of this pathname's parent. Will returns null is no pathname's parent exist.
  • File getParentFile() - Get the File instace of pathname's parent.
  • String getPath() - Get the pathname string.
  • boolean isAbsolute() - Returns true if the pathname is absolute pathname.
  • boolean isDirectory() - Returns true if the pathname is a normal file.
  • boolean isHidden() - Returns true if the pathname is a hidden file.
  • long lastModified() - Returns the time that the file was last modified.
  • long length() - Returns the length of the file.
  • String[] list() - Returns array of strings of the files and directories name in the directory.
  • File[] listFiles() - Return array of File instances of the files and directories in the directory
  • boolean mkdir() - Create the directory.
  • boolean mkdirs() - Create the directory, including any necessary parent directories.
  • boolean renameTo() - Rename the file.
  • URI toURI() - Returns the URI instance of the pathname.

Streams

In the files section you will see that there is no method to write or read the file directly, in the programming when you want to read or write the data to the memory, storage, network, etc. you need to do it via I/O streams. A stream is represent a sequence of data.

Image 2

The I/O streams allow only one-way dataflow that meant you can't use the same stream for both read and write. The application uses an input stream to read the data from only one source at a time, and the application uses an output stream to write the data to only one destination at a time. Streams support many different kinds of data such as bytes, primitive data types, object, etc.

While Android using the Java as a language to develop an application, we will learn about the basic I/O Streams in the Java and then try to use in the Android Application.


Byte Streams

Byte Streams are used to read and write the data as raw bytes. Java provides abstract classed InputStream and OutputStream and many their derived classes such as FileInputStream, ObjectInputStream, etc. In this article we will focus on the FileInputStream and FileOutputStream.

Image 3

Reading data from the Input Stream

To read the data from the source you can use the read() method that provided by the InputStream abstract classes.

 int read()  - Returns the input data as int (0-255) or return -1 if "end of stream" detected or throws IOException when I/O error

Example:

I have a file name a.txt with the content like this:

HELLO

Then I use  FileInputStream to read this file and print the result to the console:

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

I use the while loop to read each byte from the file and then I print the data as binary format, decimal integer, and character. This is results that I got:

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

 

int read(byte[] bytes) - Read the data as bytes array. Returns the number of bytes that read or -1 if "end of stream" detected.

This method has some different from the read() method without parameter. This method require the array of bytes as parameter becauseit will store the result in that array and return the data length as method result.

Example:

Using the same data source file with the example above then I create the array of bytes with length 10 and pass it as parameter of read() method:

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

Each byte that read will store in the array that give it in parameter. In each round of the loop, the array will be filled from the 0 index to the length of data that read. Then I copy a part of array with only length that read(bytes) method returns and print the data length and the binary data and its value as character on the screen. This is results that I got:

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

5 bytes was returned.

In each round of the loop I give the same array as read(bytes) method parameter, the data from previous round will override by the new one in each round. For example, you have a file which text "HELLO WORLD" as content. You give the array of bytes with length 5 to the read(bytes), in the first round you got the result as [H,E,L,L,O] and 5 was returns ,the second round you will get [ ,W,O,R,L] and 5 was returns, and the last round you will get [D,W,O,R,L] and 1 was returns. You will see that W,O,R,L that is the result of the second round still store in the array.

 

int read(byte[] bytes, int offset, int length) - Read the data to the bytes array at the specific offset and specific length as bytes array. Returns the number of bytes that read or -1 if "end of stream" detected.

This overload is working like the read(bytes) method but you can specific the index of array that you want to start to fill the data and the length that you want to read.

Example:

Using the same data source file with the example above. I have create a new variable name totalLength that is used to store the total length that read. In each round of the loop I shift the array start index to the totalLength and read only 3 bytes.

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

And this is the result:

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) 

You will see that the program have read the file for 2 times, the first round got 3 bytes of data and the second round got 2 bytes of data because I have limit the length of data that can read at a time to 3. Then I have print all data that receive from the file [H,E,L,L,O].

Writing data to the Output Stream

To write the data to the destination source you can use the write() method that provided by the OutputStream abstract classes.

write(int) - Write a single byte data into a file.

You need to convert the data into integer before write.

Example:

I want to write the word "HELLO" to the files c.txt. I have convert the word into a integers array [72,69,76,76,79] then use the write(int) to write each character to the file.

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

After run the program I'm open a file c.txt and I see a result like this:

HELLO

Seem hard? You can use the toCharArray() method of String to convert the String to characters array

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

And here is result(c.txt):

HELLO

 

write(byte[] bytes) - Write the bytes array to the files.

Example:

Like the previous example, I want to write the word "HELLO" to the file c.txt but I will use the write(bytes) instead.

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

I have convert the String to byte[] by using the getBytes() method. Then I got the same result as the previous example:

HELLO

 

write(byte[] bytes, int offset, int length) - You can also write the bytes array with specific start index and length.

Example:

I will edited the previous example code to write only String "ELL" to the file.  The letter "E"'s index is 1 and length of "ELL" is 3 thus I will use write(data,1,3) to write the file.

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

And here is result(c.txt):

ELL


Note: If you want to append the data to the end of file you can construct the FileOutputStream by using the constructure FileOutputStream(String fileName, boolean append) or FileOutputStream(Filefile, boolean append)

 

Closing the Stream

After use the Stream you should to close it by using the close() method.

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

Note: Closing a stream when it's no longer needed is very important to avoid the resources leaks. Recommends to close the stream in finally clause of try-catch block to make sure that the stream will close after use.

Now, you have learned about Bytes Stream you will found that you need some extra work to convert the data because these stream support only integer and bytes array. Using the Bytes Stream with the character data is not recommended, you should to use the Character Streams instead.

 

Character Streams

Character Streams are used to read/write the character data from/to the source, Java provides Reader and Writer abstract classes for handler character data stream. In this article we will focus on the FileReader and FileWriter classes. Java internally stores characters in 16-bit character set but the external data source may using another character set, the Character Streams will help you to convert the data from the Java internal character set to local character set.

Reading data from the Reader

There are read() methods like the InputStream that is use to read the character data.

int read() - Returns the single character as an integer value between 0 to 65535 or -1 if "end of stream" detected.

Example:

I use the same source file as the InputStream example (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();
}

The code is similar to the InputStream example code, but using a FileInputReader instead a FileInputStream. Here is the results:

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

Nothing different from the InputStream example results.

I will try to read the file that contain non-ascii characters. I created a file name d.txt and this is a content that I put in this file:

สวัสดี HELLO

There are Thai characters and English characters in this file. Then I try to read it with FileInputReader.

This is what I got:

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

The result is correct!! Now I would like to know the result if I using the 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

Wow the result is incorrect!!

 

int read(char[] chars) - Read the character data as array of characters.

Example:

I will read the file b.txt as characters array with length 10:

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

Note: You can convert the char[] to String by initiate new string with char[] as parameter.

And this is what I got:

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

 

int read(char[] chars, int offset, int length) - Read the character data as array of characters with specific the start index and length.

Example:

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

And here is the results:

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

 

Writing data to the Writer

The Writer abstract class provides the method write() that is used to write the data to the file.

write(int character) - Write a single character to a file.

Example:

I would like to write a String "สวัสดี HELLO" to a file e.txt. Just create the FileWriter and then create a loop to write every character in String to a file.

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

And this is results that I got (e.txt):

สวัสดี HELLO

 

write(char[] chars) - Write all characters in the array to a file.

Example:

From the previous example, we can write the string to a file in the simpler way by passing char[] as write() method parameter.

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

And this is the results (e.txt):

สวัสดี HELLO

 

write(char[] chars, int offset, int length) - Write the array of characters to the file with specific a start index and length.

Example:

As the previous example I would like to write only a word "HELLO" instead a full string "สวัสดี HELLO". I have to put parameters start index and length to the write() method.

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

And this is what I got (e.txt):

HELLO

Note: If you want to append the data to the end of file you can construct the FileWriterby using the constructure FileWriter(String fileName, boolean append) or FileWriter(Filefile, boolean append)

Character stream automatically translates the Java internal format to and from the local character set, however cannot guarantee that the result is correct. And the bad news is you should to avoid using FileReader/FileWriter because you have no control of the file encoding charset.

 

Buffered Streams

Buffered Streams was designed to reduce the cost of native API calling by buffer the input/output data. When you use the Buffered Streams to read the data, it will read the data from the buffer, the native input API is called only when the buffer is empty. In the other hand, when you write the data to Buffered Streams, it will place the data in the buffer, the native write API is called only when the buffer is full.

Java provides the BufferedInputStream/BufferedOutputStream classes for using to create buffered byte streams and BufferedReader/BufferedWriter classes for using to create buffered character streams.

To use the buffered streams in Java you need another non-buffered stream and chain it with buffered stream.

Example:

You can wrap the FileInputStream with the BufferedInputStream.

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

Set the buffer size

If you want to set the buffer size, you can set it by pass the size as int value to the constructure.

Example:

I would like to set the buffer size of BufferedInputStream to 512 bytes.

Java
bufferedInputStream = new BufferedInputStream(fileInputStream,512);

 

Flushing the Output Streams

When you use the Buffered Output Streams, sometimes you need to force the Output Streams write out a buffer without waiting for it to fill. This is known as flushing the buffer. Java provides the flush() method to do that works.

Example:

I would like to flush the buffer everytime when I write something to the Stream:

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

Note: Not recommend to flushing the buffer every time when write the data like the example.

There is auto-flush support in some buffered stream such as PrintWriter if you enable the auto-flush function PrintWriter will flush the buffer every time when you call the println() or format() methods.

 

Chaining the Streams

In the example code of Buffered Streams I have connect the BufferedInputStream with the FileInputStream to buffer the input. This is known as chaining or layering the streams.

Image 4

When to chain the streams? We chain the streams because of some specific purpose such as convert the data, buffering, filtering, etc. When you want to add some behaviors to the streams, you should chain them.

 

Data Streams

Want to read and write primitive data? Data Streams make it possible, with the Data Streams you can read and write all primitive type values and String values. All data streams derived from the DataInput interface or the DataOutput interface. In the article we will focus on DataInputStream and DataOutputStream classes.

Example:

I would like to write some primitive data to the file then I try to read it and print to the console.

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

And this is the result on the console:

Nexus 5
299.99
10

Note: If the order of reading in incorrect, you will get the incorrect data.

Note: There are many method to write/read the primitive data you can see it from the suggestion box of your IDE. readUTF and writeUTF are used to read/write the string in UTF-8 Character set

 

Control the Streams Character Set

When Java using the UCS-2 character set to store characters, but the data source may use another character sets. FileReader/FileWriter may can't format the data in the correct way, InputStreamReader and OutputStreamWriter are what you need.

InputStreamReader/OutputStreamWriter are the bridge from the byte streams to character streams, them allow you to specific the character set that you want to use to decode/encode the data.

These are constructures of InputStreamReader:

  • InputStreamReader(InputStream in) - Using the default charset to decode the data.
  • InputStreamReader(InputStream in, Charset cs) - Using the specific charset to encode the data.
  • InputStreamReader(InputStream in, CharsetDecoder dec) - Using the specific charset to encode the data.
  • InputStreamReader(InputStream in, String charsetName) - Using the specific charset to encode the data.

These are constructures of OutputStreamWriter:

  • OutputStreamWriter(OutputStream out) - Using the default charset to encode the data.
  • OutputStreamWriter(OutputStream out, Charset cs) - Using the specific charset to encode the data.
  • OutputStreamWriter(OutputStream out, CharsetEncoder enc) - Using the specific charset to encode the data.
  • OutputStreamWriter(OutputStream out, String charsetName) - Using the specific charset to encode the data.

Example:

I would like to write the data to the file with UTF-8 as charset.

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

And this is the results (utf8output.txt):

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

Note: You can see the charsets that supported by Android from the Official Android Reference.

 

Object Streams

The last stream type that I will describe in this article. When the Data Streams are allow you to read/write primitive data values, the Object Streams are allow you to read/write the complex data types (Object).

Serialization is a process that convert the object to the serialized bit-stream, and that bit stream can be written out or re-constructed to the object.

What kind of Object that can serialize? Most of standard classes can be serialized, and you can make your class serializable by implements the Serializable marker interface.

Example:

I have create a class for Car data with the properties brand and model. Then put the implements keyword to mark that this class can be serialized.

Java
    public class Car <code>implements Serializable</code>{
        public String brand;
        public String model;
        @Override
        public String toString() {
            return brand+" "+model;
        }
    }

Next, I will write the Car object to a file "car.obj" by using the writeObject(Object) method:

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

This is the result in 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

Now, I would like to deserialize the Car object. I can do it by using the readObject() method and then cast to Car:

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

When I print the result I get:

Toyota Camry

When you use the readObject() method, a ClassNotFoundException may throw if there is no class definition for that object you need to handle it.

Note: If your class contain the references of another class, you need to make sure that every objects that your object referred to can be serialized.

 

Android and Files

Android allow you to operate the file like Java, you can use every Streams class of Java. Most of Android devices offered built-in storage (internal storage), plus a removable storage (external storage), and you need to know that what kind of data should be written on which storage.

Image 5

Internal Storage

The internal storage can't be removed from your device that men your data on internal storage will always available. Files saved here are accessible by only your app by default and when you remove the app the data will be removed too.

Note: Android device storage mount paths are depend on the Manufacturer config. You should to avoid using the hard code pathname.

To using the internal storage, Android provides methods in a Context class that use to get the File object of internal storage pathname.

Methods

  • boolean deleteFile(String name) - Delete the specific private file, the parameter is file name but cannot contain path separators.
  • String[] fileList() - Get an array of strings naming the private files.
  • File getCacheDir() - Get the File instance of cache directory.
  • File getFileStreamPath(String name) - Get the File instance of file that created by openFileInput(String name).
  • File getFilesDir() - Get the File instance of directory of files that created by openFileInput(String name).
  • FileInputStream openFileInput(String name) - Get the FileInputStream of specific private file. The paramaeter is file name but cannot contain path separators.
  • FileOutputStream openFileOutput(String name, int mode) - Get the FileOutputStream of specific private file, and you can also set the creation mode. The paramaeter is file name but cannot contain path separators.

openFileOutput Modes

  • MODE_APPEND - Append the data to the end of existing file.
  • MODE_PRIVATE - The default mode that prevent accessing the file from other app.
  • MODE_WORLD_READABLE - Allow other app to read the file. This mode is deprecated and should to avoid using because it's very dangerous.
  • MODE_WORLD_WRITEABLE - Allow other app to write the file. This mode is deprecated and should to avoid using because it's very dangerous.
External Storage

External Storage is the optional storage such as a micro SD card, and it may be removable. This type of storage is not always available because user can unmounts, mount as USB storage, or remove it from the device. The data that store on external storage can be read and write by any app and you can't control it. When you remove the app, the system will removes you data if you save it from the getExternalFilesDir() method.

To read/write the data from/to external storage you need to request the READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permission. You can add them to the manifest element in the Manifest file like this:

XML
<?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>

There are some methods in Context class that are used to get the files on the external storage:

  • File getExternalCacheDir() - Get the File instance of the cache directory on the primary external storage.
  • File[] getExternalCacheDirs() - Get the File instances of cache directories on all external storage.
  • File getExternalFilesDir(String type) - Get the File instance of file directory on the primary external storage.
  • File[] getExternalFilesDirs(String type) - Get the File instances of file directories on all external storage.

You can also use the Environment class to manage the files on the external storage:

  • static File getExternalStorageDirectory() - Get the primary external storage directory.
  • static File getExternalStoragePublicDirectory(String type) - Get the top-level public external storage directory for specific file type.
  • static String getExternalStorageState() - Get the state of primary external storage.
  • static File getRootDirectory() - Gets the Android root directory.
  • static String getStorageState(File path) - Get the state of storage device that provides the given path.
  • static boolean isExternalStorageEmulated() - Returns true if the external storage is emulated.
  • static boolean isExternalStorageRemovable() - Returns true if the external storage is removable.

Public external storage file type (using as parameter of getExternalStoragePublicDirectory())

  • DIRECTORY_ALARMS - Directory that is used to place audio file that should be in the list of alarm.
  • DIRECTORY_DCIM - Directory that is used to place pictures and videos that user will see when mount the device to PC as a camera.
  • DIRECTORY_DOCUMENTS - Directory that is used to place the document that created by the user.
  • DIRECTORY_DOWNLOADS - Directory that is used to place files that have been downloaded by the user.
  • DIRECTORY_MOVIES - Directory that is used to place movies that are available to the user.
  • DIRECTORY_MUSIC - Directory that is used to place any music audio files.
  • DIRECTORY_NOTIFICATIONS - Directory that is used to place any notifications audio files.
  • DIRECTORY_PICTURES - Directory that is used to place pictures files.
  • DIRECTORY_PODCASTS - Directory that is used to place any podcasts audio files.
  • DIRECTORY_RINGTONES - Directory that is used to place any podcasts ringtones files.

When the external storage may can be removable, you need to check its state before read.

Java
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;
}

Let's Code

Now, you have learn about File, Streams, and Android Storage. This is the time to code, using everything that you know to solve the problem.

Task 1: Create the simple memo application that contains only one big EditText and a save Button, you need to complete the save function. Save the EditText's content when user press the save button and restore the data when use back to this page again.

Suggestion: Using the stream that is suitable to read/write the text.

Sample screen layout (please try to make it beautiful as you can):

Image 6

I decided to store the memo data on the internal storage as text file. Here is my code:

Layout XML:

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>

I have created an Activity and store the instance of an EditText and a Button as class variables,

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

Then implements the onClickListener of button.

Java
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;
        }
    }
}

After that I have create the method that is used to write the data to a file. I have used the DataOutputStream to write the text to the FileOutputStream.

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

Then add it to the onClick() method.

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

Next, I create a method that is used to restore the saved data. I have check for the existing file before read the data.

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

Finally, I add the statement restoreTheDataFromFile() to onCreate() method ;

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

Try to run and test it!!

Image 7 Image 8

 

The Databases

When you want to save a lot of data that has a same structure/schema, the database is that you need. Android provides full SQLite databases support. To use the SQLite you need to know some basic knowledge of SQL commands, you can see more at w3schools.com.

Note: There are many alternative databases for android such as db4oCouchbasemcobject, etc. don't worry if you don't like the SQLite.

Image 9

To using the SQLite you need to define a data schema and create the class that derived from SQLiteOpenHelper.

Task 2: Create the simple shopping list application. The important functions of this application are create new, mark as bought and delete. In this task we will learn to build it together :)

Image 10

Required functions

- Add new item.

- Mark as bought.

- Delete bought item from the list (only mark as delete).

- Delete all item in database (Permanently delete).

Defining a schema

When you want to use the RDBMS the schema is the first thing that you should to think about. In Android you should to create a contract class that contains constants that define names for URIs, tables, and columns.

Note: The contract class should derive from the BaseColumns interface, if you want to implement a content provider. BaseColumns provides the _ID property that is used by the content provider, you can also use the _ID as primary key if you want.

In the shopping list application you need to create a class for store the item data like this:

Java
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 +
                '}';
    }
}

There are id, title, bought, deleted, and createDate properties in this class. The createDate property will create automatically when this class object construct.

Note: I use the java.util.Date for createDate property because it's easy to format.

And then create contract classes of shopping application:

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";
    }
}

From the code above, there is the class ShoppingListContract that contain the ItemEntry inner class. The ItemEntry class is called contract class it define the table and columns name. All contract class properties are static and final, you will can access them without create the contract class instance and no need to change the property’s values in runtime.

Note: The contract class should be abstract because no need to create its instance.

Creating the Database

To create a database, you need to create a class that derived from SQLiteOpenHelper and implement its methods.

Java
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) {

    }
}

The database file will store on the private storage that other apps can't access it. Next, create the SQL statements that are used to create and drop the database table.

Note: There are some limitations of data type on SQLite version 3, you can see the supported data type from the SQLite Official Document.

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;

The _ID column was set to INTEGER and auto increase the value when insert the data. I have used INTEGER type for columns of bought and deleted properties because there is no boolean type in SQLite.

Note:  The default value of each column is NULL, if you haven't set

Then add it to onCreate() and onUpgrade() methods:

onCreate() - This method will call only one time after database created. It is used to create the tables of database.

onUpgrade() - This method will call every time when the database version changed. It is used to update the tables structures.

Java
@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() method is used to execute the raw SQL command such as create table, droptable, alter table.

I just simply drop and recreate the table in onUpgrade() method but in the real app when the database structures changed you should to alter table instead.

Putting the data

To put the data to the database, you can use the raw SQL command or using the simple method that Android provided.

This is the raw SQL for insert the data to database:

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

Or

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

Note: No need to insert the id, when the id column value is auto increase.

You can create the raw insert command for Item object in java code like this:

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

You will see that I have format the SQL string, when you want to insert the boolean value you need to convert it to int, I recommend you to use the if-else statement shortcut.

 

if-else statement shortcut

When you want to compare something and do the action you can use the if-else statement like this:

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

But if the condition and action is very simple you can use the shortcut instead:

Java
condition?foo():bar();

Date format

When you want to insert the Date value, you need to convert it to string with format "yyyy-MM-dd HH:mm:ss". You can use the SimpleDateFormat class to format the Date object to a string like this:

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

And you can also convert back to Date object by using the parse() method:

Java
Date createDate = dateFormat.parse(createDateString );

 

After format the SQL string you will got the string like this:

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

When consider our application you will found that no need to insert the bought and deleted properties because user can't to create the new Item with deleted mark or bought mark. You can re-write the SQL like this:

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

And the result string is:

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

Then we will execute third command. You need to get the writable database and using it execSQL() method to execute this command:

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

I have use Log.d() to print the insert statement for easy to debug the SQL.

Note: execSQL() method will throws SQLException if the SQL statement is invalid. You should to throw the SQLException to the caller of method and handle it if possible.

Now, we will create the insert statement by using the method that Android provided.

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

  • String table - Name of the table that you want to insert the data.
  • String nullColumnHack - It not possible to insert a completely empty row,  if the provided values is empty, the Android will use this column to create the insert statement.
  • ContentValues values - the values that you want to insert

You need to create the ContentValues instance that required by insert() method, ContentValues is the class that is used to store the key-value data. So, we will use it to store the coulum-value paris like this:

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

Put all data that need to insert to the ContentValues instance, then pass it to the insert() statement:

Java
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;
}

I have set the nullColumnHack to null because I don't want to insert an item if the values is null. The insert() method will return the row id (long) as result or -1 if an error occurred.

Now, we will try to use this database and test the put methods in our application. Create the activity if you haven't create it yet, then look at the onCreate() method of the activity and initial the database helper constant:

Java
ShoppingListDatabaseHelper shoppingListDatabaseHelper = new ShoppingListDatabaseHelper(this);

Then create the Item instance, set its title:

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

After that we will try to use the putItemWithRawSQL() method to insert the Item data to database:

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

When the putItemWithRawSQL() method may throws the SQLException, you need to handle it.

Try to run the application!!

If nothing error the item will insert into the database. If you got the SQLException, please check to SQL statement in a putItemWithRawSQL() method.

Image 11

Next, we will try to use the putItemWithInsertMethod() method:

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

Then run the app again. If the the result is not equals to -1, it meant your data is inserted.

Image 12

 

Next, we will create the UI for using to add the data to database.

First of all, add the EditText and Button to the layout:

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.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>

Then, get the instance of an EditText and a Button.

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

After that, create the instance of ShoppingListDatabaseHelper and implement the OnClickListener of addNewItemButton:

Java
....
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) {

    }
};

In the onAddButtonClickListener create the item object and add to the database:

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

Then you can add a new item by enter the title in EditText and tap the add Button to save.

Reading the data

To read the data from the database, you can use the query() method or rawQuery() method if you prefer a raw SQL.

We will start with the raw SQL query, when you want to get the data from the data base you need to create the SELECT statement.

SQL
SELECT * FROM table_name;

This statement is used to select all columns and all rows from the specific table. If you want to limit the output column use can add the columns name between the word SELECT and FROM like this:

SQL
SELECT column_name,column_name FROM table_name;

You can use the rawQuery() method to execute the raw SQL query statement.

Cursor rawQuery(String sql, String[] selectionArgs) - Used to query the data, you can put the condition parameter if you want (need to write the condition in the query and use the question mark sign(?) for the parameter such as SELECT * FROM student where score > ?).

All query method return the result as Curson object. The Cursor object is the result set of query, it store the data in somewhere like buffer/cache and you can get only one row of data at a time.

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

Caution: Don't close the SQLiteDatabase object until you don;t want to access the data in Cursor object.

I have create a new method in the ShoppingListDatabaseHelper, this method is used to retrieve all Items in the database. It return a standard Cursor object. Next, we will create the method that has a same function with getItemsWithRawQuery() methodbut using the query() method that SQLiteDatabase provided instead.

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

The query() method of SQLiteDatabase is used to query the data from the database you can also put the condition, group, and order to this method. There are many overload of query() method but we will use a simple one.

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

  • table - The name of the table that you want to query.
  • columns - An array of columns name that you want to get, pass the null value if you want all columns.
  • selection - Enter the WHERE clause of SQL statement if you want to select some rows that math the condition.
  • selectionArgs - You can use the question mark sign (?) in the selection, each ? will replace by the value in selectionArgs.
  • groupBy - Enter the SQL GROUP BY vlause if you want to group the data.
  • having -  Enter the SQL HAVING clause if you want.
  • orderBy - If you want to order the data you need to enter the SQL ORDER BY clause here.
  • limit - If you want to limit the number of result you can enter the LIMIT clause here.

After you got the Cursor object, you need to iterate it to get the data in each row:

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

The code above will print the item title on the logcat like this:

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

You will see that the Cursor has a moveToNext() method that is used to move the cursor to the next item then you can read the data.

Note: There is no data at the first position of Cursor object, you need to move it before use;

  • boolean move(int offset) - Move the Cursor by a relative amount from current position, returns true if move successful.
  • boolean moveToFirst() - Move the Cursor to the first row that has a data, returns true if move successful.
  • boolean moveToLast() - Move the Cursor to the last row that has a data, returns true if move successful.
  • boolean moveToNext() - Move the Cursor to the next row, returns true if move successful.
  • boolean moveToPosition(int position) - Move the cursor to the specific row, returns true if move successful.
  • boolean moveToPrevious() - Move the Cursor to the previous row, returns true if move successful.
  • byte[] getBlob(int columnIndex) - Get the data at specific column as array of bytes
  • int getColumnCount() - Get the total number of columns
  • int getColumnIndex(String columnName) - Get the index of column. Returns -1 if the column doesn't exist.
  • String getColumnName(int columnIndex) - Get the name of column at specific index.
  • String[] getColumnNames() - Get all columns name.
  • int getCount() - Get the number of rows in the Cursor.
  • int getPosition() - Get the current position of Cursor
  • double getDouble(int columnIndex) - Get the value of specific column as double.
  • float getFloat(int columnIndex) - Get the value of specific column as float.
  • int getInt(int columnIndex) - Get the value of specific column as int.
  • long getLong(int columnIndex) - Get the value of specific column as long.
  • short getShort(int columnIndex) - Get the value of specific column as short.
  • String getString(int columnIndex) - Get the value of specific column as string.
  • int getType(int columnIndex) - Get the type of data in specific column.

You will see that every method that are used to get the value of specific column need an index as paramater. Where can you get the index? The index is the order of column that you provide in the SELECTION clause of SQL, if you use the * the order of columns will same as the table definition.  When the column index is not strict, you should to use the getColumnIndex() method to get it.

Try to pass the data in Cursor to the Item object:

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

This is a result:

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}

From the example, you will see that I have use the == operator to convert the int value to the boolean value, and use the SimpleDateFormat to convert the createDate back.

You can also convert all data in a Cursor to the List of Item object but don't recommend because the list of object will consume a lot of memory while a Cursor don't.

 

Next, we will learn to display the query result in the ListView.

First, you have to add the ListView to the layout af Activity like this:

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.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>

Then open the Activity file, then get the instance of ListView:

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

After that, query every Items from the database:

Java
Cursor itemCursor = databaseHelper.getItemsWithQueryMethod();

Now, you need to create the adapter for a ListView. The simplest way is using the SimpleCursorAdapter like this:

Java
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);

I have create the SimpleCursorAdapter that display the item title on the layout android.R.layout.simple_list_item_1.

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

  • layoutId - you can use the layout that Android provides or create your own one.
  • columns - The array of columns name that you want to display.
  • viewIds - The id of each view that you want to use to display the data in each column. (same order as the columns)

When you run the app you will see the list of item:

Image 13

You can also create your layout and apply it to the SimpleCursorAdapter.

This is my new layout for list item:

<?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>

Then change the SimpleCursorAdaptor constructure parameters:

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

Then move it to the new method like this:

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

The swapCursor() method is used to swap the Cursor in the adapter to a new one.

Add the reloadData() statement to the onCreate() method and onAddButtonClickListener.

Here is my full source code:

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

Finally, try to run the app!!

Image 14

Query with the condition

Most of data query that use in the app have some condition such as the range of date, the status of data, etc. Now, we will learn about adding the condition to the query.

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

Thi is SELECT statement that is used to select the items that are not mask as deleted from the database:

SQL
SELECT * FROM Item WHERE deleted = 0;

You can also add more than one condition to the statement by use the wrod AND or OR as conjunction.

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

When you execute the statement above the database will filter the data and return only the row that not mark as delete or bought.

I recommend you to use the query() method with using the ? For the values in condition then pass the real values as array of strings to the forth parameter:

Java
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;
}

In the activity replace the getItemsWithQueryMethod() with x().

Updating the data

When the data changed to need to write the update to the database by using the SQL UPDATE statement.

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

Same as other stetements, you can using the raw query or the method that SQLiteDatabase provided.

For a raw query you can use the execSQL() method to execute the SQL statement like this:

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

Android provides the update() method for using to create and execute the UPDATE statement.

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

  • values - Map of column name - value that you want to update.
  • whereClause - Row select condition, all selected rows will updated.
  • whereArgs - The values that will replace ? in the whereClause .

This method will return integer value that is a number of affected rows.

Java
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;
}

Note: Writable database instance is required when update the data.

Suggestion: Recommend to use an UPDATE statement for marking the data as delete because deleting the data is dangerous, it may be affect to another data that related to the deleted data.

Now, we will implement swipe to mark as bought function.

With the SimpleCursorAdapter, you can't create dynatic liet item layout. To complete the swipe to mark as bought function, we need to create the custom CursorAdapter.

First, create the new class derived from CursorAdapter name ShoppingListCursorAdapter:

public class ShoppingListCursorAdapter extends CursorAdapter{

}

Implement the constructure, newView() and bindView()methods:

Java
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) {

    }
}

Then create the LayoutInflator instance:

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

After that, in the newView() method, inflate the layout file

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

The newView() method is used to construct the View like getView() of BaseAdapter but you can't access the item instance in this method.

Then complete the bindView() method, the bindView() method is used to apply the data to the View.

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

}

I have check for bought status, if the item was already bough, will add strikethrough to the item title.

Then back to the class signature add implements View.OnTouchListener and override the onTouch() method by use this snippet:

Java
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 is used to detect the touch event on specific view, we will use it to detect swiping on the list item to mark the item as bought.

Apply the OnTouchListener to the view:

Java
@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;
}

Now, our CursorAdapter is completed. Next, go to the activity and replace the SimpleCursorAdapter with ShoppingListCursorAdapter.

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

Run the app and try to swipe through the item!!

Image 15 Image 16

Seem work!! But we need to change the item status and update the database.

Create the ShoppingListDatabaseHelper instance inthe ShoppingListCursorAdapter:

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

After that, go to the bindView() method then get the id of item and set it as titleTextView tag:

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

Next, go to the case MotionEvent.ACTION_UP of onTouch() method. Add the code that is used to mark the item as bought:

Java
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;

After you save the update to database, the ListView will not automatic refresh. You need to re query the data. There is requery() method in the Cursor object but it's deprecated sinc API level 11, you need to query the data manually and use swapCursor() to change the cursor of adapter.

Java
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;

Now, try to run the app again!!

Image 17 Image 18

You will found that the bought was saved after you swipe though the item.

Next, we will implement the delete item function. We will only mark the item as deleted when user select the delete bought item option in the options menu.

If you don't have an options menu, create it now, then add the delete bought item option:

XML
<?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>

Add the options menu to the activity:

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

Then create the delete bought item function in the database helper class:

Java
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;
}

Call this function when user press the menu and then reload the data:

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

Run the app!!

Image 19

Deleting the data

You can also permanently delete the data from the database by using the SQL DELETE statement, however not recommend to delete the data if not necessary.

SQL
DELETE FROM table_name [WHERE some_column=some_value;]

The SQL DELETE statement is very simple, only the tablename and condition that required.

Caution: You can use the DELETE statement without provide the condition but all data in the table will be deleted.

Like others SQL statement, you can use the execSQL() method to execute the raw query or using the delete() method that SQLiteDatabase provided.

The last function of our app is delete all items from the database, we will use the SQL DELETE statement to build this function.

SQL
DELETE FROM Item;

You can use the SQL DELETE statement without any condition, because we would like to delete all items.

The delete() method:

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

There are 3 parameters that delete() method required, table, whereClause, and whereArgs. If you don't want to use the condition you can pass the null value as whereClause and whereArgs parameters. Returns the number of affected rows or -1.

Back to the app, create the method for using to delete all items:

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

Then create the delete all items menu:

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

After that back to the activity and implements the delete all function:

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

Run the app again!!

Image 20

Finish!! Now the shopping list application is completed, you can customize the styles and publish to the store if you want :)

Caution: Should to ask the user to confirm before delete.

Shared Preferences

Android has provides a key-values storage called SharedPreferences, SharedPreferences allows you to save and retrieve key-value pairs of primitive data types. You can create many SharedPreferences files as you want, and like other file you can also make it as private or shared

To retrieves the SharedPreferences object, you can use the getSharedPreferences() or getPreferences() methods of Context class.

  • SharedPreferences getSharedPreferences (String name, int mode) - Get the specific shared preferences by passing the name and creation mode.
  • SharedPreferences getPreferences (int mode) - Get defualt shared preferences with specific mode, like the getSharedPreferences() method but passing an activity's class name as the preferences name.

SharedPreferences Mode:

  • MODE_PRIVATE - Create the file that can be only accessed by this application
  • MODE_WORLD_READABLE - Create the file that every app can read.
  • MODE_WORLD_WRITEABLE- Create the file that every app can write.
  • MODE_MULTI_PROCESS - Create the shared preferences file that allow many processes of the app to read and write at the same time.

Writing the Data

To write the data to a shared preferences file, you need to get the SharedPreferences.Editor by calling the edit() method of the SharedPreferences object.

After that you can put any data to the Editor by using the Editor provided methods such as putString(), putDouble(), etc.

Then you need to call the commit() method on Editor instance to commit to write the value to the shared preferences.

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

Reading the Data

To read the data from shared preferences file, you can use the methods such as getInt(), getString(), etc. on the SharedPreferences object. You can also provide the default value to return if the key isn't exist.

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

Task 3

Create the empty app with mechanism to remind the user to rate the app after used for 7 times.

To complete the app, you need to count when user enter the app activity and increase the number every time when activity create.

First of all, create the new application or new activity and read the "app_usage_count" from shared preferences:

Java
SharedPreferences sharedPreference = getPreferences(Context.MODE_PRIVATE);

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

If the key was not found, the 0 value will return. Then increase the value and save it back to shared preferences:

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

After that, check for the value of appUsageCount, if it greater than or equals to 7 alert the user to rate the app:

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

Now, try to run the app, close it and reopen for 7 times:

Image 21 Image 22

You will see that after 7 times you still got the alert dialog, because you haven't check for the rate status. Try to add more data to check that:

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

I have add 2 key-value pair to the shared preferences for using to detect the rate status and recount if user deny.

Now, you add this mechanism to your application for increasing your app rating & review.

Web Services

Most of modern application consume the data from the internet and send some data back. The data service in the server side is called Web Services, it is may be a SOAP, WCF, REST or anything else that mobile can interact with. In this article we will focus on the simple REST/HTTP request, because it is the popular data service for mobile application.

To connect the internet you need to add the android.permission.INTERNET uses-permission to the application manifest:

XML
<?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>

Note: Android doesn't allow to connect the internet in main thread (UI Thread), you need to create a new thread to do that work.

URLConnection

URLConnection is a simplest way to read/write the data from/to the internet, you just pass only a URL of service and read the data with input stream.

For example:

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

From the example, I have create the URLConnection for using to consume the data from yahoo finance API.

HttpURLConnection

HttpURLConnection is derived from the URLConnection, it designed to use to send and receive the data from http service.

To get the HttpURLConnection instance, you can call url.openConnection() and cast it to HttpURLConnection then you can also add the request header and request body. Read the response data as stream and close the connection after use.

For example:

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

Task 4

Create the weather forecast application by consume the data from openweathermap.org. The only required function is display your hometown 7 days weather forecast.

Image 23

The service URL is:

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

Just replace YOUR_HOME_TOWN with you hometown name such as Bangbuathong, Thailand. Here is my:

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

Try to open the URL in the web browser you will see the JSON string, format it with jsonformatter you will get the more readable JSON like this:

JavaScript
{
   "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
      }
   ]
}

We will focus on the list of result (list JSON array), we need to get the dt(date), weather.main, weather.icon, and temp.day attributes for each day.

To parsing the JSON you can use the JSONObject and JSONArray classes, JSONObject contains many methods that are used to get the data from JSON such as gteBoolean(), getInt(), getJSONArray(), etc.

For example:

JavaScript
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");
}

For the weather icon, after you parse the result you will get only the name of icon. You can get the icon file from this URL:

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

For example:

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

 

AsyncTask

AsyncTask allow you to do some operations in the background thread and then publish the result and progress to the UI thread without having to manipulate threads and/or handlers. When you want to use the AsyncTask you need to create a class that derived from it.

For example:

Java
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
    }
}

From the example you will see that GetWeatherDataTask is derived from the AsyncTask with 3 type parameters.

AsyncTask<inputType, progressType, outputType>

  • inputType - Type of the parameter that you will pass to the doInBackground() method when create the instance.
  • progressType - Type of the progress that will pass to the onProgressUpdate() method.
  • outputType - Type of the parameter that onPostExecute accept and type of value that doInBackground() method returns.

AsyncTask's methods

  • onPreExecute() - Something that you want to do before execute the backgrounf task.
  • doInBackground() - The backgroung task that you want to execute.
  • onProgressUpdate() - Will call by the doInBackground() to notify the pregress of backgound task to UI thread.
  • onPostExecute() - Something that you want to do after execute the background task. (UI Thread)

When to use the AsynTask?

Every time that you need to do the long operation such as read/write the data from/to disk or network to avoid the UI blocking. Especially the network operations you should to do in AsyncTask because Android doesn't allow to do any network operation in the Main/UI Thread.

Now, we are ready to start building the app!!

First of all, create the Application/Activity with name as you want, then create the class for store a weather data:

Java
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;
    }
}

Next, create the AsyncTask in the same file of activity for using to download the data and update the UI:

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

I have create the GetWeatherDataTask that derived from AsyncTask<String, Void, List<Weather>>. Why I use the List<Weather> for outputType? Because it's easy to add to the Adapter of ListView. Then I have override the doInBackground() and onPostExecute() methods.

Next, we will get the data from a service in doInBackground() method:

Java
@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;
}

I have chain the Input stream with InputStreamReader and BufferedReader to read the data as string.

Note: You need to handle the MalformedURLException and IOException, please note that you can't throw any exception in the background thread.

Note: You can pass the integer as parameter of ArrayList constructure to define the expect size of array.

Next, we will read all data and concat it to a single string:

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

After that, we will parse the data to JSON and convert to to the List of Weather object:

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

The date that return from JSON is long value, you need to parse it to Date object.

Note: don't forget to handle the JSONException.

After that, we will put the ListView to the layout:

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.Task4WeatherForecastActivity">

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

</RelativeLayout>

Then create the list item layout:

Image 24

XML
<?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>

After that, create the Adapter for ListView:

Java
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;
    }
}

In the adapter I have provide a replaceData() method that is used to replace the data set. This adapter is almost complete but need to implement the icon image.

Next, go to the activity. Get instance of the ListView, create the WeatherListAdapter instance and apply it to the ListView:

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

Then go to the onPostExecute of AsyncTask, add the statement to set data to ListView:

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

Go to the onCreate() method execute the GetWeatherDataTask:

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

Run the application!!

Image 25

Displaying the Image from the Internet

In Android you can't set the ImageView source to internet URL, you need to read it and parse to bitmap then set to the ImageView.

You can also using the URLConnection and AsyncTask to download the image and use the BitmapFactory to convert it to the Bimap object.

For example:

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

This task will download the image from given URL and set it to the ImageView.

List Item with consume the Image from the Internet

The bad new is the ListView is alway recycle the View that nolonger display, that may make the image displayed is invalid such as the icon is rainny but the weather in that row is sunny :(

Have an idea for solve that problem? I think the View tag may be useful for this situation. Set the tag of image view to the url of image, after downloaded set the image to image view if the tag still equal to the image url.

I have modified the ImageDownloadTask to accept the ImageView in a constructure:

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

Try to use it in the 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>{

        ....
    }
}

Run the app again!!

Image 26

Is It works? yes but when your ListView consume the data from a very large set of items, it will make your app slow or crash, because it will reload the image every time and can't cancel the request if nolonger want that image.

Picasso

Picasso is the answer for every image related problems, it will help you to load the image from the internet, storage, and resources. It also help you to manage the image cache.

To use the Picasso library in the Android Studio, you need to add the compile dependency to the build.gradle.

Ope the build.gradle file (in the app directory):

Image 27

Next, look at the dependencies and add this statement:

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

Click the Sync Now button on the top right of the editor:

Image 28

Now, you can use the Picasso. You can use the Picasso every where that you can get the Context object :)

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

This is the simple way to use the Picasso library, if you want to learn more you can see at the Official Picasso Github page

Go to the adapter and delete the AsyncTask then edit the getView() method like this:

Java
@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;
}

Run the app now!!

Image 29

Now, our app is completed. However, you should to lern more and more to builder the greater app in the future.

Points of Interest

In this article you have create 4 applications, every application will help youto under stand the concept of each storage type. Sometimes you should to consider the 3rd party library, it will help you to make the complex problem easier.

Hop you enjoy your Android Application Developer life.

History

- 14/08/2014 initial release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)