Workshop:SubstituteTeacherSchedulerAndroid

From Juneday education
Jump to: navigation, search

Background

Assignment:SubstituteTeacherScheduler

Preparing

We're going to use the following Design Patterns in this workshop. To be prepared get familiar with them by reading the linked pages:

The following (non newbie) concepts will be used, so get prepared by reading the pages below:

The following pages are a good read to get prepared:

The following Android pages are a good read to get prepared:

If this workshop is used to introduce Android you need not read the below pages. Otherwise, please do:

TODO

  • explain context (passed as arg)

Source code

You can find the complete source code to the Android client here: https://github.com/progund/substitute-scheduler-android

Server

We assume that you have a studied Assignment:SubstituteTeacherScheduler. This means you should have a servlet, serving JSON data to our Android client, up and running.

JSON data

Our Android client will request JSON data using a url as defined in Assignment:SubstituteTeacherScheduler.

Example url: http://localhost:8080/v1?substitute_id=2&day=2018-01-18&format=json:

Example JSON response:

[{
  "date": "2018-01-18 08:00:00",
  "school": {
    "address": "Kruthusgatan 17 411 04 Göteborg",
    "school_name": "Jensen"
  },
  "substitute": {"name": "Henrik"}
}]

You can get the JSON using tons of different clients.

Web browser

Simply enter the url in a web browser

curl

$ curl "http://localhost:8080/v1?substitute_id=2&day=2018-01-18&format=json"
[{
  "date": "2018-01-18 08:00:00",
  "school": {
    "address": "Kruthusgatan 17 411 04 Göteborg",
    "school_name": "Jensen"
  },
  "substitute": {"name": "Henrik"}
}]

wget

$ wget "http://localhost:8080/v1?substitute_id=2&day=2018-01-18&format=json" -o subst.log -O subst.json
$ cat subst.json 
[{
  "date": "2018-01-18 08:00:00",
  "school": {
    "address": "Kruthusgatan 17 411 04 Göteborg",
    "school_name": "Jensen"
  },
  "substitute": {"name": "Henrik"}
}]

w3m

$ w3m -dump "http://localhost:8080/v1?substitute_id=2&day=2018-01-18&format=json" 
[{
  "date": "2018-01-18 08:00:00",
  "school": {
    "address": "Kruthusgatan 17 411 04 Göteborg",
    "school_name": "Jensen"
  },
  "substitute": {"name": "Henrik"}
}]

lwp-request

$ lwp-request -m GET "http://localhost:8080/v1?substitute_id=2&day=2018-01-18&format=json" 
[{
  "date": "2018-01-18 08:00:00",
  "school": {
    "address": "Kruthusgatan 17 411 04 Göteborg",
    "school_name": "Jensen"
  },
  "substitute": {"name": "Henrik"}
}]

telnet

$ telnet localhost 8080
Trying ::1...
Connected to localhost.
Escape character is '^]'.
GET /v1?substitute_id=2&day=2018-01-18&format=json HTTP/1.1    

HTTP/1.1 200 OK
Server: Winstone Servlet Engine v0.9.10
Content-Type: application/json;charset=UTF-8
Connection: Close
Date: Thu, 05 Apr 2018 08:23:17 GMT
X-Powered-By: Servlet/2.5 (Winstone/0.9.10)

[{
  "date": "2018-01-18 08:00:00",
  "school": {
    "address": "Kruthusgatan 17 411 04 Göteborg",
    "school_name": "Jensen"
  },
  "substitute": {"name": "Henrik"}
}]
Connection closed by foreign host.

Android app

What is the app supposed to?

In short the app shall present data as retrieved from the server. breaking this down a bit, we get:

  • fetch data from server over a network
  • parse the server data (JSON format)
  • present the server data (UI)
  • provide search fields for user (UI)

Ideas to make the development easier

If we start with writing code for both the UI and the network and JSON parsing code it may be too big of a problem to solve a once. So we're going to split the code into two parts:

  • networking code
    • fetch data from server over a network
    • parse the server data (JSON format)
  • UI
    • present the server data (UI)
    • provide search fields for user (UI)

We will focus on the latter (the UI). In order to present some data we need some data. How can we have data without writing code to retrieve this? Well, the solution is simple - let's fake data.

Faked server data

Instead of having data as retrieved from the server we will have hard coded data. The advantages of doing this is:

  • we don't have bother with network code
  • we don't have bother with JSON parsing
  • we know exactly what data we have - checking that the UI presents that data is then an easy task

Roadmap for Android App

During this workshop w will follow the following schedule:

  1. design gui / mock up
  2. Standard Android app
  3. Create storage providing fake data
  4. Present data (provided by storage)
  5. Get 'all' JSON from server (using Volley)
  6. Search fields should
  7. Get JSON data from server using the search criteria

Design gui / mock up

Let's scribble something on a piece of paper first to get an idea of what we should implement. Either this is something we get from a customer or something you've done yourself. See picture to the right.

Sketch of gui to write..and some notes on what Views to use

Standard Android app

Create a default for mobile devices using Android Studio:

Create Android Project screen

  • Application name - Substitute Teacher App
  • Company domain - what ever you want, we're going for juneday.se
  • Project location - go for the one Android Studio suggests

Target Android Device screen

  • Phone or tablet - use API 27

Add an an activity to Mobile screen

  • Choose Empty Activity

Configure activity screen

  • Activity name - keep MainActivity
  • Layout name - keep activity_main

domain classes

The data we're getting from the server contains teachers, date and school. Let's use the same classes as in the client. We don't have to use the same classes or the same packages - the impoortant thing is that they reflect the data from the server and what we should present. Let's settle for copied (and in some cases slightly modified) versions of the classes on the server.

To create this class we suggest you:

  • create a package domain

and then proceed creating new classes as below.

Substitute

package se.juneday.substitutescheduler.domain;

public class Substitute {
    private String name;
    private int id;

    public Substitute(String name, int id) {
        this.name = name;
        this.id=id;
    }

    public String name() {
        return name;
    }

    public String toString() {
        return name;
    }

    public int id() {
        return id;
    }

}

Note: the server supply us with substitute teachers with an id so our class need the id. One can argue and say that id is not a property of real substitute teachers. Sure, but we're ignoring that discussion.

School

package se.juneday.substitutescheduler.domain;

public class School {

    private String name;
    private String address;

    public School(String name, String address) {
        this.name = name;
        this.address = address;
    }

    public String name() {
        return name;
    }

    public String address() {
        return address;
    }

}

Assignment

package se.juneday.substitutescheduler.domain;


public class Assignment {

    private Substitute teacher;
    private String date;
    private School school;

    /**
     * Creates a new Assignment
     * @param teacher This assignment's substitute teacher
     * @param date The date of this Assignment
     * @param school The school of this Assignment
     */
    public Assignment(Substitute teacher, String date, School school) {
        this.teacher = teacher;
        this.date = date;
        this.school = school;
    }

    /**
     * Returns this Assignment's teacher's name
     * @return This Assignment's teacher's name
     */
    public Substitute teacher() {
        return this.teacher;
    }

    /**
     * Returns the date of this Assignment
     * @return The date of this Assignment
     */
    public String date() {
        return this.date;
    }

    /**
     * Returns this Assignment's school
     * @return This Assignment's school
     */
    public School school() {
        return this.school;
    }

    /**
     * This Assignment, represented as a String
     * @return This Assignment represented as a String
     */
    @Override
    public String toString() {
        return new StringBuilder(date)
                .append(" (").append(teacher.name()).append(")")
                .append(" at ").append(school.name())
                .toString();
    }
}

Create storage class with faked data

We need a separate class for storing/fetching data. Let's go for the name AssignmentStore. The authors of the material you're reading would like to point out that we probably should have gone for an interface AssignmentStore. and a factory AssignmentStoreFactory and a class implementing the interface, typically WebAssignmentStore. But we would like to focus on Android here. Of course a cooler name for the class would be MasterOfReality as in the Black Sabbath album, but let's keep AssignmentStore.

To reduce complexity we will, as pointed out earlier, skip fetching data and instead provide a list of faked assignments. When things seem to be working nicely we can introduce the fetching of data over the web api. What data do we need to fetch (and in turn fake)? A real scenario would give us:

  • substitute teachers
  • assignments

The first (substitute teachers) is not currently provided by the web API, but we can pretend it will be in a near future so let's prepare for that. Fetching new substitute teachers is not something that we need to do every time we search for assignments so let's settle for two different fetch methods:

  • fetchAll() - fetches substitute teachers and all assignments
  • fetchAssignments(criterion) - fetches only assignments, given a criterion this method fetches data corresponding to the criterion

Fetching data from the internet may take a long time so we would like to do this asynchronously (using the Volley API, see Android Network). This will have implications on the class offering the service of fetching the data. Since this method will run in a separate thread (using Volley), not run in the GUI thread, we need a way to signal the GUI that we have new data. We'll do this in a separate Observer class, see below.

If we make this code a bit more java alike it will (or may) look something like this:

public void fetchAssignments(int id, String date) {
 ....
}

See below for complete code.

Note: we chose to use String as type for the date. We do this since it reflects the web api and the focus of this workshop is on writing a example (not perfect) client in Android.

Every time the method fetchAssignments in AssignmentStore is invoked to fetch (or fail doing doing so) new data from the server the method will notify the listener (in MainActivity) that there's new data. Along with this notification the store sends the assignments belonging to the specific fetch. Once the listener (in MainActivity) gets this notification we know that we have an up-to-date list of substitute assignments.

Observer interface

We assume you are now familiar with the pattern: Obersver design pattern. The method fetchAssignments in AssignmentStore should notify a registered listener about new data. To accomplish this we are using the Obersever pattern. We need to have an interface (which should be implemented by the listener, e g some part of the MainActivity class). In the storage class we should add a small interface, AssignmentListener with a method:

public void assignmentsReceived(List<Assignment> assignments);

See below for complete code.

Observer handler

The storage class AssignmentStore need to offer a way to register listeners (when new data has arrived). The easiest way to do this is to allow only one listener, so a method like this will do:

public void registerAssignmentListener(AssignmentListener listener) {
 ..
};

See below for complete code.

AssignmentStore

To create this class we suggest you:

  • create a package storage
  • create a class AssignmentStore in that package

Substitute teachers

We need to provide a list of substitute teachers. This is not something currently supported by the web API so we need to create are own - matching the list of teachers in the assignment we get from the web API. Let's create an instance variable of type List:

    private List<Substitute> subs;

Constructor

We only need one AssignmentStore so let's use the Singleton pattern to prevent creating more (than one).

    private static AssignmentStore instance;

    public static AssignmentStore getInstance() {
        if (instance==null) {
            instance = new AssignmentStore();
        }
        return instance;
    }

    private AssignmentStore() {

        // used to look up id for sub
        substituteMap = new HashMap<>();

        // Substitute teacher storage
        subs = new ArrayList<>();
        addFakeSubstitutes();
    }  // private to prevent instance creation

Later on in this workshop we need the AssignmentStore to have an instance variable containing a so called Context. We will add code to pass and store such a context this right now and discuss this below.

Add the following instance variable:

 private Context context;

and change the method getInstance to the following:

    public static AssignmentStore getInstance(Context context) {
        if (instance==null) {
            instance = new AssignmentStore(context);
        }
        return instance;
    }

and finally and change the constructor to the following:

    private AssignmentStore(Context context) {
        this.context = context;

        // Substitute teacher storage - and used to look up id for sub
        substituteMap = new HashMap<>();

        addFakeSubstitutes();
    }

Substitute teachers

We now need to:

  • create a list of substitute teachers
  • map Substitute teacher name and id

Since the JSON data comes with only names for substitute teachers and wour domain object Substitute has an instance variable id we need way to get an id from a name. We can go for either

  • a List which we keep sorted and loop through or do a binary search to find the id for a given name
  • a Map from which we simply get the id

The obvious choice is the latter so let's write some code:

  • create a map String,int
  • fill the map with substitute names and ids

Hmmm, do we a need a separate list to store the Substitute objects when we have a map? Actually we don't - we have all the information we need in the map. So let's remove the List and the code dealing with the list - always nice to remove code. Instead create the Map as follows:

    private Map<String, Substitute> substituteMap;

This way we can use the map as a mapping between names and id and at the same time expose the Substitute objects as a list using:

    public List<Substitute> substitutes() {
        return new ArrayList<Substitute>(substituteMap.values());
    }

Note: this only works if we have unique names for our substitute teachers.... but this is ok for now.

Ok, let's fill the map with the substitute data as retrieved from the server. We writing a separate private method for this.

    private void addFakeSubstitutes() {
        createSubstitute("Rikard", 1);
        createSubstitute("Henrik", 2);
        createSubstitute("Anders", 3);
        createSubstitute("Nahid", 4);
        createSubstitute("Conny", 5);
        createSubstitute("Svante", 6);
        createSubstitute("Elisabeth", 7);
        createSubstitute("Eva", 8);
        createSubstitute("Kristina", 9);
        createSubstitute("Bengt", 10);
    }

   private Substitute createSubstitute(String name, int id) {
        Substitute s = new Substitute(name, id);
        substituteMap.put(name, s);
        return s;
    }

    private Substitute createSubstitute(String name) {
        return new Substitute(name, idForSubstituteName(name));
    }

   // This method is compensating for the lack of id lookup
    private int idForSubstituteName(String name) {
        return substituteMap.get(name).id();
    }

Note: the names and ids are (and must be) exactly the same as on the server. In the future (in a cinema close to you) the server might provide a way to get the substitutes over the web API.

The constructor now looks (something) like this:

    // private to prevent instance creation
    private AssignmentStore() {

        // Substitute teacher storage - and used to look up id for sub
        substituteMap = new HashMap<>();

        addFakeSubstitutes();
    }

Dates

We don't know if this is the optimal solution, but we've decided that we shall get valid dates from the AssignmentStore. So let's add a method, dates, that does this:

    public List<String> dates() {
        List<String> dates = new ArrayList<>();
        for (int i = 15; i < 20; i++) {
            dates.add("2018-01-" + i);
        }
        return dates;
    }

Note: the dates returned here fit the dates in the database when we developed this course, you may need to check the dates in the database and change check the method accordingly.

Fetch assignments

We need to add some methods dealing with assignments:

  • public void fetchAll()
  • public void fetchAssignments(String id, String date)

A simple implementation of public void fetchAll() looks like:

    public void fetchAll() {
        // no need to fetch subs and dates ... server does not have 'em
        // fetchSubstituteTeachers();
        fetchAssignments("","");
    }


Let's create a List of faked assignments and use in public void fetchAssignments(String id, String date):

   public void fetchAssignments(String id, String date) {
        List<Assignment> assignments = new ArrayList<>();
        Random r = new Random();
        Log.d(LOG_TAG, "fetch()");

        // Create some fake assignments
        School school = new School("Bullshit Academy", "at home");
        for (int i=10;i<20;i++) {
            Substitute s = substitutes().get(r.nextInt(substitutes().size()));
            Assignment assignment = new Assignment(s, "2018-04-" + i, school);
            assignments.add(assignment);
        }

        // fake waiting for data from the web api
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // notify listener that new data has arrived
        if (listener!=null) {
            Log.d(LOG_TAG, "informing listener()");
            listener.assignmentsReceived(assignments);
        } else {
            Log.d(LOG_TAG, "No listener registered");
        }
    }

MainActivity - faked assignments

We should now add some code to the MainActivity class. The only thing we do in this time is to add code to fetch data and print it out.

Let's keep the onCreate method as it is and create a method onCreate which (as is onCreate) is invoked by Android when the app is starting up. The method should:

  • get hold of an AssignmentStore reference
  • register a listener - it's enough if the listener simply outputs the assignments to the console
  • call the fetchAll method in the onStart method

You also need to add an instance variable private AssignmentStore store; to the MainActivity class.

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

        Log.d(LOG_TAG, "onStart()");

        // get a store reference
        store = AssignmentStore.getInstance(this);

        // register listener
        store.registerAssignmentListener(data -> Log.d(LOG_TAG, "data recevied: " + data));

        // fetch data (arguments are ignored right now so don't bother about them
        store.fetchAll();
    }
}

Note: If you're seeing this Lambda expressions are not supported at this language level go to the text where the warning/error is marked with "red" code. Click Alt-Enter and choose Set language level to 8...

Present the assignments

Layout

If you feel like reading more about layout in Android, check out this page: Laying out components.

Let's scrap the layout Android Studio created for you and use the simpler LinearLayout instead. The layout should look something like this:

<?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.substitutescheduler.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</LinearLayout>

There's really no need for the "Hello world!" text so remove the following from the layout

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

Next thing is to add two so called Spinners. Read more about Spinners here: Android Spinner. Let's add two spinners to the layout:

<?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"
    android:orientation="vertical"
    android:paddingLeft="16dp"
    android:paddingRight="16dp"
    tools:context="se.juneday.substitutescheduler.MainActivity">


    <Spinner
        android:id="@+id/date_spinner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"></Spinner>

    <Spinner
        android:id="@+id/substitute_spinner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"></Spinner>

    <ListView
        android:id="@+id/assignments"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

    </ListView>


</LinearLayout>

Let's add some methods to handle data (items) to the spinners. We can do this in the MainActivity.

Add instance variables (to MainActivity):

    private String dateExpr = "";
    private String substituteExpr = "";

    private final static String ALL_DATES_FIELD = "All dates";
    private final static String ALL_SUBSTITUTES_FIELD = "All substitutes";

    private List<String> dates;
    private List<Substitute> substitutes;

Add instance methods (to MainActivity):

    private void updateViews(List<Assignment> assignments) {
        Log.d(LOG_TAG, "updateViews()");
        if (assignments != null) {

            // Assignments
            updateListView(assignments);

            // Spinners
            updateSubstituteSpinner();
            updateDateSpinner();

        } else {
            Log.d(LOG_TAG, "Failure ... fetching assignments");
        }
    }

    private void updateDateSpinner() {

        Log.d(LOG_TAG, "updateDateSpinner()");
        if (dates != null) {
            return;
        }
        dates = AssignmentStore.getInstance(this).dates();
        // Might be slow to add an item at pos 0 if the List is an ArrayList
        dates.add(0, ALL_DATES_FIELD);

        // Find spinner id (the spinner is defined in the XML file)
        Spinner dateSpinner = findViewById(R.id.date_spinner);

        ArrayAdapter<String> adapter =
                new ArrayAdapter<String>(this,
                        android.R.layout.simple_spinner_item,
                        dates);

        //  More space in spinner dropdown layout?, use
        //  adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

        // Set spinner's adapter (with data)
        dateSpinner.setAdapter(adapter);

        // Add listener to spinner items
        dateSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {

            public void onItemSelected(AdapterView<?> adapterView, View view, int pos, long l) {
                Log.d(LOG_TAG, "onItemSelected " + this);
                String s = (String) adapterView.getItemAtPosition(pos);
                if (ALL_DATES_FIELD.equals(s)) {
                    dateExpr = "";
                } else {
                    dateExpr = s;
                }
                Log.d(LOG_TAG, " s: " + s);
                // fetch assignments
                store.fetchAssignments(substituteExpr, dateExpr);
            }

            @Override
            public void onNothingSelected(AdapterView<?> adapterView) {
            }
        });
    }


    private void updateSubstituteSpinner() {
        Log.d(LOG_TAG, "updateSubstituteSpinner()");

        if (substitutes != null) {
            return;
        }

        // Find spinner id (the spinner is defined in the XML file)
        Spinner substitutueSpinner = findViewById(R.id.substitute_spinner);

        substitutes = store.substitutes();
        // Might be slow to add an item at pos 0 if the List is an ArrayList
        substitutes.add(0, new Substitute(ALL_SUBSTITUTES_FIELD, -1));

        ArrayAdapter<Substitute> adapter =
                new ArrayAdapter<Substitute>(this,
                        android.R.layout.simple_spinner_item,
                        substitutes);

        //  More space in spinner dropdown layout?, use
        // adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

        // Set spinner's adapter (with data)
        substitutueSpinner.setAdapter(adapter);

        // Add listener to spinner items
        substitutueSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {

            public void onItemSelected(AdapterView<?> adapterView, View view, int pos, long l) {
                Log.d(LOG_TAG, "onItemSelected " + this);
                Substitute s = (Substitute) adapterView.getItemAtPosition(pos);
                if (s.name().equals(ALL_SUBSTITUTES_FIELD)) {
                    substituteExpr = "";
                } else {
                    substituteExpr = "" + s.id();
                }

                Log.d(LOG_TAG, " s: " + s + "  --> " + substituteExpr);
                // fetch assignments
                store.fetchAssignments(substituteExpr, dateExpr);
            }

            @Override
            public void onNothingSelected(AdapterView<?> adapterView) {
            }
        });
    }

And lastly update the listview when getting notified. If you want to read more about ListView, check out his page: Android ListView.

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

        Log.d(LOG_TAG, "onStart()");

        updateDateSpinner();
        updateSubstituteSpinner();

        // register listener
        store = AssignmentStore.getInstance(this);
        store.registerAssignmentListener(data -> updateListView(data));

        // fetch data (arguments are ignored right now so don't bother about them
        store.fetchAll();

    }


    private void updateListView(List<Assignment> assignments) {
        Log.d(LOG_TAG, "updateListView()");
        if (assignments != null) {
            // Lookup ListView
            ListView listView = (ListView) findViewById(R.id.assignments);

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

            // Set listView's adapter to the new adapter
            listView.setAdapter(adapter);
        } else {
            Log.d(LOG_TAG, "Failure ... fetching assignments");
        }
    }

Complete source code (so far)

Expand using link to the right to see the full source code of storage/AssignmentStore.java, MainActivity.java and activity_main.xml .

storage/AssignmentStore.java

package se.juneday.substitutescheduler.storage;

import android.content.Context;
import android.util.Log;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import se.juneday.substitutescheduler.domain.Assignment;
import se.juneday.substitutescheduler.domain.School;
import se.juneday.substitutescheduler.domain.Substitute;

public class AssignmentStore {

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

    private AssignmentListener listener;
    private Context context;
    private static AssignmentStore instance;
    private Map<String, Substitute> substituteMap;

    public static AssignmentStore getInstance(Context context) {
        if (instance==null) {
            instance = new AssignmentStore(context);
        }
        return instance;
    }

    // private to prevent instance creation
    private AssignmentStore(Context context) {
        this.context = context;

        // Substitute teacher storage - and used to look up id for sub
        substituteMap = new HashMap<>();

        addFakeSubstitutes();
    }

    public interface AssignmentListener {
        public void assignmentReceived(List<Assignment> assignments);
    }

    public void registerAssignmentListener(AssignmentListener listener) {
        this.listener = listener;
    };

    public List<Substitute> substitutes() {
        return new ArrayList<Substitute>(substituteMap.values());
    }

    public void fetchAll() {
        // no need to fetch subs and dates ... server does not have 'em
        fetchAssignments("","");
    }

    public void fetchAssignments(String id, String date) {
        List<Assignment> assignments = new ArrayList<>();
        Random r = new Random();
        Log.d(LOG_TAG, "fetch()");

        // Create some fake assignments
        School school = new School("Bullshit Academy", "at home");
        for (int i=10;i<20;i++) {
            Substitute s = substitutes().get(r.nextInt(substitutes().size()));
            Assignment assignment = new Assignment(s, "2018-04-" + i, school);
            assignments.add(assignment);
        }

        // fake waiting for data from the web api
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // notify listener that new data has arrived
        if (listener!=null) {
            Log.d(LOG_TAG, "informing listener()");
            listener.assignmentReceived(assignments);
        } else {
            Log.d(LOG_TAG, "No listener registered");
        }
    }

    private Substitute createSubstitute(String name) {
        return new Substitute(name, idForSubstituteName(name));
    }

    // This method is compensating for the lack of id lookup
    private int idForSubstituteName(String name) {
        return substituteMap.get(name).id();
    }

    private Substitute createSubstitute(String name, int id) {
        Substitute s = new Substitute(name, id);
        substituteMap.put(name, s);
        return s;
    }

    private void addFakeSubstitutes() {
        createSubstitute("Rikard", 1);
        createSubstitute("Henrik", 2);
        createSubstitute("Anders", 3);
        createSubstitute("Nahid", 4);
        createSubstitute("Conny", 5);
        createSubstitute("Svante", 6);
        createSubstitute("Elisabeth", 7);
        createSubstitute("Eva", 8);
        createSubstitute("Kristina", 9);
        createSubstitute("Bengt", 10);
    }
}

MainActivity.java

package se.juneday.substitutescheduler;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import se.juneday.substitutescheduler.storage.AssignmentStore;

public class MainActivity extends AppCompatActivity {

    private static final String LOG_TAG = MainActivity.class.getName() ;
    private AssignmentStore store;

    private String dateExpr = "";
    private String substituteExpr = "";

    private final static String ALL_DATES_FIELD = "All dates";
    private final static String ALL_SUBSTITUTES_FIELD = "All substitutes";

    private List<String> dates;
    private List<Substitute> substitutes;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

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

        Log.d(LOG_TAG, "onStart()");

        updateDateSpinner();
        updateSubstituteSpinner();

        // register listener
        store = AssignmentStore.getInstance(this);
        store.registerAssignmentListener(data -> updateListView(data));

        // fetch data (arguments are ignored right now so don't bother about them
        store.fetchAll();

    }

    private void updateViews(List<Assignment> assignments) {
        Log.d(LOG_TAG, "updateViews()");
        if (assignments != null) {

            // Assignments
            updateListView(assignments);

            // Spinners
            updateSubstituteSpinner();
            updateDateSpinner();

        } else {
            Log.d(LOG_TAG, "Failure ... fetching assignments");
        }
    }

    private void updateDateSpinner() {

        Log.d(LOG_TAG, "updateDateSpinner()");
        if (dates != null) {
            return;
        }
        dates = AssignmentStore.getInstance(this).dates();
        // Might be slow to add an item at pos 0 if the List is an ArrayList
        dates.add(0, ALL_DATES_FIELD);

        // Find spinner id (the spinner is defined in the XML file)
        Spinner dateSpinner = findViewById(R.id.date_spinner);

        ArrayAdapter<String> adapter =
                new ArrayAdapter<String>(this,
                        android.R.layout.simple_spinner_item,
                        dates);

        //  More space in spinner dropdown layout?, use
        //  adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

        // Set spinner's adapter (with data)
        dateSpinner.setAdapter(adapter);

        // Add listener to spinner items
        dateSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {

            public void onItemSelected(AdapterView<?> adapterView, View view, int pos, long l) {
                Log.d(LOG_TAG, "onItemSelected " + this);
                String s = (String) adapterView.getItemAtPosition(pos);
                if (ALL_DATES_FIELD.equals(s)) {
                    dateExpr = "";
                } else {
                    dateExpr = s;
                }
                Log.d(LOG_TAG, " s: " + s);
                // fetch assignments
                store.fetchAssignments(substituteExpr, dateExpr);
            }

            @Override
            public void onNothingSelected(AdapterView<?> adapterView) {
            }
        });
    }


    private void updateSubstituteSpinner() {
        Log.d(LOG_TAG, "updateSubstituteSpinner()");

        if (substitutes != null) {
            return;
        }

        // Find spinner id (the spinner is defined in the XML file)
        Spinner substitutueSpinner = findViewById(R.id.substitute_spinner);

        substitutes = store.substitutes();
        // Might be slow to add an item at pos 0 if the List is an ArrayList
        substitutes.add(0, new Substitute(ALL_SUBSTITUTES_FIELD, -1));

        ArrayAdapter<Substitute> adapter =
                new ArrayAdapter<Substitute>(this,
                        android.R.layout.simple_spinner_item,
                        substitutes);

        //  More space in spinner dropdown layout?, use
        // adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

        // Set spinner's adapter (with data)
        substitutueSpinner.setAdapter(adapter);

        // Add listener to spinner items
        substitutueSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {

            public void onItemSelected(AdapterView<?> adapterView, View view, int pos, long l) {
                Log.d(LOG_TAG, "onItemSelected " + this);
                Substitute s = (Substitute) adapterView.getItemAtPosition(pos);
                if (s.name().equals(ALL_SUBSTITUTES_FIELD)) {
                    substituteExpr = "";
                } else {
                    substituteExpr = "" + s.id();
                }

                Log.d(LOG_TAG, " s: " + s + "  --> " + substituteExpr);
                // fetch assignments
                store.fetchAssignments(substituteExpr, dateExpr);
            }

            @Override
            public void onNothingSelected(AdapterView<?> adapterView) {
            }
        });
    }



    private void updateListView(List<Assignment> assignments) {
        Log.d(LOG_TAG, "updateListView()");
        if (assignments != null) {
            // Lookup ListView
            ListView listView = (ListView) findViewById(R.id.assignments);

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

            // Set listView's adapter to the new adapter
            listView.setAdapter(adapter);
        } else {
            Log.d(LOG_TAG, "Failure ... fetching assignments");
        }
    }

}

activity_main.xml

<?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"
    android:orientation="vertical"
    android:paddingLeft="16dp"
    android:paddingRight="16dp"
    tools:context="se.juneday.substitutescheduler.MainActivity">


    <Spinner
        android:id="@+id/date_spinner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"></Spinner>

    <Spinner
        android:id="@+id/substitute_spinner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"></Spinner>

    <ListView
        android:id="@+id/assignments"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

    </ListView>


</LinearLayout>

Fetch assignments for real

Instead of using faked assignments we're now going to get data from the server. We're going to do this using the Volley API (see our page on Volley). Read the page and make sure you follow the instructions at Preparing to use Volley. We're going to use Volley as in Volley code outside the Activity using Observer.

We need to change the method fetchAssignments in AssignmentStore to actually contact the server and get assignments (instead of returning faked assignments).

The web API accepts some parameters:

  • format
  • substitue_id
  • day

These parameters should be sent according to what the user have chosen in the Spinners. To reduced the code in the AssignmentStore we suggest you create a separate class, ServerSettings that deals with all his:

package se.juneday.substitutescheduler.storage;


public class ServerSettings {

    private static String SERVER_PROTO = "http";
    private static String SERVER_HOST = "10.0.2.2";
    private static String SERVER_PORT = "8080";
    private static String SERVER_PATH = "v1";

    private static String FORMAT = "format=json";

    private static String SERVER_PARAM_SUBSTITUTE = "substitute_id";
    private static String SERVER_PARAM_DAY = "day";

    public static String serverUrl(String id, String date) {
        String idParam = "";
        String dateParam = "";
        StringBuilder url = new StringBuilder();

        url
                .append(SERVER_PROTO)
                .append("://")
                .append(SERVER_HOST)
                .append(":")
                .append(SERVER_PORT)
                .append("/")
                .append(SERVER_PATH)
                .append("?")
                .append(FORMAT);

        if (date != null && (!date.equals(""))) {
            url
                    .append("&day=")
                    .append(date);
        }

        if (id != null && (!id.equals(""))) {
            url
                    .append("&substitute_id=")
                    .append(id);
        }

        return url.toString();
    }


}

So, now we can proceed implementing the method fetchAssignments in AssignmentStore

    public void fetchAssignments(String id, String date) {

        Log.d(LOG_TAG, "fetchAssignments()");

        RequestQueue queue = Volley.newRequestQueue(context);

        String url = ServerSettings.serverUrl(id, date);
        Log.d(LOG_TAG, " url: " + url);

        JsonArrayRequest jsonArrayRequest = new JsonArrayRequest(
                Request.Method.GET,
                url,
                null,
                new Response.Listener<JSONArray>() {

                    @Override
                    public void onResponse(JSONArray array) {
                        Log.d(LOG_TAG, "onResponse() " + array);
                        List <Assignment> assignments = jsonToAssignments(array);
                        listener.assignmentsReceived(assignments);
                    }
                }, new Response.ErrorListener() {

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

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

    }

    private List<Assignment> jsonToAssignments(JSONArray array) {
        List<Assignment> assignments = new ArrayList<>();
        for (int i = 0; i < array.length(); i++) {
            Log.d(LOG_TAG, " JSON parse " + i + " of " + array.length());
            try {
                JSONObject row = array.getJSONObject(i);

                JSONObject schoolJson = row.getJSONObject("school");
                School school = new School(schoolJson.getString("school_name"), schoolJson.getString("address"));

                JSONObject substituteJson = row.getJSONObject("substitute");
                Substitute substitute = createSubstitute(substituteJson.getString("name"));

                Log.d(LOG_TAG, " * " + substitute);

                String date = row.getString("date");
                Assignment assignment = new Assignment(substitute, date, school);

                assignments.add(assignment);
            } catch (JSONException e) {
                Log.d(LOG_TAG, " JSON parse exception " + e);
            }
        }
        return assignments;
    }

What's this context? Volley needs it so we to get it from somewhere. One solution, and the solution we're going to go for, is to pass a context to the AssignmentStore constructor. But hey, that constructor is private! Ok, so we hace to pass a context to the method getInstance method and store that context in a private (instance) variable. This means we need to alter the class AssignmentStore a bit. We're showing the relevant parts here below

public class AssignmentStore {

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

    private static AssignmentStore instance;

    private AssignmentListener listener;
    private Context context;
    private Map<String, Substitute> substituteMap;

    public static AssignmentStore getInstance(Context context) {
        if (instance==null) {
            instance = new AssignmentStore(context);
        }
        return instance;
    }

    // private to prevent instance creation
    private AssignmentStore(Context context) {
        this.context = context;

        // Substitute teacher storage - and used to look up id for sub
        substituteMap = new HashMap<>();

        addFakeSubstitutes();
    }

...... and so on.

Note: you need to pass this in MainActivity when invoking getInstance, like this: AssignmentStore.getInstance(this);

Challenge

  1. no assignments, how to update list???
  2. Toast
  3. Cache latest list objects
  4. Favorite searches
  5. Refresh all when swiping down activity

Design