How to read and write to CSV files using Xuni FlexGrid for Android
Xuni provides simple and easy-to-use controls for visualizing and editing user data. One area that we haven't explored too much yet is saving to and loading from some type of external source. We’ve touched on this somewhat in our webcasts for Xuni for Android (as well as Xuni for iOS) with regards to loading data from a web request, but we haven't worked with data coming from a local source. In this article, we'll examine loading data from and saving data to a local CSV file on the Android platform using Xuni FlexGrid. I’ve chosen to focus on the CSV format for several reasons. First, this is a common format between many applications, and, perhaps most importantly, spreadsheet applications like Excel and Google Sheets. Thus it’s relatively easy to export data into this format and import it elsewhere. These files are also conceptually very easy to work with, and make for a good instructional tool since they’re easy to read. This example uses some stock data that has been saved into a CSV. We'll deal with loading the data from a CSV saved as an internal raw resource, optionally loading from external storage, and saving to external storage.
Reading a CSV File
The first step to implementing our example is to create a class that handles reading a CSV file and parsing the data. The basic construction will be to package the file data into an InputStream; use a BufferedReader to grab the data line by line as we iterate through it; and split the incoming lines into string arrays. When we've finished iterating through the CSV we'll be left with an arrayList containing String arrays that represent each line.
public class ReadCSV {
InputStream in;
public ReadCSV(InputStream in){
this.in = in;
}
public List<String[]> read(){
List<String[]> results = new ArrayList<String[]>();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
try {
String line;
while((line = reader.readLine()) != null){
String[] row = line.split(",");
System.out.println(Arrays.toString(row));
results.add(row);
}
}
catch (IOException e){
throw new RuntimeException("Error reading CSV File " + e);
}
finally{
try {
in.close();
}
catch (IOException e){
throw new RuntimeException("Error closing inputstream " + e);
}
}
return results;
}
}
At this point we've created something very generic, but we can create another class to act as our model that more strongly represents the data we'll be working with.
Creating a Model for your Data
The next step for our project is to create a model where we can strongly type our data (rather than treat every value generically as a string). Creating another class to act as a model gives us an opportunity to parse the Strings into data types that better reflect the values. A single line of the stock.csv file looks like this: GOOG,01-Sep-2015,602.359985,612.859985,594.099976,597.789978,3680200 These values reflect a ticker, date, open, high, low, close, and volume, which means we'll need to create properties that correspond to each. We'll also create a method for parsing a List of string arrays into an ObservableCollection of StockData objects so that we can set it as the FlexGrid's itemsSource. Likewise, we'll provide a mechanism for taking the ObservableCollection after it's been edited with the FlexGrid and converting it back into a List of string arrays for the purpose of saving the data to disk.
public ObservableList<StockData> getObservableList(List<String[]> stocks){
ObservableList<StockData> list = new ObservableList<StockData>();
String[] sarray;
SimpleDateFormat sformat = new SimpleDateFormat("dd-MMM-yyyy", Locale.ENGLISH);
for(int i = 0; i < stocks.size(); i++){
StockData sd = new StockData();
sarray = stocks.get(i);
sd.ticker = sarray[0];
try {
sd.date = sformat.parse(sarray[1]);
} catch (ParseException e) {
e.printStackTrace();
}
sd.open = Double.parseDouble(sarray[2]);
sd.high = Double.parseDouble(sarray[3]);
sd.low = Double.parseDouble(sarray[4]);
sd.close = Double.parseDouble(sarray[5]);
sd.volume = Integer.parseInt(sarray[6]);
list.add(sd);
}
return list;
}
public List<String[]> getStringList(ObservableList<StockData> list){
List<String[]> stringList = new ArrayList<String[]>();
SimpleDateFormat sformat = new SimpleDateFormat("dd-MMM-yyyy", Locale.ENGLISH);
for(int i = 0; i < list.size(); i++){
String[] row = new String[]{list.get(i).ticker, sformat.format(list.get(i).date),
Double.toString(list.get(i).open),Double.toString(list.get(i).high),
Double.toString(list.get(i).low), Double.toString(list.get(i).close),
Integer.toString(list.get(i).volume)};
stringList.add(row);
}
return stringList;
}
Saving to a CSV File
Saving the data from our grid back to CSV is essentially a reversal of the earlier process of reading the CSV. First, we provide a location to write the file. For this example, we're going to save the file to external storage, since this allows our saved CSV to potentially be used by other applications. (Internal storage is also be a possibility, but since this limits access to the saved CSV to our application, we won't pursue that option here.) Android requires that you give it permission to write to external storage by adding a line to the manifest:
<uses-permission android:name="android.permission.WRITE\_EXTERNAL\_STORAGE" />
We pass a List of string arrays into our save() method, which does most of the work in this Class:
public class SaveCSV {
File file;
public SaveCSV(File file){
this.file = file;
}
public void save(List<String[]> list){
if(!file.exists()){
try {
file.createNewFile();
} catch (IOException e) {
throw new RuntimeException("Unable to create File " + e);
}
}
try {
FileWriter writer = new FileWriter(file);
for(int i = 0; i < list.size(); i++){
String[] row = list.get(i);
for(int j = 0; j < row.length; j++)
{
writer.write(row[j]);
if(j != (row.length - 1)){
writer.write(',');
}
else{
writer.write('\\n');
}
}
}
writer.flush();
writer.close();
} catch (IOException e) {
throw new RuntimeException("Unable to write to File " + e);
}
}
}
Setting up the Activity
The final step is to configure our MainActivity so that it loads data into our FlexGrid when the application starts, and provide a button that allows us to save the modified grid back to CSV. I'm including a "built-in" stock.csv file, which I've saved to the internal application directory res\raw\stock.csv. This allows the application to load a copy of our CSV (packaged with the application) when there isn't a user-modified copy in external storage. Our code checks external storage first for an existing file; if none is found, it loads the internal raw resource file using:
InputStream in = getResources().openRawResource(R.raw.stock);
ReadCSV csv = new ReadCSV(in);
Google provides further information on both the storage options as well as saving files. Next, we'll add a button for saving the data from the grid back to CSV. This requires adding some logic in the onClickListener for file-saving. A more complete version of the onCreate() method will look like this:
private FlexGrid grid;
private Button saveButton;
private ObservableList<StockData> stockList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
com.grapecity.xuni.core.LicenseManager.KEY = License.KEY;
setContentView(R.layout.activity_main);
grid = (FlexGrid)findViewById(R.id.flexgrid);
saveButton = (Button)findViewById(R.id.button1);
final File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
final StockData stock = new StockData();
File file = new File(path, "stock.csv");
if(file.exists()){
InputStream input;
try {
input = new FileInputStream(file);
ReadCSV csv = new ReadCSV(input);
stockList = stock.getObservableList(csv.read());
grid.setItemsSource(stockList);
} catch (FileNotFoundException e) {
throw new RuntimeException("File not found" + e);
}
}
else{
InputStream in = getResources().openRawResource(R.raw.stock);
ReadCSV csv = new ReadCSV(in);
stockList = stock.getObservableList(csv.read());
grid.setItemsSource(stockList);
}
grid.setAutoGenerateColumns(true);
grid.setReadOnly(false);
GridColumn dateCol = grid.getColumns().getColumn("date");
dateCol.setFormat("M-dd-yyyy");
dateCol.setName("date/time");
grid.autoSizeColumns(0, grid.getColumns().size() - 1);
saveButton.setOnClickListener(new OnClickListener(){
public void onClick(View v){
path.mkdirs();
File file = new File(path, "stock.csv");
SaveCSV sCSV = new SaveCSV(file);
sCSV.save(stock.getStringList(stockList));
}
});
}
Now, running our app will look like this:
Conclusion
Xuni FlexGrid is perfect for visualizing the type of data normally contained in CSV files, and CSVs provide a quick, common avenue for sharing information. The Android platform offers a lot of flexibility in terms of saving and sharing information, so the three items together provide a very easy and open way of working with data. Xuni provides an easy way to visualize your data, and (as the above sample shows) can be a great mechanism for manipulating and editing as well.