Assignment:Exposing data over http lab2 Android Client

From Juneday education
Jump to: navigation, search

Preparations

Repeat the following Java chapters:

Check out the introduction material in our Android course:

This page is not intended as a course in Android, rather it is intended to make it easy to check out Android and hopefully motivate you to study Android.

Draft design

Overview

 +--------------------+
 | Systemet         : |
 |====================|
 | Pilsner Urquell    |
 |--------------------|
 | Zubr               |
 |--------------------|
 | Baron Trenk        |
 |--------------------|
 |                    |
 |--------------------|
 |                    |
 |--------------------|
 |                    |
 +--------------------+

Top and menu

For the top menu we use a normal ActionBar in Android. Not that we have a menu (:) up in the top-right corner

 +--------------------+
 | Systemet         : |
 |====================|

When clicking the menu we should see something like the following:

 +--------------------+
 | Systemet    |    : |
 |=============|search|
 |             |clear |
 |             +------+

Product list

For the list of products we use a ListView and a plain ArrayAdapter.

 | Pilsner Urquell    |
 |--------------------|
 | Zubr               |
 |--------------------|
 | Baron Trenk        |
 |--------------------|
 |                    |
 |--------------------|
 |                    |
 |--------------------|
 |                    |
 +--------------------+

Create new Android project

Name, domain and path

In Android Studio click File, New and New Project. You should see a window where you can enter:

  • name - chose what ever you want
  • domain - we've chosen our own domain, you can go for your whatever you want
  • go for the suggested path

and click Next.

Systemet-Android-start-01.png

Target platform

Choose Phone or tablet and click Next.

Systemet-Android-start-02.png

Activity

Go for the suggested "Empty Activity" and click Next.

Systemet-Android-start-03.png

Main page name

Next thing is to give the main activity a name, go for the suggested and click Next.

Systemet-Android-start-04.png

Layout and ListView

You should now see something like this:

Systemet-Android-design-01.png

Next thing is to add a ListView to the layout. In Android Studio, open the file activity_main.xml which is an XML file where you (can) specify the graphical user interface. This file can be edited either using the "Design" or "Text" view - go for the latter. Remove the TextView you got "for free". Add a ListView in the same spot as the TextView you just removed. The layout should look something like this instead:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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=".MainActivity">

  <ListView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/product_list">
  </ListView>
  
</android.support.constraint.ConstraintLayout>

A short explanation of this file might be nice, so let's try to explain what's going on. The file contains a layout (ConstraintLayout). In this layout we have a ListView. The listview is given an id product_list which we're going to use later on in our Java code.

Display fake products

Instead of writing code to display fetch, parse and create products and the display these it is easier if we simply present some fake products. To do this we need a product class.

Product class

Next thing to do is to create a class Product. Let's do this is a separate package (domain). We can use the Product class from the servlet part of this lab: Product.java.

In Android Studio:

  • make sure you use the "Project" view (left side of the Studio)
  • open the path to the Java file (MainActivity.java). SystemetApp -> app -> src -> main -> java -> se.juneday.systemetapp -> MainActivity.java
  • right-click the package se.juneday.systemetapp and click File, New, Package

Click File, Package and enter domain as package name and finally click OK. See screenshot below.

  • click the newly created package and (again) click File, New, Package. Enter Product and click OK. See screenshots below.
  • add the code below to the Product class (this is an edit of the code of Product.java).

Product class

package se.juneday.systemetapp.domain;

import java.util.Comparator;

public class Product {

  private String name;
  private double price; // SEK
  private double alcohol; // % alcohol by volume
  private int volume; // milliliters
  private int nr; // XML: <nr>nnn</nr> unique nr in the catalog
  private String productGroup; // e.g. <Varugrupp>Okryddad sprit</Varugrupp>
  private String type; // e.g. <Typ>Syrlig öl</Typ>

  /**
   * Defines a Builder for an Object.
   */
  public static class Builder {
    private String name;
    private double price; // SEK
    private double alcohol; // % alcohol by volume
    private int volume; // milliliters
    private int nr; // XML: <nr>nnn</nr> unique nr in the catalog
    private String productGroup; // e.g. <Varugrupp>Okryddad sprit</Varugrupp>
    private String type;

    /**
     * Provides the Product name
     * @param name The name for the Product to build
     * @return this Builder so that you can continue building
     */
    public Builder name(String name) {
      this.name = name;
      return this;
    }

    /**
     *
     */
    public Builder price(double price) {
      this.price = price;
      return this;
    }

    /**
     *
     */
    public Builder alcohol(double alcohol) {
      this.alcohol = alcohol;
      return this;
    }

    /**
     *
     */
    public Builder volume(int volume) {
      this.volume = volume;
      return this;
    }

    /**
     *
     */
    public Builder nr(int nr) {
      this.nr = nr;
      return this;
    }

    /**
     *
     */
    public Builder productGroup(String productGroup) {
      this.productGroup = productGroup;
      return this;
    }

    /**
     *
     */
    public Builder type(String type) {
      this.type = type;
      return this;
    }

    /**
     *
     */
    public Product build() {
      return new Product(this);
    }
  }

  /**
   * Constructs a Product using a Product.Builder
   *
   * @param builder The builder to use for building this Product
   */
  public Product(Builder builder) {
    this.name = builder.name;
    this.price = builder.price;
    this.alcohol = builder.alcohol;
    this.volume = builder.volume;
    this.nr = builder.nr;
    this.productGroup = builder.productGroup;
    this.type = builder.type;
  }

  /**
   * Constructs a new Product.
   * @param name The name of this Product
   * @param alcohol The alcohol level (in percent alcohol by weight) of this Product
   * @param price The price of this Product (in SEK)
   * @param volume The volume (in millilitres) of this product
   */
  public Product(String name, double alcohol, double price, int volume) {
    this.name = name;
    this.alcohol = alcohol;
    this.price = price;
    this.volume = volume;
  }

  /**
   * Returns the name of this Product
   * @return The name of this Product
   */
  public String name() { return name; }

  /**
   * Returns the alcohol level of this Product
   * @return The alcohol level of this Product
   */
  public double alcohol() { return alcohol; }

  /**
   * Returns the price of this Product
   * @return The price of this Product
   */
  public double price() { return price; }

  /**
   * Returns the volume of this Product
   * @return The volume of this Product
   */
  public int volume() { return volume; }

  /**
   * Returns the product group of this Product
   * @return The product group of this Product
   */
  public String productGroup() {
    return productGroup;
  }

  /**
   * Returns the product type of this Product
   * @return The product type of this Product
   */
  public String type() {
    return type;
  }

  /**
   * Returns the product number of this Product
   * @return The product number of this Product
   */
  public int nr() {
    return nr;
  }

  @Override
  public String toString() {
    return name;
  }

  /**
   * Uses name, alcohol, volume, price and nr
   * when deciding if this Product is equal to
   * another Product.
   *
   * Note: We could have settled with nr, since
   * it is the identity of a Product from the Systembolaget
   * source data (the XML file etc).
   */
  @Override
  public boolean equals(Object other) {
    if (other == null) {
      return false;
    }
    if (! (other instanceof Product)) {
      return false;
    }
    Product that = (Product)other;
    return this.name.equals(that.name) &&
        this.alcohol == that.alcohol &&
        this.volume == that.volume &&
        this.price == that.price &&
        this.nr == that.nr;
  }

  @Override
  public int hashCode() {
    // Using hash algorithm from
    // Joshua Bloch - Effective Java - on hashcodes
    int code = 17;
    code = 31 * code + name.hashCode();
    long alc = Double.doubleToLongBits(alcohol);
    code = 31 * code + (int) (alc ^ (alc >>> 32));
    code = 31 * code + volume;
    long pri = Double.doubleToLongBits(price);
    code = 31 * code + (int) (pri ^ (pri >>> 32));
    code = 31 * code + nr;
    return code;
  }
}

Screenshots

Add new domain package:

Systemet-Android-package.png

Add new Product class:

Systemet-Android-product-class.png

Systemet-Android-product-class-2.png

Fake products

Open the Java file (MainActivity.java) and add code that creates two faked products. Typically like this:

package se.juneday.systemetapp;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import java.util.List;
import se.juneday.systemetapp.domain.Product;

public class MainActivity extends AppCompatActivity {

  private List<Product> products;

  private void createFakedProducts() {
    products = new ArrayList<>();
    Product p1 = new Product.Builder()
        .alcohol(4.4)
        .name("Pilsner Urquell")
        .nr(1234)
        .productGroup("Öl")
        .type("Öl")
        .volume(330).build();
    Product p2 = new Product.Builder()
        .alcohol(4.4)
        .name("Baron Trenk")
        .nr(1234)
        .productGroup("Öl")
        .type("Öl")
        .volume(330).build();
      products.add(p1);
      products.add(p2);
  }

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

    // set up faked products
    createFakedProducts();
  }
}

Setup ListView

Next thing to do is to set up and use the ListView we created earlier in the XML file (activity_main.xml). The ListView is created in XML so ho do we get hold of a reference to the ListView object? We need to look it up using the method findViewById.

Add the following code as instance variables in the MainActivity class:

  private ListView listView;
  private ArrayAdapter<Product> adapter;

Find the ListView object, create an ArrayAdapter and connect them together. Add the following to the onCreate method:

  private void setupListView() {
    // look up a reference to the ListView object
    listView = findViewById(R.id.product_list);

    // create an adapter (with the faked products)
    adapter = new ArrayAdapter<Product>(this,
        android.R.layout.simple_list_item_1,
        products);

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

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

    // set up faked products
    createFakedProducts();

    // setup listview (and friends)
    setupListView();
  }

Build and run the app

Create a virtual device

This is the basic workflow (screenshots below)

  • Click Tools and AVD Manager
  • In the new windows, click Create Virtual Device...
  • Choose Phone, Nexus 6 and click Next
  • Choose x86 Images and click download of Oreo (27). You may have to wait a couple of minutes. Click Finish ...
  • Click Next
  • Give the device a fancy name, how about "Nexus 6 API 25", and click Finish

You can now go back to the normal Android Studio window.

Screenshots

Systemet-Android-avd-1.png

Systemet-Android-avd-2.png

Systemet-Android-avd-3.png

Systemet-Android-avd-4.png

Systemet-Android-avd-5.png

Build and install the app on the device

To build and install the app click the green "play button". In the pop up window, choose the newly created virtual device and click Ok. This starts up an emulated device and installs the app you've just written. Hopefully you'll see something like this:

Systemet-Android-app-v1.png

Add the menu

  • add a directory to the res (same level as the java folder) called menu. Right-click res and click new -> Directory. Name the new directory menu.
  • right-click on the newly created directory and click New -> Menu resource file
  • use actionbar_menu as name (for the menu layout)

Now you should have a menu resource file called actionbar_menu. Open this up and choose "Text" view (not "Design"). Add a menu item to the menu:

  <item
    android:id="@+id/action_search"
    android:title="Search">
  </item>

Note: the menu resource file may look different depending on the version of Android Studio.

This is not enough. You also need to add a method to the java file (MainActivity.java):

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.actionbar_menu, menu);

    return true;
  }

If you were to execute the app now, please feel free to do, you'd see the menu but nothing will happen if you click. So let's write some code that reacts to the click. Add the following to the java code:

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
      // action with ID action_refresh was selected
      case R.id.action_search:
        Log.d(LOG_TAG, "user presssed SEARCH");
        break;
      default:
        Log.d(LOG_TAG, "uh oh ;)");
        break;
    }
    return true;
  }

You can probably see that we're now using a class called Log. We're using this to see something in the so called Logcat when some presses the search menu item. You also nee to create the "tag" LOG_TAG. Do this by placing the cursor on the word LOG_TAG and click Alt-Enter and choose "Create constant field 'LOG_TAG'". Complete the code Android Studio writes for you so you get something like this:

  private static final String LOG_TAG = MainActivity.class.getSimpleName();
.

If you install the program to the virtual device and click the Search menu item you should something like this in the Logcat window:

2019-01-03 01:31:31.481 5789-5789/se.juneday.systemetapp D/MainActivity: user presssed SEARCH

Create a menu/view to enter search criteria

We need a separate window to enter search values. Let's start by making a sketch:

 +---------------
 | Min price    |
 |______________|
 | Max price    |
 |______________|
 | Min alco     |
 |______________|
 | Max alco     |
 |______________|
 +--------------+

This is easiest done with a so called LinearLayout and EditText (with hint texts) objects. Let's create this window:

  • right-click on the layout folder and choose New -> Layout Resource File (see screenshot below). Enter a name, e g search_dialog, for the layout
  • use the "Text" view (not the "Design" view) in Android Studio to edit the layout
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content">
  <EditText
    android:id="@+id/min_alco_input"
    android:inputType="numberDecimal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="16dp"
    android:layout_marginLeft="4dp"
    android:layout_marginRight="4dp"
    android:layout_marginBottom="4dp"
    android:hint="minimum alcohol, eg 4.0" />
  <EditText
    android:id="@+id/max_alco_input"
    android:inputType="numberDecimal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="16dp"
    android:layout_marginLeft="4dp"
    android:layout_marginRight="4dp"
    android:layout_marginBottom="4dp"
    android:hint="maximum alcohol, e g 5.0" />
  <EditText
    android:id="@+id/min_price_input"
    android:inputType="numberDecimal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="16dp"
    android:layout_marginLeft="4dp"
    android:layout_marginRight="4dp"
    android:layout_marginBottom="4dp"
    android:hint="minimum price, e g 12" />
  <EditText
    android:id="@+id/max_price_input"
    android:inputType="numberDecimal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="16dp"
    android:layout_marginLeft="4dp"
    android:layout_marginRight="4dp"
    android:layout_marginBottom="4dp"
    android:hint="maximum price, e g 19" />
</LinearLayout>

How do we display this layout? We have to write some code that creates and displays the search dialog. Let's write some code to read the text input and pass these values to a method that (soon) shall retriece products over the net:

  private static final String MIN_ALCO = "min_alcohol";
  private static final String MAX_ALCO = "max_alcohol";
  private static final String MIN_PRICE = "min_price";
  private static final String MAX_PRICE = "max_price";
  private static final String TYPE = "product_group";
  private static final String NAME = "name";


 // get the entered text from a view
  private String valueFromView(View inflated, int viewId) {
    return ((EditText) inflated.findViewById(viewId)).getText().toString();
  }

  // if the value is valid, add it to the map
  private void addToMap(Map<String, String> map, String key, String value) {
    if (value!=null && !value.equals("")) {
      map.put(key, value);
    }
  }

  private void showSearchDialog() {
    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setTitle("Search products");
    final View viewInflated = LayoutInflater
        .from(this).inflate(R.layout.search_dialog, null);

    builder.setView(viewInflated);

    builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        dialog.dismiss();
        // Create a map to pass to the search method
        // The map makes it easy to add more search parameters with no changes in method signatures
        Map<String, String> arguments = new HashMap<>();

        // Add user supplied argument (if valid) to the map
        addToMap(arguments, MIN_ALCO, valueFromView(viewInflated, R.id.min_alco_input));
        addToMap(arguments, MAX_ALCO, valueFromView(viewInflated, R.id.max_alco_input));
        addToMap(arguments, MIN_PRICE, valueFromView(viewInflated, R.id.min_price_input));
        addToMap(arguments, MAX_PRICE, valueFromView(viewInflated, R.id.max_price_input));

        // Given the map, s earch for products and update the listview
        searchProducts(arguments);
      }
    });
    builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        Log.d(LOG_TAG, " User cancelled search");
        dialog.cancel();
      }
    });
    builder.show();
  }

  private void searchProducts(Map<String, String> arguments) {
    // empty search string will give a lot of products :)
    String argumentString = "";

    // iterate over the map and build up a string to pass over the network
    for (Map.Entry<String, String> entry : arguments.entrySet())
    {
      // If first arg use "?", otherwise use "&"
      // E g:    ?min_alcohol=4.4&max_alcohol=5.4
      argumentString += (argumentString.equals("")?"?":"&")
          + entry.getKey()
          + "="
          + entry.getValue();
    }
    // print argument 
    Log.d(LOG_TAG, " arguments: " + argumentString);

    // search for products later on :)
  }

Ok, all fine... but when to display the dialog? Do you remember the menu item ("Search") we entered earlier? Let's rewrite that code slightly:

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
      // action with ID action_refresh was selected
      case R.id.action_search:
        Log.d(LOG_TAG, "user presssed SEARCH");
        showSearchDialog();
        break;
      default:
        Log.d(LOG_TAG, "uh oh ;)");
        break;
    }
    return true;
  }

Note: the hard coded values for the suggested input really should be done using Resources, but in this Android workshop we'll settle for hard code values.


TODO:


Screenshots

Create new layout:

Systemet-Android-search-layout.png

Retrieve products over a network

We're going to use Volley to do the networking for us. We can't be sure that the response from the server (typically the servlet you developed in previous parts of this lab) is quick so we really need to make the GUI responsive (a pleasant user experience) so we need to do the search in the background. This is one of the benefits of using Volley. With Volley you can request some data over a network and get on with your business (pleasing the user with a responsive GUI) and Volley will call you back when Volley gets data.

Faked search

Before we proceed with Volley we would like to make sure that we can update the ListView. So instead of searching we could alter the list of products, and yes you guessed right, by faking some products. Let's add some code to searchProducts:

  private void searchProducts(Map<String, String> arguments) {
    // empty search string will give a lot of products :)
    String argumentString = "";

    // iterate over the map and build up a string to pass over the network
    for (Map.Entry<String, String> entry : arguments.entrySet())
    {
      // If first arg use "?", otherwise use "&"
      // E g:    ?min_alcohol=4.4&max_alcohol=5.4
      argumentString += (argumentString.equals("")?"?":"&")
          + entry.getKey()
          + "="
          + entry.getValue();
    }
    // print argument
    Log.d(LOG_TAG, " arguments: " + argumentString);

    // search for products later on :)
    // Add one for now
    Product p = new Product.Builder()
        .alcohol(4.4)
        .name("Budvar")
        .nr(1234)
        .productGroup("Öl")
        .type("Öl")
        .volume(330).build();
    products.add(p);
    adapter.notifyDataSetChanged();
  }

You should be seeing something like this (after having "searched"):

Systemet-Android-search-faked.png

Real search using Volley

Let's start with setting up Volley. Follow the instructions at Network_using_Volley to update AndroidManifest.xml and build.gradle (Module:app) and rebuild the project.

We're starting of with a parser for the JSON reponse containing products:

  private List<Product> jsonToProducts(JSONArray array) {
    Log.d(LOG_TAG, "jsonToProducts()");
    List<Product> productList = new ArrayList<>();
    for (int i = 0; i < array.length(); i++) {
      try {
        JSONObject row = array.getJSONObject(i);
        String name = row.getString("name");
        double alcohol = row.getDouble("alcohol");
        double price = row.getDouble("price");
        int volume = row.getInt("volume");

        Product m = new Product(name, alcohol, price, volume);
        productList .add(m);
        Log.d(LOG_TAG, " * " + m);
      } catch (JSONException e) {
        ; // is ok since this is debug
      }
    }
    return productList;
  }

This is the method we should use when Volley is ready with fetching data from a server. We have setup a server (http://rameau.sandklef.com:9090) but we don't guarantee that this server is up so we suggest you connect to a server/servlet you're executing on your computer. You can not connect from Android to localhost:8080 since this (on the Android phone) refers to the localhost which is the actual phone rather than your computer. Instead you should connect to http://10.0.2.2:8080. The servlet should be started as before connecting to localhost. The Android tools (based on qemu) sets up the network so you can connect from the Android device to your computer.

Let's write some Volley code to fetch products using the search criteria that the user supplied:

  private void searchProducts(Map<String, String> arguments) {
    // empty search string will give a lot of products :)
    String argumentString = "";

    // iterate over the map and build up a string to pass over the network
    for (Map.Entry<String, String> entry : arguments.entrySet())
    {
      // If first arg use "?", otherwise use "&"
      // E g:    ?min_alcohol=4.4&max_alcohol=5.4
      argumentString += (argumentString.equals("")?"?":"&")
          + entry.getKey()
          + "="
          + entry.getValue();
    }
    // print argument
    Log.d(LOG_TAG, " arguments: " + argumentString);

    RequestQueue queue = Volley.newRequestQueue(this);
    String url = "http://rameau.sandklef.com:9090/search/products/all/" + argumentString;
    Log.d(LOG_TAG, "Searching using 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()");
            products.clear();
            products.addAll(jsonToProducts(array));
            adapter.notifyDataSetChanged();
          }
        }, 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);
  }


When using the search dialog and enter data something like this:

Systemet-Android-search-dialog.png

you should get some nice products shown in your app:

Systemet-Android-search-results.png

Display product information in separate window

If the user clicks on a product item in the list a separate window (Activity) should be displayed with more information about the product. To achieve this we need to do a couple of things:

  • register to listen for clicks in the list
  • create a new Activity
  • launch new Activity (when item clicked)

Register to listen for clicks

This is fairly simple. All we do is tell the ListView that we would like to get notified when the user clicks on an item by calling the method setOnItemClickListener. As an argument to this method we need to pass an object (who's class is) implementing the interface ListView.OnItemClickListener. We do this by creating an anonymous inner class at the end of the method setupListView:

    listView.setOnItemClickListener(new ListView.OnItemClickListener() {
      @Override
      public void onItemClick(AdapterView<?> parent,
          final View view,
          int position /*The position of the view in the adapter.*/,
          long id /* The row id of the item that was clicked */) {
        Log.d(LOG_TAG, "item clicked, pos:" + position + " id: " + id);
      }
    });

Note: we could have done the above using a separate class and object or with a lambda expression. We're settling for an inner class here - since we would like to repeat the interface concept.

To verify that this works we suggest you execute the app again (in the android device) and make sure you see the log message (in the Logcat window) when you click on an item in the list. You should see something like this:

2019-01-03 19:01:01.142 11061-11061/se.juneday.systemetapp D/MainActivity: item clicked, pos:1 id: 1

Create new Activity

Android Studio helps us with a lot of the details when creating a new Activity - after all it was quite easy creating your first activity (your MainActivity) wasn't it?

Mark a file in Android Studio, typically MainActivity.java.

Click File -> Activity -> Empty Activity. See screenshot below.

In the pop up window fill in a name for the new Activity, why not go for ProductActivity, and click Finish. See screenshot below.

Open the layout for this class, typically by putting the cursor/market on the word R.layout.activity_product and click Ctrl-b. Make sure you're using the "Text" view rather than "Design" view. Change the layout to a LinearLayout and add some TextViews to your layout. Like this:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical" android:layout_width="match_parent"
  android:layout_height="match_parent">

  <TextView
    android:id="@+id/product_name"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

  <TextView
    android:id="@+id/product_volume"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

  <TextView
    android:id="@+id/product_price"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

  <TextView
    android:id="@+id/product_alcohol"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />


</LinearLayout>

And we need some Java code to display the Product:

package se.juneday.systemetapp;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.Html;
import android.util.Log;
import android.widget.TextView;
import se.juneday.systemetapp.domain.Product;

public class ProductActivity extends AppCompatActivity {

  private static final String LOG_TAG = ProductActivity.class.getSimpleName();

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

    // extract the Product pass in the bundle
    Bundle extras = getIntent().getExtras();
    Product p = (Product) extras.get("product");
    // display the product
    displayProduct(p);
  }

  private void setViewText(int viewId, String label, String text) {
    TextView tv = findViewById(viewId);
    tv.setText(Html.fromHtml("<b>"+label+"</b>: " + text));
    Log.d(LOG_TAG, " * " + text);
  }

  private void displayProduct(Product product) {
    setViewText(R.id.product_name, "Name", product.name());
    setViewText(R.id.product_volume, "Volume", String.valueOf(product.volume()));
    setViewText(R.id.product_alcohol, "Alcohol", String.valueOf(product.alcohol()));
    setViewText(R.id.product_price, "Price", String.valueOf(product.price()));
  }

}

Screenshots

New Activity dialog:

Systemet-Android-new-activity.png

Fill in name etc in Activity dialog:

Systemet-Android-new-activity-2.png

Launch new Activity

First of all we need to make the Product class possible to pass. The easiest way to do this is to (rather relaxed at first) make the Product class implement Serializable. For now, modify the Product class just a tiny bit:

public class Product implements Serializable {

And then we need to, in the MainActivity, create a so called Intent object and pass that intent to the startActivity method:

  private void setupListView() {
    // look up a reference to the ListView object
    listView = findViewById(R.id.product_list);

    // create an adapter (with the faked products)
    adapter = new ArrayAdapter<Product>(this,
        android.R.layout.simple_list_item_1,
        products);

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

    listView.setOnItemClickListener(new ListView.OnItemClickListener() {
      @Override
      public void onItemClick(AdapterView<?> parent,
          final View view,
          int position /*The position of the view in the adapter.*/,
          long id /* The row id of the item that was clicked */) {
        Log.d(LOG_TAG, "item clicked, pos:" + position + " id: " + id);
        Product p = products.get(position);
        Intent intent = new Intent(MainActivity.this, ProductActivity.class);
        intent.putExtra("product", p);
        startActivity(intent);
      }
    });
  }

Hopefully you will something like this when you click an item (Product) in the list:

Systemet-Android-productactivity.png

... and yes, we're quite sure this app will not win any awards when it comes to nice design. When it comes to nice design and such stuff the authors would like to quote an English philosopher:

  Don't look to me for answers
  Don't ask me, I don't know

Source code

The source code above, with some minor changes (see below), can be found over at github: SystemetAppBasic. The purpose of this workshop is to write the application from scratch by following the instructions above and we strongly recommend you do so.

Using git

You can import, however we do not recommend or support it, this project into Android Studio by following the the steps below:

  • File --> New --> Project from Version Control --> GitHub
  • Git Repository URL: https://github.com/progund/SystemetAppBasic.git
  • Parent directory: chose any place .... probably ok to go wit the suggested directory
  • Directory name: SystemetAppBasic

After this you have two options:

1. Set an environment variable available to Android Studio (you may need to restart it for it to read the variable) called ANDROID_HOME with the value /the-path-to/your-user/Android/Sdk - note that you'll have to figure out the exact path. See here for more details.

2. Create a file in the project root directory and name it local.settings.

In that file, add the two following lines but with paths that work for your user:

ndk.dir=/the-path-to/your-user/Android/Sdk/ndk-bundle	
sdk.dir=/the-path-to/your-user/Android/Sdk

Again, you need to figure out the exact path.

Differencees

The package name for the app in the project above is se.juneday.systemetappbasic (not se.juneday.systemetapp as in this wiki).

TODO

  • explain that fake products is fake with regards to that they do not originate from Systemet
  • add a context menu to show details about a product
  • procedure to get Volley started (manifest, gradle)

Challaneges

  • sort product list before displaying it
  • make the tag "product" used when passing a Product to the ProductActivity be a final static String in one class
  • make it possible to search for the name of a product
  • store the latest search criteria (used in next search)