Skip to content
Permalink
7669eae2ac
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time

XML and Gradle

Many of you have used virtual machines such as VMware Workstation Player or VirtualBox where one can setup several virtual operating systems in parallel on a single machine. In fact, Android uses similar technologies. In Android, each app runs within its own virtual machine a.k.a. sandbox. The Android system treats apps as different 'users', hence they need permissions to access sensors etc.

In order to produce functional apps, you need to design front-end GUI layouts and back-end actions. You also need to define app-level entry point and permissions etc. Eventually, all these are packed into an apk archive that can go to Google Play store, and eventually your users' screens:

XML (.XML) ==> Java (.JAVA) ==> ByteCode (.DEX) ==> App (.APK) ==> Runtime (DVM) ==> Screen

Lab 1 XML

First, let's look at how to provide and access resources such as layouts and strings using XML. You have used XMLs to design your apps previously, now we'll examine XMLs again in more details.

Import existing projects

The current lab is based on a previous project called 'My Car'. If that project is not available on your hard drive anymore, you need to do the following to download:

  1. There're two ways to checkout a Git repository:

    • Open a terminal window and navigate to the location where you want to save it. Then issue the following command to clone the whole repository into your hard drive git clone https://github.coventry.ac.uk/300CEM-1718SEPJAN/TEACHING-MATERIALS.git. Alternatively, go to https://github.coventry.ac.uk/300CEM-1718SEPJAN/TEACHING-MATERIALS, click 'Clone or download' and then 'Download Zip' to download the whole repository.

      Once downloaded, unzip and location the 'My Car' project under 'Week_02' folder. You might want to copy the project to somewhere handy for you.

    • Alternatively, open a terminal window and navigate to the location where you want to save the project. Then issue the following command svn checkout https://github.coventry.ac.uk/300CEM-1718SEPJAN/TEACHING-MATERIALS/trunk/Week_02_The_Java_language/MyCar. You'll see that the project is downloaded into the folder you have chosen.

  2. Rename the folder from 'MyCar' to 'MyXml', and then open it using Android Studio. When opening existing projects in Android Studio, first click 'Open an existing Android Studio project', navigate to where you saved the project and select file 'build.gradle', then click OK.

  3. Right-click on 'app' within the Project tool window, click Open Module Settings. Go to the Flavors tab, change Application Id from 'com.example.jianhuayang.mycar' to 'com.example.jianhuayang.myxml'

  4. Double click to open the string resource file, change app_name from 'My Car' to 'My Xml'.

    string

  5. Locate the long package name under Java folder (mine is 'com.example.jianhuayang.mycar'), right-click on it, select Refactor ==> Rename ==> Rename package. In the small window that pops up, type in 'myxml'. A tool windows called 'Find Preview' will show at the bottom of Android Studio, click 'Do Refactor'.

  6. From the menu bar, select Build ==> Clean Project, and then from the menu bar select File ==> Synchronize. This should clear out traces of the old project.

  7. To verify that you don't have anything in this current projects that relate to 'mycar' or 'my car', in the project tool window, right-click on app, then select Find in Path. Type in mycar, and hit enter. You'll see that indeed there're two occurrences. Change both to 'MyXml' or 'My Xml' as appropriate. Instead of using 'Find in Path', what you could do is to use 'Search Everywhere'. This is available if you hit 'shift' key twice.

    search_everywhere

XML basics - layouts

Open activity_main.xml under app ==> res ==> layout folder and switch to Text view mode. You have seen this layout file quite a few times already. Now let's have a closer examination of it.

  1. The very first line of the file i.e. <?xml version="1.0" encoding="utf-8"?> is called a prolog. It defines xml version and encoding. The prolog is optional. If it exists, it must come first in the document. This is automatically generated by Android Studio and you don't need to touch it.

  2. The second line of the xml defines a container layout called LinearLayout. This corresponds to a class called LinearLayout. In fact, all it does here in the xml file can be done in Java source code. But probably xml is easier as it's more visual.

  3. In XML, all tags come in pairs. If you look down the file, you'll see the closing tab of LinearLayout i.e. </LinearLayout> towards the end of the file. This is for container layouts where other elements can be contained in it. For layouts such as Button, you'll need to close the tag using />.

  4. XML is case-sensitive. If you use linearLayout at where LinearLayout is expected, the system will complain.

  5. xmlns stands for XML namespace. The purpose of a namespace in XML standard is to avoid conflicts. For example, in the current file, the word 'android' is set to be 'http://schemas.android.com/apk/res/android'. When the system parses the file, where it sees the word android it'll replace it with this long string.

  6. For Android, the namespace refers to http://schemas.android.com/apk/res/android, which is a URI (NOT URL). This cannot be changed.

  7. Attributes are required by the Java class to initialize the GUI on your screen. Note here although you use words such as 'wrap_content' or 'match_parent', these will, later on, be substituted with constants. For example wrap_content is equal to -1. See Android layout parameters for more info.

  8. In the project tool window, under 'res' folder there're several different sub-folders apart from layouts, such as mipmap and values. These are to be discussed later.

    XML Syntax Rules from w3school. Note this is for xml in general.

More XML - dimensions, strings, images

First, let's re-design the interface by removing some 'hard-coded' values and adding some more elements.

  1. Right-click values folder, select New ==> Values resource file. Select Dimension in Available qualifiers, name your file dimens.xml, save it.

  2. Open dimens.xml, insert the following into <resources> tags.

    <dimen name="margin_left">19dp</dimen>
    <dimen name="margin_top">5dp</dimen>
    <dimen name="margin_right">20dp</dimen>

    Here dp stands for density-independent pixels, which is a unit of measurement for UI elements. Typically sp (scale-independent pixels) is used for font sizes, and dp for everything else. Refer to here for a thorough explanation of different units avaiable in Android.

  3. Go back to activity_main.xml, replace '19dp' with '@dimen/margin_left', '20dp' with '@dimen/margin_right', '10dp' with '@dimen/margin_top'. Your screen should look similar to the following:

    tidy

  4. Open strings.xml, insert the following in between <resources> tags.

    <string name="button_car">Create car</string>
    <string name="button_diesel">Create diesel</string>

    Go back to activity_main.xml, replace android:text="Run Petrol" with android:text="@string/button_car". Similarly, replace android:text="Run Diesel" with android:text="@string/button_diesel".

  5. Insert the following into the activity_main.xml file, just before the inner LinearLayout. Now that you have two more EditTexts on your screen.

    <TextView
    android:id="@+id/labelValue"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="@dimen/margin_left"
    android:layout_marginTop="@dimen/margin_top"
    android:text="Puchase price (£):"
    android:textAppearance="?android:attr/textAppearanceSmall" />
    
    <EditText
    android:id="@+id/inputPrice"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="@dimen/margin_left"
    android:layout_marginRight="@dimen/margin_right"
    android:ems="10"
    android:hint="e.g. 35,000"
    android:inputType="number" />
    
    <TextView
    android:id="@+id/labelEngine"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="@dimen/margin_left"
    android:layout_marginTop="@dimen/margin_top"
    android:text="Engine size (litre):"
    android:textAppearance="?android:attr/textAppearanceSmall" />
    
    <EditText
    android:id="@+id/inputEngine"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="@dimen/margin_left"
    android:layout_marginRight="@dimen/margin_right"
    android:ems="10"
    android:hint="e.g. 1.4"
    android:inputType="numberDecimal" />

    Next, insert the following TextView just after the inner LinearLayout.

    <TextView
    android:id="@+id/textBlock"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_marginBottom="10dp"
    android:layout_marginTop="@dimen/margin_top"
    android:layout_weight="1"
    android:background="?android:attr/colorActivatedHighlight"
    android:scrollbars="vertical" />

    At this point your layout should look like this

    more_fields

  6. Go to https://material.io/icons/#ic_add, and click PNGS to download the 'plus' icon. In the same way, download the 'minus' icon at https://material.io/icons/#ic_remove.

    add

  7. Extract the zip file you just downloaded, go into the android folder in it. You'll see a set of folders whose names begin with 'drawable'. Copy all drawable folders and paste into MyXml/app/src/main/res. Repeat for both icons you downloaded. If the system asks whether you want to merge file, just merge.

    Now your res folder should look like this

    res

    If you go back to Android Studio, in the project tool window, the two icons have been added automatically

    drawable

Xml again - menus

All apps we saw so far are single-page apps, and app titles always appear in the top left corner. The area where the title stays is called 'App Bar', we can also put some more useful things in it.

  1. Make sure that the MainActivity class is a subclass of AppCompatActivity. If you downloaded your project files from the module Github, it is the case. But if you're using the project you created earlier yourself, you'll need to double check this.

    public class MainActivity extends AppCompatActivity {
  2. Open AndroidManifest.xml, replace android:theme="@style/AppTheme" with android:theme="@style/Theme.AppCompat.Light.NoActionBar". Open activity_main.xml, in the preview tool window click the Theme button to bring up the Theme selector. Then select AppCompat.Light.NoActionBar and click OK.

    theme

  3. Insert the following into activity_main.xml, just before the first TextView

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:background="?attr/colorAccent"
        android:elevation="4dp"
        android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

    This will create a toolbar floating below the title bar. It doesn't look very nice at the moment. Don't worry, we'll change it later.

  4. In the containing LinearLayout, remove all padding attributes. Now your opening tag of LinearLayout should look like

    <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:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.jianhuayang.myxml.MainActivity">
  5. Open MainActivity.java and insert the toolbar initialization code into the onCreate() callback function, so it looks like below

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar myToolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(myToolbar);
    }

    Now if you run the app you'll have something similar to below. Can you put some paddings around 'Type and Run' to make it look nicer? It is possible to get the toolbar in Java and do some customizations such as getSupportActionBar().setDisplayShowTitleEnabled(false). Have a look at Code Path tutorial on Using the App ToolBar.

    title_bar

  6. Under Android view, right-click on the res folder and select New ==> Android resource file. In the window that pops up, fill out info as below and click OK. You'll notice that in the Project tool window a new file called menu_main.xml is created in a folder named menu.

    menu

  7. Open menu_main.xml in text view, insert the following in between <menu> tags:

    <item
        android:id="@+id/menu_add"
        android:icon="@drawable/ic_add_black_24dp"
        android:orderInCategory="1"
        android:title="@string/menu_add"
        app:showAsAction="always|withText" />
    <item
        android:id="@+id/menu_clear"
        android:icon="@drawable/ic_remove_black_24dp"
        android:orderInCategory="10"
        android:title="@string/menu_clear"
        app:showAsAction="ifRoom" />
    <item
        android:id="@+id/action_settings"
        android:orderInCategory="100"
        android:title="@string/action_settings"
        app:showAsAction="never" />

    Note in the above oderInCategory sets the appearance order of that menu item -- smaller values appear first. For English language apps (left to right) that is on the left handside.

    You'll see immediately that some texts are highlighted in red. This is because the two string resources we are refering to are not available. Add the following to the string resource file.

    <string name="action_settings">Settings</string>
    <string name="menu_add">Add to list</string>
    <string name="menu_clear">Clear list</string>

    Insert the following into strings.xml in between <resources> tags. We'll need it for later.

    <integer name="depreciation">80</integer>
  8. Open MainActivity.java and insert the following two methods into the class

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }
    
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
    
        if (id == R.id.action_settings) {
            return true;
        }
        
        return super.onOptionsItemSelected(item);
    }

    These two are callback functions that belong to Activity class, as indicated by the @Override notation. The first method inflates the menu using menu resource 'R.menu.menu_main' which indicates the folder and xml file name. In the second method, we can define different actions according to which icon is being clicked. This is similar to our previous example where one method is capable of responding to several different button clicks.

    'The onCreate method is called first, and before it finishes onCreateOptionsMenu is called.'. Activity has a method invalidateOptionsMenu() which declares that the 'options menu has changed, so should be recreated' i.e. the onCreateOptionsMenu()method will be called the next time it needs to be displayed. Activity lifecycle is the topic of next week.

At this point, if you run the app you should have something similar to below:

menu_ready

Some house keeping tasks

Make the following changes to the MainActivity classes to accommodate the UI change.

  1. Open Vehicle.java and insert two new member variables and generate getter methods for both.

    private int price;
    private double engine;
  2. Change the constructor that takes two input parameter, make it take all four instead.

    public Vehicle(String make, int year, int price, double engine) {
        this.make = make;
        this.year = year;
        this.price = price;
        this.engine = engine;
        this.message = "Your car is a " + make + " built in " + year + ".";
        count();
    }
  3. Generate a getter method for member variable 'make'.

  4. Open Car.java, change Car constructor to

    public Car(String make, int year, String color, int price, double engine){
        super(make, year, price, engine);
        this.color = color;
        setMessage(getMessage() + " I like your shining " + color + " color.");
    }

    Similarly, Change Diesel constructor to

    public Diesel(String make, int year, int price, double engine){
        super(make, year, price, engine);
        this.type = "Diesel";
        }
  5. Change Car and Diesel initialization methods in ActivityMain.java to accommodate new variables. Your switch block should now look like below.

    switch (view.getId()) {
        case R.id.buttonRunPetrol:
            vehicle = new Car(make, intYear, color, price, engine);
            break;
        case R.id.buttonRunDiesel:
            vehicle = new Diesel(make, intYear, price, engine);
            break;
        default:
            vehicle = new Vehicle();
            break;
    }

    Now both constructors are capable of handling the new fields in the UI. You'll notice that both price and engine are red (i.e. something goes wrong). Don't worry about at the moment, we'll get those sorted later.

ArrayList, StringBuilder, Wrapper class, autoboxing

Next, we need to make some changes to MainActivity.java to handle menu button click and display a message back to our users.

  1. Declare variables that correspond to new UI element. These should be put together with declarations that are already in the class.

    private EditText editTextPrice;
    private EditText editTextEngine;
    private TextView textViewBlock;

    Next, remove the vehicle declaration in onButtonClick() function (i.e. Vehicle vehicle;). Declare a new Vehicle object as a member variable of the class (where to put it?). This will be used for the current vehicle on the screen.

    private Vehicle vehicle;

    In addition, declare an ArrayList that holds Vehicles the user adds through the menu, and a couple of other helper variables as member variables.

    private ArrayList<Vehicle> vehicleList = new ArrayList<>();
    // the diamond syntax: because the empty angle brackets have the shape of a diamond, "core java for the impatient" C. Horstmann    
    private StringBuilder outputs;
    private static Double depreciation;

    There're some new classes that you haven't seen before:

    • In Java, arrays are initialized with a fixed size. For example, int[] aVariable = new int[10] will declare an integer array called aVariable of size 10. But the problem is if later on you have more than 10 elements, it won't fit into this array, The way to get around this is to use the ArrayList class from java.util package. The ArrayList class extends AbstractList and implements the List interface. In the declaration above, in between angle brackets are types you want to hold using this ArrayList, in our case, this is the Vehicle class.

    • In Java, String objects are immutable. 'Immutable' here means once assigned the string itself cannot be changed. For example, consider the code in the following figure, this is possible in Python but not in Java. To get around this, Java introduced StringBuilder class, which is effectively a String class with mutable strings. We'll see how to use it later on. Checkout StackOverflow discussions on when to use String and when to use StringBuilder

      python_string

    • You have seen primitive data types such as int or double. In the codes above Double was used, which is not to be confused with double. Double here is a wrapper class for the primitive type double (there's also Integer for int etc.). The main reason that you need these wrapper classes is that these are objects, and primitive types are not. For example, you can declare ArrayList<Double> but not ArrayList<double>.

      Wrapper class can be initialized using proper object initialization methods (i.e. 'new' keyword). For example, Double a = new Double(100.00). Or it can be initialized like an ordinary primitive type, e.g. Double a = 100.00. This is called autoboxing. By definition, autoboxing refers to the automatic conversion that the Java compiler makes between the primitive types and their corresponding object wrapper classes. Check out StackOverflow discussions on when to use primitive types and when to use wrapper classes

  2. Initialization of these variables are inside method onButtonClick, you need to move it into onCreate. Also, insert initialization of new UI element. Your onCreate method should now look like this

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
    
        editTextMake = (EditText) findViewById(R.id.inputMake);
        editTextYear = (EditText) findViewById(R.id.inputYear);
        editTextColor = (EditText) findViewById(R.id.inputColor);
        editTextPrice = (EditText) findViewById(R.id.inputPrice);
        editTextEngine = (EditText) findViewById(R.id.inputEngine);
        textViewBlock = (TextView) findViewById(R.id.textBlock);
        textViewBlock.setMovementMethod(new ScrollingMovementMethod());
        depreciation = getResources().getInteger(R.integer.depreciation) / 100.00;
    }

    In the code above:

    • By now you've seen how to use XML to provide system resources such as layouts, dimensions, strings, integers. In the codes above you see how to access them. For example, R.layout.activity_main refers to the layout XML file 'activity_main.xml', and R.integer.depreciation refers to the depreciation rate we added in strings.xml.
    • R is a class automatically generated by Android Studio by collecting system resources available. In the system resource folder, i.e. the 'res' folder, folder name matters but file name does not. For example, you can rename your 'strings.xml' as 'stringsForSql.xml' and it'll still work. But if you rename the folder 'valuesForSql' it won't work.
    • Line textViewBlock.setMovementMethod(new ScrollingMovementMethod()); is to make TextView scrollable. This needs to work in pair with android:scrollbars="vertical" attribute in xml.
    • In Java, you need to be really careful with integer division. For example, 5/10 in Java will give you 0. This is because Java doesn't count decimal values if both operands are integers. To get around this, you can use 5/10.0, in which case both operands are automatically converted to higher precision (double) and will produce the desired output.
  3. Insert the following two lines into onButtonClick to initialize price and engine

    Integer price = new Integer(editTextPrice.getText().toString());
    Double engine = new Double(editTextEngine.getText().toString());

    Check your onButtonClick method, which should look like:

    public void onButtonClick(View view) {
        String make = editTextMake.getText().toString();
        String strYear = editTextYear.getText().toString();
        int intYear = Integer.parseInt(strYear);
        String color = editTextColor.getText().toString();
        Integer price = new Integer(editTextPrice.getText().toString());
        Double engine = new Double(editTextEngine.getText().toString());
    
        switch (view.getId()) {
            case R.id.buttonRunPetrol:
                vehicle = new Car(make, intYear, color, price, engine);
                break;
            case R.id.buttonRunDiesel:
                vehicle = new Diesel(make, intYear, price, engine);
                break;
            default:
                vehicle = new Vehicle();
                break;
        }
    
        if (Vehicle.counter == 5) {
            vehicle = new Vehicle() {
                @Override
                public String getMessage() {
                    return "You have pressed 5 times, stop it!";
                }
            };
        }
    
        Toast.makeText(getApplicationContext(), vehicle.getMessage(), Toast.LENGTH_SHORT).show();
        Log.d(TAG, "User clicked " + Vehicle.counter + " times.");
        Log.d(TAG, "User message is \"" + vehicle + "\".");
    }
  4. Amend onOptionsItemSelected(MenuItem item) so it looks like the following

    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
    
        switch (id) {
            case R.id.menu_add:
                addVehicle();
                return true;
            case R.id.menu_clear:
                return clearVehicleList();
            case R.id.action_settings:
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    The syntax looks familiar here - Depending on which button being clicked on the options menu, it performs diffeernt actions. In the default case, it calls the overridedn method in the super class.

  5. Insert the following two methods to the class. These two methods respond to option menu clicks so that the current 'vehicle' object can be added to the list, or to clear the list. In addition, we update the outputs for our user to see.

    private void addVehicle() {
        vehicleList.add(vehicle);
        resetOutputs();
    }
    
    private boolean clearVehicleList() {
        vehicleList.clear();
        resetOutputs();
        return true;
    }
  6. Insert the following method into the class.

    private void resetOutputs() {
        if (vehicleList.size() == 0) {
            outputs = new StringBuilder("Your vehicle list is currently empty.;");
        } else {
            outputs = new StringBuilder();
            for (Vehicle v : vehicleList) {
                outputs.append("This is vehicle No. " + (vehicleList.indexOf(v) + 1) + System.getProperty("line.separator"));
                outputs.append("Manufacturer: " + v.getMake());
                outputs.append("; Current value: " + depreciatePrice(v.getPrice()));
                outputs.append("; Effective engine size: " + depreciateEngine(v.getEngine()));
                outputs.append(System.getProperty("line.separator"));
                outputs.append(System.getProperty("line.separator"));
            }
        }
        textViewBlock.setText(outputs);
    }

    The idea of the code above is that once the 'add' button is pressed, the app will add the current vehicle to the list. Then it will go through the list one by one to collect info of each vehicle. The info collected will then be displayed back to the user in the big TextView area. Note that:

    • Vehicle v : vehicleList is called enhanced for loop. Normally the syntax in Java is for (int i = 0; i < vehicleList.size(); i++). But this enhanced for loop provides a handy shortcut.
    • v.getPrice() give us back variable of int type, while v.getEngine() gives us double.
  7. Insert the following to handle depreciation of value and engine capability.

    private int depreciatePrice(int price) {
        return (int) (price * depreciation);
    }
    
    private double depreciateEngine(double engine) {
        return (double) Math.round(engine * depreciation * 100) / 100 ;
    }

    What happens in the code above is that given the depreciation rate and original price, the app calculates the vehicle's current value. Ideally, you should use more sophisticated algorithms in your app. But in this simple example, it uses a constant rate of 80%. Note for values of double type, you have to round it to a desired decimal place otherwise you'll be given a (very) long decimal number.

    There's a good story for this called Pentium FDIV bug. Also, check this out What Every Computer Scientist Should Know About Floating-Point Arithmetic.

    Now you can run the app to test. Fill ALL text fields and press 'plus' sign in the app bar, what do you see?

    mini_bmw

    emptylist

However, the app you just created contains several bugs that need to be fixed:

  • You probably have noticed that as you move to text boxes towards the bottom of the screen, they appear behind the keyboard. How do we resolve these using techniques you have just learned?

  • The reason that you need to fill all text fields is that if you don't the app will crash. What can we do to get around this?

  • If you continuously press 'add' button you'll see in the output window the list gets longer, but the serial number remains the same. Why?

Lab 2 Gradle

In this lab, you will continue exploring Android resources and see how these resources can be built into apk files using Gradle.

Styles, theme, densities, and screen sizes

Duplicate the project folder you created for the first lab, rename it 'MyXml2'. In Android Studio navigate to where 'MyXml2' is and open the project. (Or you can continue working on the previous 'MyXml' project.)

We first look at styles.xml. Insert the following into the file in between resources tags

<style name="CodeFont" parent="@android:style/TextAppearance.Small">
    <item name="android:layout_width">fill_parent</item>
    <item name="android:layout_height">wrap_content</item>
    <item name="android:textColor">#00FF00</item>
    <item name="android:typeface">monospace</item>
</style>

In the code above:

  • We define a new style called 'CodeFont', that inherits everything from Android system style TextAppearance.Small. This inheritance is the same as in CSS.

    Unfortunately Google didn't document Android styles very well. This has now become a trial-and-error miracle. Check out the style source code to find more.

  • System style TextAppearance.Small also inherits from another system style called TextAppearance. The full stop '.' indicates this inheritance. But you cannot inherit system style this way. You can only inherit your own styles using this notation, as you'll see later.

  • 'item' tags define the attributes that you want to override from the parent style. In this case, you override textColor and typeface etc.

  • Android Studio has a built-in theme editor (Tools ==> Android ==> Theme Editor),where you can use palettes in a way similar to Photoshop.

    palette

Open activity_main.xml, insert style="@style/CodeFont" into the last TextView so it becomes

<TextView
    android:id="@+id/textBlock"
    style="@style/CodeFont"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_marginBottom="10dp"
    android:layout_marginTop="@dimen/margin_top"
    android:layout_weight="1"
    android:background="?android:attr/colorActivatedHighlight"
    android:scrollbars="vertical"/>

Note normally there is an android namespace for attributes, but for styles, you don't need to do that. (Don't ask me why).

In the preview tool window, in my case, it's currently Nexus 4 being used.

n4

Click on the littler black triangle next to 'Nexus 4' and select 'Nexus 7'. If you look closely you'll see that the 'output' area is a bit too big on Nexus 7. (OK, this is very subjective!).

n7

When you clicked on the littler black triangle you probably noticed different screen resolutions for different phones. Also when you extracted 'plus' and 'minus' icons in the previous lab you probably noticed different versions of the same icon file. So what do those mean?

In Android, there're many ways to customize system resources for your users according to their languages, screen sizes, screen density, screen orientation etc. This is very important for user experience. Here dpi refers to screen density, mdpi is around ~160dpi. There are also a set of definitions for screen sizes such as normal, large etc.

These additional resource files are called alternative resources. They are named using the convention <resources_name>-<config_qualifier>. For example, for the options menu icons, we have drawable-hdpi, drawable-mdpi etc. As mentioned previously that folder name matters, this is why.

In the project tool window, right-click on styles.xml folder icon, select New ==> Values resource file. In the window that comes up, select Smallest Screen Width as the qualifier, click on the icon '>>' to add it to the selection window to the right, and then give it a value of 600. Name the file styles.xml and click OK. Now you just created an alternative style file for devices that has a width greater than 600dp e.g. Nexus 7.

new_resources

600dp

Copy the following lines from 'styles.xml' and paste into 'styles.xml (sw600dp)' resources tags.

<style name="CodeFont" parent="@android:style/TextAppearance.Small">
    <item name="android:layout_width">fill_parent</item>
    <item name="android:layout_height">wrap_content</item>
    <item name="android:textColor">#00FF00</item>
    <item name="android:typeface">monospace</item>
</style>

Change TextAppearance.Small in the above to TextAppearance.Large. Now if you run the app on Nexus 7, in the output area the font size should be larger than on Nexus 4.

Next, create a new dimens.xml for sw600dp devices. Insert the following values into the file:

<dimen name="margin_left">25dp</dimen>
<dimen name="margin_top">20dp</dimen>
<dimen name="margin_right">80dp</dimen>

Now if you look at the preview window, for Nexus 7 it looks much better. But there're still quite a bit of space at the bottom of the screen that hasn't been used. Let's do something for that!

n7_2

In the project tool window, right-click on the layout folder icon, select New ==> Layout resource file. Set it to be sw600dp, and give it a name 'activity_main.xml', click OK.

Copy contents in 'activity_main.xml' and paste into 'activity_main.xml (sw600dp)' to replace what's in there already. Delete the last TextView tag and all attributes within it. Insert the following instead

<ScrollView
    android:id="@+id/scrollView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center_horizontal"
    android:layout_marginBottom="20dp"
    android:layout_marginTop="15dp"
    android:background="@color/colorPrimary"
    android:fillViewport="true">
    
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        
        <TextView
            android:id="@+id/textBlock"
            style="@style/CodeFont"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:scrollbars="vertical" />
        
        <Button
            android:id="@+id/buttonCearAll"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_alignParentRight="true"
            android:layout_marginBottom="10dp"
            android:layout_marginRight="10dp"
            android:background="@color/colorPrimaryDark"
            android:onClick="onClearClick"
            android:text="@string/menu_clear" />
    </RelativeLayout>
    
</ScrollView>

Note in the code above:

  • You added a ScrollView to make the large area scrollable. In the previous lab, we used a scrollable TextView, but that's not ideal as it's not a container layout. Here you use ScrollView so that you can have a button at the bottom. This also answers one of the questions at the end of the previous lab.
  • ScrollView can only contain one child element.
  • fillViewport="true" makes the scroll view’s child expand to the height of the ScrollView. If this is set to 'false' the child element will stay at the very top of the layout.

Now the layout looks better on Nexus 7.

n7_3

HashMap and Generic

Now turn to MainActivity.java file. To add a listener to the 'Clear All' button above, you need to insert the following into MainActivity.java

public void onClearClick(View v){
        clearVehicleList();
    }

In addition, it'll be good to show some information about car manufacturers in our app. You'll need to build a 'dictionary' for it.

Insert the following into strings.xml file

<string-array name="manufacturer_array">
    <item>Volvo</item>
    <item>Mini</item>
    <item>volkswagen</item>
</string-array>
<string-array name="description_array">
    <item>Volvo Car Corporation is a Swedish premium automobile manufacturer, headquartered in the VAK building in Gothenburg.</item>
    <item>The Mini is a small economy car made by the British Motor Corporation (BMC) and its successors from 1959 until 2000. The original is considered a British icon of the 1960s.</item>
    <item>Volkswagen is a German car manufacturer headquartered in Wolfsburg, Lower Saxony, Germany.</item>
</string-array>

In MainActivity.java declare a variable to hold car manufacturers information. This declaration goes together with the declaration of UI elements i.e. outside of any methods.

private Map<String, String> mapCarMaker = new HashMap<>();

Similar to Python dictionary, the Java way of doing a dictionary is to use the HashMap class. HashMap allows key-value pairs to be stored and looked up quickly. In fact, both List and Map are interfaces. The declaration Map<String, String> but not HashMap<String, String> is an example of programming to an interface

Insert the following lines into the onCreate method.

    String[] manufacturers = getResources().getStringArray(R.array.manufacturer_array);
    String[] descriptions = getResources().getStringArray(R.array.description_array);
    for (int i = 0; i < manufacturers.length; i++ ){
        mapCarMaker.put(manufacturers[i], descriptions[i]);
        }

What the code above does is to read car manufacturer names and their info. These are then put into a dictionary for later use. Also note:

  • The use of String array instead of ArrayList as the size of the array is fixed.
  • For an array, it has a field (i.e. a member variable) called length for its size. But for ArrayList, to get its size we need to call the size() method.

Locate the resetOutputs() method, insert/rearrange the for loop so that it looks like below. What this code does is that it checks if a description is available for the manufacture. If yes, append the info to the output.

for (Vehicle v : vehicleList) {
    String vehicleDescription = mapCarMaker.get(v.getMake());
    if (vehicleDescription == null){
        vehicleDescription = "No description available.";
    }
    outputs.append("This is vehicle No. " + (vehicleList.indexOf(v) + 1) + System.getProperty("line.separator"));
    outputs.append("Manufacturer: " + v.getMake());
    outputs.append("; Current value: " + depreciatePrice(v.getPrice()));
    outputs.append("; Effective engine size: " + depreciateEngine(v.getEngine()));
    outputs.append("; Desciption: " + vehicleDescription);
    outputs.append(System.getProperty("line.separator"));
    outputs.append(System.getProperty("line.separator"));
}

If you look at the MainActivity.java class, at the moment it's a bit redundant as it has two methods doing a similar job i.e. depreciatePrice and depreciateEngine. As Java is a type-safe language, it's necessary to have different methods for different different input types. But a more convenient way to do it is to use Java Generics.

Comment out the two methods mentioned above by selecting the two methods and then click cmd + /. Now insert the following line of codes instead

private <T extends Number> Double depreciateAnything(T originalValue) {
    Double result;
    if (originalValue instanceof Double) {
        result = Math.round(originalValue.doubleValue() * 0.8 * 100) / 100.00;
    } else {
        result = originalValue.intValue() * 0.8;
    }
    return result;
}

The code above is called a generic method. What it does is that we declare a type parameter 'T' to represent input type. But this type must be subclasses of Number e.g. Integer or Double. It then checks the type of input variable to see the actual type and perform calculations accordingly.

  • T extends Number is called bounded type parameter. In this case, it requires that the variable type passed into our method must be a subclass of Number.
  • Type declaration goes before the return type i.e. Double
  • instanceof is an operator in Java. This is equivalent to type() in Python.

Replace depreciatePrice and depreciateEngine with this new generic method.

outputs.append("; Current value: " + depreciateAnything(v.getPrice()));
outputs.append("; Effective engine size: " + depreciateAnything(v.getEngine()));

If you run the app on Nexus 7, either a real device or AVD, you should see the following. Fill out the form, click 'create diesel' and then the '+' icon. You'll see the message pops up in the output area. The description for the car maker is also available.

volvo

App manifest

Every application must have an AndroidManifest.xml file (with precisely that name) in its root directory. Open the AndroidManifest.xml file in your editor. There're several things to note in this file:

  1. The file defines 'metadata' of the app. There's one only one pair of manifest and application tag allowed.
  2. The application tag has several attributes i.e. icon, label, theme. Here, the label is your application's name, and shouldn't be changed. The label attribute here is for the texts in the top left corner of the screen.
  3. There can be more than one activity tags in your app.
  4. Activity name can be shortened. For example, you can use ".MainActivity" instead of "com.example.jianhuayang.myxml.MainActivity". The leading dot '.' in the former denotes the package attribute in 'manifest' tag.
  5. There can only be one launcher activity i.e. with tags "android.intent.category.LAUNCHER". This denotes the entry point of the app.

As you go along, you'll know more about app manifest.

Gradle build system

The ultimate targets for Android application development are apk files. You need to provide resources in XML format and Java source code to make it happen. But the problem is that, as you have seen already, the system is very complicated with such a lot of different packages/resources - it needS a build tool that is powerful enough to handle all these, not just a javac or make. Fortunately, in Android Studio the build process is (almost!) automated. We don't normally interfere with it. But it's important you understand how the build system works. In particular, there're three things that you need to be aware of:

  1. Files generated by Android Studio for build purposes. Among those files, the most important one is the R.java.
  2. The build.gradle syntax.
  3. Project dependencies

Explanation of different files/tools in the image above

IntelliJ build system

When you first create your project, Android Studio generates a set of files/folders for you. All these are grouped under 'Gradle Scripts' in the project tool window.

gradle_files

This is a bit confusing when you first look at them as there're so many different files. But fortunately, there are short descriptions next to file names i.e. those in brackets. For example, local.properties file contain configurations for SDK location.

To make things worse, there're two files with the same name - build.gradle - one for 'Project' and one for 'Module':

  1. build.gradle (Project: MyXml2) defines Gradle itself, for example, repositories (jcenter) and dependencies.
  2. build.gradle (Module: app) defines various aspects of module build settings. More on this later.

You have used many times R.id.XXXX in your Java source code, now it's the time to have a closer look at it. On your hard drive, navigate to app/build/generated/source/r/debug/com/example/[your name]/myxml file, open R.java file in a text editor. Try to locate 'id' class. Now you'll see different IDs available to you, either generated by the system or by yourself. Note that id itself is a class, and different IDs in it are actually integers.

id

build.gradle

The most important file is build.gradle (Module: app). The basic structure of Gradle build scripts comprises configuration and task blocks. Task blocks are rare in our case apply plugin: 'com.android.application', but ther're lots of configuration blocks in the form of

label {
//Configuration code...
}

Most of the configurations e.g. applicationId and targetSdkVersion have been explained already. There're two new configurations: buildTypes refers to whether it's 'release' or 'debug'. If it's release it'll apply a tool called ProGuard to it. What ProGuard does is 'shrinks, optimizes, and obfuscates your code by removing unused code and renaming classes, fields, and methods with semantically obscure names'. Click here for details. productFlavors refers to, for example, if it's a free (lite) version or a paid (full) version. You could set up two different flavors here and Gradle will build both for us. See an example app with build variants.

Project dependencies

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
}

In the current file we have two types of dependencies:

  • compile fileTree(include: ['*.jar'], dir: 'libs') is called 'local binary dependencies'. In case your Java needs some additional jar files, you'll have to create a folder called 'libs' under 'app' and put jar files there.
  • testCompile 'junit:junit:4.12' is called 'remote binary dependencies'. The resource you need is in the format of Maven coordinates i.e. group:name:version.

There's one type of dependencies missing here, which is module dependency. An example of module dependency would be compile project(":lib"), where 'lib' is the module name our current module depends on.

Finally, remember Gradle works on conventions, you'll have to change the configurations if you change default file names/locations etc.