Android - client strategy

From Juneday education
Jump to: navigation, search

Videos

Android:Strategy (Full playlist) Android strategy (PDF)

The application

Backstory

Git is a version control system for source code. Source code needs to be controlled in versions, using some software, so that when more than one developer works on the same code, conflicts can be resolved etc.

On service on line for storing source code to be controlled with Git, is Github. Source code in a Git versioning system is stored in what is called "repositories". A repository is a container for source code for one project (or application).

Let's say that we want to build an app for getting statistics or information from such source code repositories.

What the app should be able to do

Present information about a github user’s repositories

  • Look up information at github
  • Present information in the Android application

Technical description

Here’s a rough overview of the application:

  +-------+                             +-------+
  |       |                             |       |
  |       |<----- JSON over http ------>|       |
  |       |                             |       |
  |       |                             |       |
  |       |                             |       |
  |       |                             |       |
  +-------+                             +-------+

 github.com                             Android
                                      application

Application at a glance

The application should, in short, do:

  • Fetch user data as JSON (over http)
  • Parse JSON and create domain objects
  • Present domain objects

Application details

Name: Git Repo Viewer

Package: se.juneday.gitrepoviewer

Repository information to display:

  • name
  • private or public
  • description
  • license

github.com

Github provides an API to retrieve information about the repositories users’ keep there.

Required preparations

If you want to get the most out of this stugg we suggest you check out:

Problems - and how to divide them

Too many things

Perhaps we will be doing too much at one time. It’s hard to know exactly what fails if we’re writting all the code at once. Let’s look at what parts we have:

  • Get data over network (JSON over http) using Volley
  • Parse data (JSON format) into domain objects
  • Present data

Slow turn around time

When developing for Android things take longer time than what you’ve been used to so far in our courses. When developing Java programs it is usually only:

  • compilation
  • launch program

and you can test again but in Android you need to:

  • compile
  • convert the java bytecode into DEX
  • uninstall the previous version on the device (emulated or physical)
  • transfer the new package to the device
  • install the new package
  • finally lauuch the application

This takes time so it is a bit cumbersome to develop and test. We need to do as much as possible to speed things up.

Let’s start dividing the problem into five smaller ones

Problem division (1) - parsing user data

Using the same library (org.json) as in Android we could develop and test the code that parses the JSON data locally. During this phase we will develop the so domain objects (e g the Repository class representing a repository and the information we would like to present).

Instead of retrieving data from from github using Java code we can put some JSON data in a String and parse that instead. Doing this we can focus entirely on parsing and not worry about network transfer.

Preparing a string with JSON data

Analysing the use data

Let’s look at the data from github. We can do this by either using a browser (i e Firefox or Chrome) or by using a command line tool such as wget and curl.

First of all, download the JSON file with information about the repositories:

$ curl -o progund-repos.json "https://api.github.com/orgs/progund/repos?per_page=400"

How many lines do we have in this file? We can use wc to do the counting:

$ wc -l progund-repos.json
8236 progund-repos.json

Uh oh, 8236 lines of JSON data. Seems as if there are tons of repositories in the JSON file. Can we keep just two repos? Let’s look at parts of the file first:

[
  {
    "id": 24987853,
    "node_id": "MDEwOlJlcG9zaXRvcnkyNDk4Nzg1Mw==",
    "name": "timelapse",
    "full_name": "progund/timelapse",
    "private": false,
    "owner": {
      "login": "progund",
      "id": 19474334,
      "node_id": "MDEyOk9yZ2FuaXphdGlvbjE5NDc0MzM0",
      "avatar_url": "https://avatars0.githubusercontent.com/u/19474334?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/progund",
      "html_url": "https://github.com/progund",
      "followers_url": "https://api.github.com/users/progund/followers",
      "following_url": "https://api.github.com/users/progund/following{/other_user}",
      "gists_url": "https://api.github.com/users/progund/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/progund/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/progund/subscriptions",
      "organizations_url": "https://api.github.com/users/progund/orgs",
      "repos_url": "https://api.github.com/users/progund/repos",
      "events_url": "https://api.github.com/users/progund/events{/privacy}",
      "received_events_url": "https://api.github.com/users/progund/received_events",
      "type": "Organization",
      "site_admin": false
    },
    "html_url": "https://github.com/progund/timelapse",
    "description": "Timelapse scripts (bash) for Raspberry PI",
    "fork": false,
    "url": "https://api.github.com/repos/progund/timelapse",
    "forks_url": "https://api.github.com/repos/progund/timelapse/forks",
    "keys_url": "https://api.github.com/repos/progund/timelapse/keys{/key_id}",
    "collaborators_url": "https://api.github.com/repos/progund/timelapse/collaborators{/collaborator}",
    "teams_url": "https://api.github.com/repos/progund/timelapse/teams",
    "hooks_url": "https://api.github.com/repos/progund/timelapse/hooks",
    "issue_events_url": "https://api.github.com/repos/progund/timelapse/issues/events{/number}",
    "events_url": "https://api.github.com/repos/progund/timelapse/events",
    "assignees_url": "https://api.github.com/repos/progund/timelapse/assignees{/user}",
    "branches_url": "https://api.github.com/repos/progund/timelapse/branches{/branch}",
    "tags_url": "https://api.github.com/repos/progund/timelapse/tags",
    .....
        "archived": false,
    "open_issues_count": 0,
    "license": {
      "key": "gpl-3.0",
      "name": "GNU General Public License v3.0",
      "spdx_id": "GPL-3.0",
      "url": "https://api.github.com/licenses/gpl-3.0",
      "node_id": "MDc6TGljZW5zZTk="
    },
    "forks": 0,
    "open_issues": 0,
    "watchers": 0,
    "default_branch": "master",
    "permissions": {
      "admin": false,
      "push": false,
      "pull": true
    }
  },
  {
  .....

From the above we can deduce that:

  • the repositories come in an array
  • each array element (a repo) contains
    • name
    • private
    • description
    • license (element with name of license)

Creating a string with JSON data

Manually creating a string

We can either delete the elements after the two first or we can use tools to do this. Let’s go for the second (a) it’s easier and b) we get to learn a new tool). We’re going to use jq. Extracting the first element from the JSON file is done like this:

$ jq '.[0]' progund-repos.json

If we put the following in a file:

  • [
  • the first element (jq '.[0]' progund-repos.json)
  • ,
  • the second element (jq '.[1]' progund-repos.json)
  • ]

We can do the above like this:

 echo "[" > tmp.json
 cat progund-repos.json | jq '.[0]' >> tmp.json
 echo "," >> tmp.json
 cat progund-repos.json | jq '.[1]' >> tmp.json
 echo "]" >> tmp.json

Now we have a file called tmp.json containing the first two elements as retrieved from github. This will do fine to test with. The number of lines in this file? Well, let's use wc again:

$ wc -l tmp.json 
205 tmp.json

205 lines. A lot easier to manage than 8000+ lines in the complete JSON file. Ok, we're all fine and ready to go? No, hold your horse... if we copy the content in this file to use in a string it will not work due to fact that both Java and JSON use ". So we need to escape the " to \" and also remove the newlines before we can copy/paste the value:

We can do the above like this:

 
cat tmp.json| sed -e 's,\",\\\",g' | tr '\n' ' ' > string.txt

The file string.txt now contains data that you can put in a String and pass that String to your parser method. The content of the file begins like this:

[ {   \"id\": 24987853,   \"node_id\": \"MDEwOlJlcG9zaXRvcnkyNDk4Nzg1Mw==\",   \"name\": \"timelapse\",   \"full_name\": \"progund/timelapse\",   \"private\": false,   \"owner\": {     \"login\": \"progund\",     \"id\": 19474334,     \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjE5NDc0MzM0\",     \"avatar_url\": \"https://avatars0.githubusercontent.com/u/19474334?v=4\",     \"gravatar_id\": \"\",     \"url\": \"https://api.github.com/users/progund\",     \"html_url\": \"https://github.com/progund\",     \"followers_url\": \"https://api.github.com/users/progund/followers\",     \"following_url\": \"https://api.github.com/users/progund/following{/other_user}\",     \"gists_url\": \"https://api.github.com/users/progund/gists{/gist_id}\",  
........

Note: we have cut away most of the content of the file in this wiki to keep this page as clean as possibly, so printing the content of your file will result in something different from this.

Automatically creating a string

Download and execute the file fetch_repos-info.sh if you want to automate the above.

Source code

Create a directory called local. The name local reflects that the files and directories in it are supposed to be used locally (not in Android - some will later on!). Let’s use the package as we should use later on in the application. This means that we need a directory structure (in the directory local) like this:

.
`-- se
    `-- juneday
        `-- gitrepoviewer
            |-- domain
            |   `-- Repository.java
            `-- util
                `-- JsonParser.java

We will use this code in Android later on so we’re going to keep our test code away from the above code. Let’s put the test code in a separate folder:

.
`-- test-code
    `-- se
        `-- juneday
            `-- gitrepoviewer
                |-- domain
                |   `-- test
                |       `-- RepositoryTest.java
                `-- util
                    `-- test
                        `-- JsonParserTest.java

Repository.java

package se.juneday.gitrepoviewer.domain;

public class Repository {

  /*
   *
   * enum for repository states (private/public)
   *
   */
  public enum RepoAccess {
    PRIVATE("private"),
    PUBLIC("public");
    private String accessName;
    RepoAccess(String name) {
      this.accessName = name;
    }
    public String accessName() {
      return this.accessName;
    }
  }
  
  private String name;
  private String description;
  private String license;
  private RepoAccess access;

  public Repository(String name,    String description,
                    String license, RepoAccess access) {
    this.name = name;
    this.access = access;
    this.description = description;
    this.license = license;
  }

  public String name() {
    return name;
  }

  public String description() {
    return description;
  }

  public String license() {
    return license;
  }
  
  public RepoAccess access() {
    return access;
  }
  
  @Override
  public String toString() {
    return new StringBuilder(name)
      .append(" - ")
      .append(description)
      .append(" (")
      .append(license)
      .append(", ")
      .append(access.accessName())
      .append(")")
      .toString();
  }
  
}

JsonParser.java

package se.juneday.gitrepoviewer.util;

import java.util.List;
import java.util.ArrayList;

import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;

import se.juneday.gitrepoviewer.domain.Repository;
import static se.juneday.gitrepoviewer.domain.Repository.RepoAccess;

public class JsonParser {

  public static List<Repository> parse(String json) {
    
    List<Repository> repos = new ArrayList<>();
    JSONArray jsonArray = null;

    try {
      jsonArray = new JSONArray(json);
    }  catch ( JSONException e ) {
      return repos;
    }

    for(int i = 0; i < jsonArray.length(); i++) {

      JSONObject jsonObject = null;

      try {
        jsonObject = jsonArray.getJSONObject(i);
      } catch ( JSONException e ) {
        continue;
      }

      //default values
      String name = null;
      String description = ""; 
      RepoAccess repoAccess = RepoAccess.PUBLIC;
      String license = ""; 

      try {
        name = jsonObject.getString("name");
      } catch ( JSONException e ) {
        continue;
      }      

      // extract description (if any)
      try {
        description = jsonObject.getString("description");
      } catch ( JSONException e ) {
        //System.err.println("warning: " + e.getMessage());
      }

      // extract repo access (private/public)
      try {
        boolean privateRepo = jsonObject.getBoolean("private");
        if (privateRepo) {
          repoAccess = RepoAccess.PRIVATE;
        }
      } catch ( JSONException e ) {
        continue;
      }      


      // extract license (if any)
      try {
        JSONObject licenseObject = jsonObject.getJSONObject("license");
        license = licenseObject.getString("name");
      } catch ( JSONException e ) {
        //System.err.println("warning: " + e.getMessage());
      }

      repos.add(new Repository(name,
                               description,
                               license,
                               repoAccess));
    }
    return repos;
  }

 
}

RepositoryTest.java

package se.juneday.gitrepoviewer.domain.test;

import java.util.List;

import se.juneday.gitrepoviewer.domain.Repository;
import se.juneday.gitrepoviewer.util.JsonParser;
import static se.juneday.gitrepoviewer.domain.Repository.RepoAccess;

public class RepositoryTest {

  public static void main(String[] args) {

    System.out.print("Creating repository:");
    Repository repo =
      new Repository("faked-name", "bla bla bla", "GPLv3", RepoAccess.PRIVATE);
    System.out.println("OK");

    System.out.println("Checking repository:");
    System.out.print(" * name: ");
    assert (repo.name().equals("faked-name")) : "Fail, name differs";
    System.out.println("OK");

    System.out.print(" * description: ");
    assert (repo.description().equals("bla bla bla")) : "Fail, description differs";
    System.out.println("OK");

    System.out.print(" * license: ");
    assert (repo.license().equals("GPLv3")) : "Fail, license differs";
    System.out.println("OK");

    System.out.print(" * access: ");
    assert (repo.access().equals("private")) : "Access differs";
    System.out.println("OK");

    
    System.out.println("RepositoryTest succeeded :)");
  }

  
}

JsonParserTest

package se.juneday.gitrepoviewer.util.test;

import java.util.List;

import se.juneday.gitrepoviewer.domain.Repository;
import se.juneday.gitrepoviewer.util.JsonParser;
import static se.juneday.gitrepoviewer.domain.Repository.RepoAccess;

public class JsonParserTest {

  public static void main(String[] args) {

    System.out.print("Parsing JSON String: ");
    List<Repository> repos = JsonParser.parse(jsonData);
    System.out.println("OK");

    System.out.print("Check nr of elements: ");
    assert (repos.size()==2) : "Fail, number of elements (" + repos.size() + ") not 2";
    System.out.println("OK");

    System.out.println("Check repositories: ");
    for (Repository r : repos) {
      System.out.print(" * check name: ");
      assert ( r.name() != null && r.name().length() > 0 ) : "Fail, name null or empty";
      System.out.println("OK");
      
      System.out.print(" * check access : ");
      assert ( r.access() != null
               &&
               ( r.access() == RepoAccess.PRIVATE ||
                 r.access() == RepoAccess.PUBLIC)) : "Fail, access null or faulty";
      System.out.println("OK");
      
      System.out.print(" * check description not null: ");
      assert ( r.description() != null ) : "Fail, description null";
      System.out.println("OK");
      
      System.out.print(" * check license not null: ");
      assert ( r.license() != null ) : "Fail, license null";
      System.out.println("OK");
      
    }
    
    System.out.println("JsonParserTest succeeded :)");
  }


  private static String jsonData = ""; // NOTE: this string must be replaced with the content in the file string.txt
  
}

Compile and test

You can use the script build-and-test.sh which will:

  • download the JSON jar if needed
  • compile android code
  • compile test code
  • execute tests

Or you can do the following manually:

Download JSON jar file:

$ wget 'https://search.maven.org/remotecontent?filepath=org/json/json/20171018/json-20171018.jar' -O org.json.jar

Compile all android files:

$ javac -cp org.json.jar se/juneday/gitrepoviewer/domain/Repository.java  se/juneday/gitrepoviewer/util/JsonParser.java

Compile and execute RespositoryTest:

$ javac -cp org.json.jar:. test-code/se/juneday/gitrepoviewer/domain/test/RepositoryTest.java
$ java -cp org.json.jar:test-code:. se.juneday.gitrepoviewer.domain.test.RepositoryTest
Creating repository:OK
Checking repository:
 * name: OK
 * description: OK
 * license: OK
 * access: OK
RepositoryTest succeeded :)

Compile and execute JsonParserTest:

$ javac -cp org.json.jar:. test-code/se/juneday/gitrepoviewer/util/test/JsonParserTest.java 
$ java -cp org.json.jar:test-code:. se.juneday.gitrepoviewer.util.test.JsonParserTest
Parsing JSON String: OK
Check nr of elements: OK
Check repositories: 
 * check name: OK
 * check access : OK
 * check description not null: OK
 * check license not null: OK
 * check name: OK
 * check access : OK
 * check description not null: OK
 * check license not null: OK
JsonParserTest succeeded :)

Conclusion

Even if it may feel cumbersome and not worth the while dividing the problem into smaller pieces if you had to use tools such as jq, sed, bash …. But you have to take our waords for it. It is worth it. It may be tough to grasp the whole thing but having a parser that you can test standalone (no android device) is a great thing. Imagine if github changes the structure of the JSON data? it will be a pieace of cake to rewrite with having to upload and install the app to a device each time you want to test a small change.

We have a JSONParser and a domain object. Nice, let’s proceed with the presentation in the Android app.

Problem division (2) - presenting user data

Presenting the repository information really has nothing to do with retrieving and parsing the data so let’s keep it that way. Let’s create some fake data (typically put some Repository objects in a List) and present these data.

Source code

Repository.java

Move the file Repository.java to the Android directory structure. No, don’t keep a copy for your self - move the file! And move (do not copy) the JSON parser file as well.

MainActivity.java

package se.juneday.gitrepoviewer;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import java.lang.reflect.Member;
import java.util.ArrayList;
import java.util.List;
import se.juneday.gitrepoviewer.domain.Repository;
import se.juneday.gitrepoviewer.domain.Repository.RepoAccess;

public class MainActivity extends AppCompatActivity {

  private ArrayAdapter<Repository> adapter;
  private List<Repository> repos = new ArrayList<>();

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

  private void populateWithFakedData() {
    repos.add(new Repository("dummy", "faked repo for test", "GPLv3", RepoAccess.PRIVATE));
    repos.add(new Repository("dummy again", "faked repo for test2", "GPLv3", RepoAccess.PUBLIC));
  }

  @Override
  public void onStart() {
    super.onStart();

    // fake data
    populateWithFakedData();

    // Lookup ListView
    ListView listView = (ListView) findViewById(R.id.repos_list);

    // Create Adapter
    adapter = new ArrayAdapter<Repository>(this,
        android.R.layout.simple_list_item_1,
        repos);

    // Set listView's adapter to the new adapter
    listView.setAdapter(adapter);
  }

}

The method populateWithFakedData adds faked repositories to the list of repositories.

main_activity.xml

We’re using a LinearLayout since we’re only having one element in the entire Activity. In this list we’re adding a ListView which we will use th present the repositories one by one in a scrollable list. If you want to read more about ListView, check out our page: Android:ListView.

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

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

</LinearLayout>

Problem division (3) - parsing and presenting

We have a user interface that can present repository data - at least faked data from a list. Remeber the JSON string we used earlier when testing the JSON parser. Let’s use that string and the JSON parser and present the resulting list.

Source code

MainActivity.java

package se.juneday.gitrepoviewer;

import android.content.Context;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
import java.lang.reflect.Member;
import java.util.ArrayList;
import java.util.List;
import se.juneday.gitrepoviewer.domain.Repository;
import se.juneday.gitrepoviewer.domain.Repository.RepoAccess;
import se.juneday.gitrepoviewer.network.VolleyRepositoryFetcher;
import se.juneday.gitrepoviewer.network.VolleyRepositoryFetcher.RepositioryChangeListener;
import se.juneday.gitrepoviewer.util.JsonParser;

public class MainActivity extends AppCompatActivity {

  private static final String LOG_TAG = MainActivity.class.getName();
  private ArrayAdapter<Repository> adapter;
  private List<Repository> repos = new ArrayList<>();

  private ListView listView;

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


  private void populateWithFakedData() {
    repos = JsonParser.parse(jsonData);
    Log.d(LOG_TAG, " repos: " + repos.size());
  //    repos.add(new Repository("dummy", "faked repo for test", "GPLv3", RepoAccess.PRIVATE));
  //  repos.add(new Repository("dummy again", "faked repo for test2", "GPLv3", RepoAccess.PUBLIC));
  }


  private void showToast(String msg) {
    Log.d(LOG_TAG, " showToast: " + msg);
    int duration = Toast.LENGTH_SHORT;
    Toast toast = Toast.makeText(this, msg, duration);
    toast.show();
  }

  private void resetListView(List<Repository> repos) {
    listView = (ListView) findViewById(R.id.repos_list);
    adapter = new ArrayAdapter<>(this,
        android.R.layout.simple_list_item_1, repos);
    listView.setAdapter(adapter);
  }

  @Override
  public void onStart() {
    super.onStart();

    // fake data
    populateWithFakedData();

    // Lookup ListView
    listView = (ListView) findViewById(R.id.repos_list);

    // Create Adapter
    adapter = new ArrayAdapter<Repository>(this,
        android.R.layout.simple_list_item_1,
        repos);

    // Set listView's adapter to the new adapter
    listView.setAdapter(adapter);

  }

  private static String jsonData = ""; // copy the content of the file string.txt to this string

}

We’ve added some Log calls to make it possible to check the progress in the Logcat window in Android Studio.

JsonParser.java

When compiling the JSON parser you will see a lot of compilation errors. This comes from the fact that Android have changed the Exception approach a bit. Let’s look at the inheritance structure of JSONException at both org.json and android :

https://stleary.github.io/JSON-java/

java.lang.Object
  java.lang.Throwable
    java.lang.Exception
      java.lang.RuntimeException
        org.json.JSONException

https://developer.android.com/reference/org/json/JSONException

java.lang.Object
  java.lang.Throwable
    java.lang.Exception
      org.json.JSONException

So in Android JSONException is a checked exception and in org.json it is a RuntimeException. We now need to rewrite the JSON parser a bit and handle all JSONException.

package se.juneday.gitrepoviewer.util;

import android.util.Log;
import java.util.List;
import java.util.ArrayList;

import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;

import se.juneday.gitrepoviewer.domain.Repository;
import static se.juneday.gitrepoviewer.domain.Repository.RepoAccess;

public class JsonParser {

  private static final String LOG_TAG = JsonParser.class.getName();

  public static List<Repository> parse(JSONArray jsonArray) {
    List<Repository> repos = new ArrayList<>();

    for(int i = 0; i < jsonArray.length(); i++) {
      Log.d(LOG_TAG, "parse() for: i: " + i);

      JSONObject jsonObject = null;

      try {
        jsonObject = jsonArray.getJSONObject(i);
      } catch ( JSONException e ) {
        continue;
      }

      //default values
      String name = null;
      String description = "";
      RepoAccess repoAccess = RepoAccess.PUBLIC;
      String license = "";

      try {
        name = jsonObject.getString("name");
      } catch ( JSONException e ) {
        continue;
      }

      // extract description (if any)
      try {
        description = jsonObject.getString("description");
      } catch ( JSONException e ) {
        //System.err.println("warning: " + e.getMessage());
      }

      // extract repo access (private/public)
      try {
        boolean privateRepo = jsonObject.getBoolean("private");
        if (privateRepo) {
          repoAccess = RepoAccess.PRIVATE;
        }
      } catch ( JSONException e ) {
        continue;
      }


      // extract license (if any)
      try {
        JSONObject licenseObject = jsonObject.getJSONObject("license");
        license = licenseObject.getString("name");
      } catch ( JSONException e ) {
        //System.err.println("warning: " + e.getMessage());
      }

      repos.add(new Repository(name,
          description,
          license,
          repoAccess));
    }
    return repos;
  }

  public static List<Repository> parse(String json) {
    Log.d(LOG_TAG, "parse()");

    JSONArray jsonArray = null;

    try {
      jsonArray = new JSONArray(json);
    }  catch ( JSONException e ) {
      Log.d(LOG_TAG, "can not create array. " + e);
      return new ArrayList<>();
    }

    return parse(jsonArray);
  }

 
}

We’ve added some Log calls to make it possible to check the progress in the Logcat window in Android Studio.

Making sure things work locally (not on a device)

Let’s make sure everything works locally to make sure we can rewrite and test our JSON parser locally. Since we have moved the files a bit we will end up with different command lines:

Compile the android files:

javac -cp org.json.jar ../app/src/main/java/se/juneday/gitrepoviewer/domain/Repository.java ../app/src/main/java/se/juneday/gitrepoviewer/util/JsonParser.java 
../app/src/main/java/se/juneday/gitrepoviewer/util/JsonParser.java:3: error: package android.util does not exist
import android.util.Log;
                   ^
../app/src/main/java/se/juneday/gitrepoviewer/util/JsonParser.java:22: error: cannot find symbol
      Log.d(LOG_TAG, "parse() for: i: " + i);
      ^
  symbol:   variable Log
  location: class JsonParser
../app/src/main/java/se/juneday/gitrepoviewer/util/JsonParser.java:79: error: cannot find symbol
    Log.d(LOG_TAG, "parse()");
    ^
  symbol:   variable Log
  location: class JsonParser
../app/src/main/java/se/juneday/gitrepoviewer/util/JsonParser.java:86: error: cannot find symbol
      Log.d(LOG_TAG, "can not create array. " + e);
      ^
  symbol:   variable Log
  location: class JsonParser
4 errors

Uh oh, there’s no Log class here. Should we scrap the idea of making it possible to test locally. No. Let’s create a simplified version of the Log class. How many different calls the Log do we do? One! Couldn’t be that hard?

Create a stub class

Let’s create a class test-code/android/util/Log.java like this:

test-code/android/util/Log.java

package android.util;

public class Log {

  public static void d(String tag, String s) {
    System.err.println("[" + tag + "]: " + s);
  }

}

A class like this, with the same name as another class and a new implementation of the API, is called a stub or a stubbed class. This is useful if you want to shortcut a big API and replace that with your own version which might come in handy when testing to give but one example. One example would be to replace some slow authorization over network with a stub always returning true (ok, login worked fine).

Use the stub class and compile again

Let us try to compile again (and adding test-code to the classpath):

$ javac -cp org.json.jar:test-code ../app/src/main/java/se/juneday/gitrepoviewer/domain/Repository.java ../app/src/main/java/se/juneday/gitrepoviewer/util/JsonParser.java

and let’s compile and execute the tests:

$ javac -cp org.json.jar:test-code:../app/src/main/java/ test-code/se/juneday/gitrepoviewer/util/test/JsonParserTest.java 
$ java -cp org.json.jar:test-code:../app/src/main/java/ se.juneday.gitrepoviewer.util.test.JsonParserTest
Parsing JSON String: [se.juneday.gitrepoviewer.util.JsonParser]: parse()
[se.juneday.gitrepoviewer.util.JsonParser]: parse() for: i: 0
[se.juneday.gitrepoviewer.util.JsonParser]: parse() for: i: 1
OK
Check nr of elements: OK
Check repositories: 
 * check name: OK
 * check access : OK
 * check description not null: OK
 * check license not null: OK
 * check name: OK
 * check access : OK
 * check description not null: OK
 * check license not null: OK
JsonParserTest succeeded :)

Ok, great we have a parser that can be used in Android and locally (without a device).

Problem division (4) - network with Volley

We’re going to use the Volley approach we call Volley code outside the Activity using Observer. Check out our page on how this works: Volley_code_outside_the_Activity_using_Observer

VolleyRepositoryFetcher.java

package se.juneday.gitrepoviewer.network;


import android.content.Context;
import android.util.Log;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonArrayRequest;
import com.android.volley.toolbox.Volley;
import java.util.ArrayList;
import java.util.List;
import org.json.JSONArray;
import se.juneday.gitrepoviewer.domain.Repository;
import se.juneday.gitrepoviewer.util.JsonParser;

public class VolleyRepositoryFetcher {

  private static final String LOG_TAG = VolleyRepositoryFetcher.class.getName();
  private static VolleyRepositoryFetcher fetcher;
  private Context context;
  private List<RepositioryChangeListener> listeners;


  public static synchronized VolleyRepositoryFetcher getInstance(Context context) {
    if (fetcher == null) {
      fetcher = new VolleyRepositoryFetcher(context);
    }
    return fetcher;
  }

  private VolleyRepositoryFetcher(Context context) {
    listeners = new ArrayList<>();
    this.context = context;
  }


  public void getRepositories() {
    RequestQueue queue = Volley.newRequestQueue(context);

    JsonArrayRequest jsonArrayRequest = new JsonArrayRequest(
        Request.Method.GET,
        "https://api.github.com/orgs/progund/repos?per_page=400",
        null,
        new Response.Listener<JSONArray>() {

          @Override
          public void onResponse(JSONArray array) {
            Log.d(LOG_TAG, " got data from Volley");
            List<Repository> repos = JsonParser.parse(array);
            for (RepositioryChangeListener r : listeners) {
              r.onRepositoryChange(repos);
            }
          }
        }, new Response.ErrorListener() {

      @Override
      public void onErrorResponse(VolleyError error) {
        Log.d(LOG_TAG, " cause: " + error.getCause().getMessage());
      }

    });

    // Add the request to the RequestQueue.
    queue.add(jsonArrayRequest);
  }


  /******************************************
   RepositioryChangeListener
   ******************************************/


  public interface RepositioryChangeListener {

    void onRepositoryChange(List<Repository> repos);
  }

  public void addRepositoryChangeListener(RepositioryChangeListener l) {
    listeners.add(l);

  }

}

MainActivity.java

package se.juneday.gitrepoviewer;

import android.content.Context;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
import java.lang.reflect.Member;
import java.util.ArrayList;
import java.util.List;
import se.juneday.gitrepoviewer.domain.Repository;
import se.juneday.gitrepoviewer.domain.Repository.RepoAccess;
import se.juneday.gitrepoviewer.network.VolleyRepositoryFetcher;
import se.juneday.gitrepoviewer.network.VolleyRepositoryFetcher.RepositioryChangeListener;
import se.juneday.gitrepoviewer.util.JsonParser;

public class MainActivity extends AppCompatActivity {

  private static final String LOG_TAG = MainActivity.class.getName();
  private ArrayAdapter<Repository> adapter;
  private List<Repository> repos ;

  private ListView listView;

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

    repos = new ArrayList<>();
  }


  /*
  private void populateWithFakedData() {
    repos = JsonParser.parse(jsonData);
    Log.d(LOG_TAG, " repos: " + repos.size());
  //    repos.add(new Repository("dummy", "faked repo for test", "GPLv3", RepoAccess.PRIVATE));
  //  repos.add(new Repository("dummy again", "faked repo for test2", "GPLv3", RepoAccess.PUBLIC));
  }
  */

  private void showToast(String msg) {
    Log.d(LOG_TAG, " showToast: " + msg);
    int duration = Toast.LENGTH_SHORT;
    Toast toast = Toast.makeText(this, msg, duration);
    toast.show();
  }

  private void resetListView(List<Repository> repos) {
    this.repos = repos;
    listView = (ListView) findViewById(R.id.repos_list);
    adapter = new ArrayAdapter<>(this,
        android.R.layout.simple_list_item_1, repos);
    listView.setAdapter(adapter);
    showToast("Displaying " + repos.size() + " repositories" );
  }

  @Override
  public void onStart() {
    super.onStart();

    // fake data
    /* populateWithFakedData();
     */

   // register to listen to member updates in VolleyMember
   VolleyRepositoryFetcher.getInstance(this).addRepositoryChangeListener(new RepositioryChangeListener() {
      @Override
      public void onRepositoryChange(List<Repository> repos) {
        resetListView(repos);
      }
    });


    // Lookup ListView
    listView = (ListView) findViewById(R.id.repos_list);

    // Create Adapter
    adapter = new ArrayAdapter<Repository>(this,
        android.R.layout.simple_list_item_1,
        repos);

    // Set listView's adapter to the new adapter
    listView.setAdapter(adapter);


    // Let's get all the repos and await an async "callback" to onRepositoryChange (above)
    VolleyRepositoryFetcher.getInstance(this).getRepositories();

  }

  /*
  private static String jsonData = ""; // not needed 
  */

}

Problem division (5) - integration

Fact is, we already have integrated all the code. Nothing more to do. But before we close this page we will go through what we have done:

  • We started out by writing classes for our domain objects (repositories) and a JSON parser. We developed and tested this locally (no android device).
  • After that we created a GUI that displayed a list of Repository objects.
  • We then proceeded with parsing prepared JSON data and displayed the resulting list of Repositories.
  • Having all these pieces in place we continued with actually retrieving the JSON data from github and presented that.

Challenge - Add caching

If you use an app that does not function after a network failure you'd probably throw it away - unless if there was a good reason for the app behaving like that. We would too. So let's add a bit of caching using the ObjectCache written by yours truly att Juneday.

It would be great to be able to simple do:

  • readObjectCache()
  • writeObjectCache(repos)

This is not something that ObjectCache offers so we need to add a small helper class providing these methods, a class dedicated to cache a List of Repository objects.

Use ObjectCache

Copy the files ObjectCache.java and ObjectCacheReader.java to your Android project. Make sure to add the files according to the package name (se.juneday).

Serialize Repository

First of all, let's make Repository serializable. Makethe first lines of the class look like this:

package se.juneday.gitrepoviewer.domain;

import java.io.Serializable;

public class Repository implements Serializable {

  private static final long serialVersionUID = 1L;

Repository cache

package se.juneday.gitrepoviewer.util;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import se.juneday.ObjectCache;
import se.juneday.gitrepoviewer.domain.Repository;

public class RepositoryCache {

  Context context;
  private ObjectCache<Repository> cache;
  private static RepositoryCache instance;
  private String className;
  private String LOG_TAG = RepositoryCache.class.getName();

  private RepositoryCache() {};

  public static RepositoryCache getInstance(Context context,String className) {
    if (instance==null) {
      instance = new RepositoryCache(context, className);
    }
    return instance;
  }

  private RepositoryCache (Context context, String className) {
    this.context = context;
    this.className = className;
  }

  private void initObjectCache() {
    // Create an ObjectCache instance - pass the Repository class as a
    // parameter (used to set the name of the file to write/cache to).
    PackageManager m = context.getPackageManager();
    String s = context.getPackageName();
    try {
      PackageInfo p = m.getPackageInfo(s, 0);
      s = p.applicationInfo.dataDir;
    } catch (PackageManager.NameNotFoundException e) {
      Log.d(LOG_TAG, "Error, could not build file name for serialization", e);
      return;
    }
    String fileName = s +
        "/" + className;

    cache = new ObjectCache<>(fileName);
  }

  public List<Repository> readObjectCache() {
    if (cache==null) {
      initObjectCache();
    }
    cache.pull();

    List<Repository> cachedRepos = cache.get();
    Log.d(LOG_TAG, "cached: " + ((cachedRepos==null)?0:cachedRepos.size()));
    if (cachedRepos!=null) {
      //      Log.d(LOG_TAG, " - using cache");
      return cachedRepos;
    }
    //Log.d(LOG_TAG, " - no cache, creating empty list");
    return new ArrayList<>();
  }

  public  void writeObjectCache(List<Repository> repos) {
    if (cache==null) {
      initObjectCache();
    }
    cache.set(repos);
    cache.push();
  }


}

Use the Repository cache in MainActivity

We need to make a few changes to our MainActivity class. We're pointing out the affected parts.

Add a private field:

  private static final String LOG_TAG = MainActivity.class.getName();
  private ArrayAdapter<Repository> adapter;
  private List<Repository> repos ;
  private RepositoryCache cache;

Add a method resetRepos:

  private void resetRepos(List<Repository> repos) {
    this.repos = repos;

    // GUI stuff
    listView = (ListView) findViewById(R.id.repos_list);
    adapter = new ArrayAdapter<>(this,
        android.R.layout.simple_list_item_1, repos);
    listView.setAdapter(adapter);
    showToast("Displaying " + repos.size() + " repositories" );

    // ObjectCache
    cache.writeObjectCache(repos);
  }

Initialise and use the cache in onStart():

  @Override
  public void onStart() {
    super.onStart();

    cache = RepositoryCache.getInstance(this, MainActivity.class.getCanonicalName());
    repos = cache.readObjectCache();

   // register to listen to member updates in VolleyMember
   VolleyRepositoryFetcher.getInstance(this).addRepositoryChangeListener(new RepositioryChangeListener() {
      @Override
      public void onRepositoryChange(List<Repository> repos) {
        Log.d(LOG_TAG, " filling cache with " + repos.size() + " repos");
        resetRepos(repos);
      }
    });

Compile and test locally - write more stub classes

Let's write a simple test for this class, for local execution (no android device). Here's a file se/juneday/gitrepoviewer/util/test/RepositoryCacheTest.java:

package se.juneday.gitrepoviewer.util.test;

import android.content.Context;

import java.io.File;
import java.util.List;
import java.util.ArrayList;

import se.juneday.ObjectCache;
import se.juneday.gitrepoviewer.domain.Repository;
import static se.juneday.gitrepoviewer.domain.Repository.RepoAccess;

import se.juneday.gitrepoviewer.util.RepositoryCache;


public class RepositoryCacheTest {

  private static String fileName = "repositories";


  
  public static void main(String[] args) {
    
    RepositoryCache cache;
    Context context = new Context();

    System.out.println("Deleting file: " + fileName);
    new File(fileName  + "_serialized.data" ).delete();

    System.out.print("Creating cache: ");
    cache = RepositoryCache.getInstance(context, fileName);
    assert (cache != null) : "Failed creating cache";
    System.out.println("OK");

    System.out.print("Reading cache: ");
    List<Repository> repos = cache.readObjectCache();
    assert (repos.size() == 0) : "Reading empty cache failed";
    System.out.println("OK");

    System.out.print("Creating cache: ");
    cache = RepositoryCache.getInstance(context, fileName);
    assert (cache != null) : "Failed creating cache";
    System.out.println("OK");

    System.out.print("Writing to cache: ");
    int reposToAdd = 1000;
    repos = new ArrayList<>();
    for (int i=0; i<reposToAdd ; i++) {
      repos.add(
                new Repository("faked-name-"+i, "bla bla bla"+i,
                               "GPLv3",
                               RepoAccess.PRIVATE));
    }
    cache.writeObjectCache(repos);
    System.out.println("OK");
    
    System.out.print("Creating cache: ");
    cache = RepositoryCache.getInstance(context, fileName);
    assert (cache != null) : "Failed creating cache";
    System.out.println("OK");

    System.out.print("Reading cache: ");
    repos = cache.readObjectCache();
    assert (repos.size() == reposToAdd) : "Reading cache with 1 element failed";
    System.out.println("OK repos size: " + repos.size());
  
  }

}

When compiling you'll get tons of errors on missing Android classes. Remember we had to write a stub class before? Same thing know. We need the following files:

test-code/android/
|-- app
|   `-- support
|       `-- v7
|           `-- AppCompatActivity.java
|-- content
|   |-- Context.java
|   `-- pm
|       |-- ApplicationInfo.java
|       |-- PackageInfo.java
|       `-- PackageManager.java
|-- os
|   `-- Parcelable.java
`-- util
    `-- Log.java

They need not be big or fancy. The following will do fine:

AppCompatActivity.java

package android.support.v7.app;

public class AppCompatActivity {
  
}

Context.java

package android.content;

import android.content.pm.PackageManager;

public class Context {

  static PackageManager manager =
    new PackageManager();
  
  public PackageManager getPackageManager() {
    return manager;
  }
 
  public String getPackageName() {
    return "";
  }
  
}

ApplicationInfo.java

package android.content.pm;

import android.os.Parcelable;


public class ApplicationInfo /* extends PackageItemInfo */ implements Parcelable {
  public static String dataDir = ".";

}


PackageInfo.java

package android.content.pm;

public class PackageInfo {

  public ApplicationInfo applicationInfo = new ApplicationInfo();
}

PackageManager.java

package android.content.pm;

public class PackageManager {

  public static PackageInfo pInfo = new PackageInfo();
  
  public PackageInfo getPackageInfo(String s, int nr) throws PackageManager.NameNotFoundException {
    return pInfo;
  }

  public static class NameNotFoundException extends /*Android*/Exception {
    public NameNotFoundException() {
      throw new RuntimeException("Stub!");
    }

    public NameNotFoundException(String name) {
      throw new RuntimeException("Stub!");
    }
  }

}

Reading cached data from a device using adhd

Download ADHD

download the script adhd.sh from here: adhd.sh.

Read data from the device

You can read the cached file from an android device (either rooted or an emulated device) using ADHD. Run the Repository Viewer application and make sure it loads repositories (either from network or from cache).

List the attached devices:

$ ./adhd.sh -ld
List of devices attached
emulator-5554	device

Only one device. Then life is easy. If you have several devices you have to use adhd's --device option, but with only one device attached life's easier. Let's look at the installed applications using ObjectCache:

$ ./adhd.sh -lsa 
 *** AVAILABLE APPS  ***
 * se.juneday.gitrepoviewer

and to read the cached data from the device (make sure to point out where the domain class (Repository) can be found using the -cp option):

$ ./adhd.sh -cp ../app/src/main/java/  se.juneday.gitrepoviewer serialized

Handling serialized file # 1: se.juneday.gitrepoviewer.MainActivity_serialized.data
========================================================
* Preparing download of /data/data/se.juneday.gitrepoviewer/se.juneday.gitrepoviewer.MainActivity_serialized.data: OK
* Downloading /mnt/sdcard/Download//se.juneday.gitrepoviewer.MainActivity_serialized.data:           OK
* Moving file se.juneday.gitrepoviewer.MainActivity_serialized.data:           OK

Converting serialized files to txt files
========================================================
 * creating adhd/apps/se.juneday.gitrepoviewer/se.juneday.gitrepoviewer.MainActivity.txt: OK

and finally to look at the data stored in the cache:

$ cat adhd/apps/se.juneday.gitrepoviewer/se.juneday.gitrepoviewer.MainActivity.txt
 * timelapse - Timelapse scripts (bash) for Raspberry PI (GNU General Public License v3.0, public)
 * computer-introduction - null (, public)
 * control-flow - null (, public)
 * our-first-java-program - null (GNU General Public License v3.0, public)
 * programming-introduction - null (GNU General Public License v3.0, public)
 * programming-in-java - null (GNU General Public License v3.0, public)
 * objects_in_java - Source code for the chapter Objects in Java (, public)
 * programming-with-c - null (GNU General Public License v3.0, public)
 * intro-java-assignment-1 - Files for the first assignment of the programming with Java book (, public)
 * lab2-staging - testing code for assn 2 - remove later (, public)
 * classes - null (, public)
 * java-programming-assignment2-public - The source code tree for the second assignment of the Programming in Java book (, public)
 * inheritance - null (GNU General Public License v3.0, public)
 * interfaces - null (GNU General Public License v3.0, public)
 * exceptions - Code from the Exceptions chapter of the book Programming with Java (, public)
 * design_patterns_introduction - Code and exercises from the chapter Design patterns introduction (from the book More programming with Java) (GNU General Public License v3.0, public)
 * design_patterns_builder - Source code from the Chapter Design Patterns - Builder (of the More programming with Java book) (GNU General Public License v3.0, public)
 * design-patterns-bi-directional-builder - Slides for the video lecture about the bi-directional builders example (More programming with Java) (, public)
 ....

Yes, we cut away the last couple of lines from the printout.