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.
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.
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.
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).
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.
Sample of File
operate in 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.
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.
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 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
.
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:
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:
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.
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.
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
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.
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.
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.
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 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:
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:
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.
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.
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.
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 FileWriter
by 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 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
.
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.
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.
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.
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.
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.
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
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.
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.
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.
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:
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
:
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 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.
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:
="1.0"="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.
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):
I decided to store the memo data on the internal storage as text file. Here is my code:
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.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.
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:{
}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.
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.
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 ;
@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!!
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 db4o, Couchbase, mcobject, etc. don't worry if you don't like the SQLite.
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 :)
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).
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:
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.
To create a database, you need to create a class that derived from SQLiteOpenHelper
and implement its methods.
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.
@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.
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:
INSERT INTO table_name VALUES (value1,value2,value3,...);
Or
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:
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:
if(condition)
foo();
else
bar();
But if the condition and action is very simple you can use the shortcut instead:
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:
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:
Date createDate = dateFormat.parse(createDateString );
After format the SQL string you will got the string like this:
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:
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:
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:
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:
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:
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:
ShoppingListDatabaseHelper shoppingListDatabaseHelper = new ShoppingListDatabaseHelper(this);
Then create the Item instance, set its title:
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:
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.
Next, we will try to use the putItemWithInsertMethod() method:
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.
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:
<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
.
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
:
....
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:
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.
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.
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:
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.
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:
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:
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:
<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
:
ListView listView;
@Override
protected void onCreate(Bundle savedInstanceState) {
....
listView = (ListView) findViewById(android.R.id.list);
}
After that, query every Items from the database:
Cursor itemCursor = databaseHelper.getItemsWithQueryMethod();
Now, you need to create the adapter for a ListView
. The simplest way is using the SimpleCursorAdapter
like this:
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:
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:
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!!
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.
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:
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.
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:
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()
.
When the data changed to need to write the update to the database by using the SQL UPDATE statement.
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:
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.
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:
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:
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
@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
.
@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:
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:
@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
.
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!!
Seem work!! But we need to change the item status and update the database.
Create the ShoppingListDatabaseHelper
instance inthe ShoppingListCursorAdapter
:
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:
@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:
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.
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!!
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:
="1.0"="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:
@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:
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:
@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!!
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.
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.
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:
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:
<item
android:id="@+id/menu_delete_all"
android:title="DELETE ALL"
/>
After that back to the activity and implements the delete all function:
case R.id.menu_delete_all :{
if(databaseHelper != null)
{
databaseHelper.deleteAllItems();
reloadData();
}
}
Run the app again!!
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.
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.
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.
SharedPreferences sharedPreference = getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreference.edit();
editor.putInt("app_usage_count", count);
editor.commit();
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.
SharedPreferences sharedPreference = getPreferences(Context.MODE_PRIVATE);
int defaultValue = 0;
int appUsageCount = sharedPreference.getInt("app_usage_count", defaultValue);
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:
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:
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:
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:
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:
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.
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:
="1.0"="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 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:
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 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();
}
}
Create the weather forecast application by consume the data from openweathermap.org. The only required function is display your hometown 7 days weather forecast.
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:
{
"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:
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
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:
private class GetWeatherDataTask extends AsyncTask<URL, Integer, Long> {
@Override
protected void onPreExecute() {
}
@Override
protected Long doInBackground(URL... urls) {
}
@Override
protected void onProgressUpdate(Integer... progress) {
}
@Override
protected void onPostExecute(Long result) {
}
}
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:
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:
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:
@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:
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:
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:
<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:
="1.0"="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
:
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
:
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
:
@Override
protected void onPostExecute(List<Weather> weathers) {
super.onPostExecute(weathers);
adapter.replaceData(weathers);
}
Go to the onCreate() method execute the GetWeatherDataTask:
protected void onCreate(Bundle savedInstanceState) {
....
new GetWeatherDataTask().execute();
}
Run the application!!
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:
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);
imageView.setImageBitmap(bitmap);
}
}
This task will download the image from given URL and set it to the ImageView.
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:
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!!
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 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):
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:
Now, you can use the Picasso
. You can use the Picasso
every where that you can get the Context
object :)
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:
@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!!
Now, our app is completed. However, you should to lern more and more to builder the greater app in the future.
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.
- 14/08/2014 initial release