Welcome, Guest User :: Click here to login

Logo 67443

Lab 1A: Stopwatch / TempConverter (Android)

Due Date: September 10

Objectives

  • Learn the basics of Kotlin Programing
  • learn to build basic Android apps from scratch
  • learn to work with dates and times in Kotlin

README.md

Lab 1: TempConverter and Stopwatch

  1. If you have not done so already, download Android Studio. After installing, open Android Studio and agree to license. This download/installation may take 20-30 minutes, so please be prepared.

  2. Create a virtual Android device to test your apps on quickly. Select the Android Virtual Device (AVD) Manager (from the Welcome Screen > More Actions (...) OR from the Tools menu). Click + Create Virtual Device, then under Category select Phone, and finally select Pixel 3a. If you’re using x86 (you probably are) select Nougat – API Level 25 – x86 – Android 7.1.1 (Google Play) then click Finish. If the Finish option is not available then you will need to download the AVD image first by clicking on “Download”. It will take up close to 1GB of space. You can start your new AVD to check it out.

    Android System Image

If you are running out of disk space, you can use the image that doesn't include the Google Play Services APIs (300MB, under x86 images). Those services are not used during most labs, but will be useful for building other projects.

Part 1: Temp Converter (Kotlin)

  1. Typically we will do the exercises for a new language second, but since it's the first week and we want to keep this short we'll get started by going back to an old favorite from 67-272: Temp Converter.

    We'll start by creating a new project that you will also use for Part 2. Start from a Phone Empty Activity, and name it TempConverterStopwatch. The rest of the defaults for you new project are alright, but set the Minimum SDK to API 25 so that your app will run on your AVD. Once Gradle (the build system used by Android Studio) finishes setting up your project, create a new Kotlin scratch worksheet (use File > New > Scratch File > Kotlin). To find your file, switch from the Android view in the Project sidebar to Scratches and Consoles, then name it Playground.

    New Android Project

  2. In this playground, create a class called TempConverter as follows:

import java.util.*

class TempConverter (temp: Int){
    private val temp: Int
    init{
        this.temp = temp
    }
}

Below the TempConverter class, create an instance of the TempConverter class with the line:

      val t = TempConverter(10)
  1. Now we want to add a convert method that will convert the temperature into Fahrenheit or Celsius, depending on what we specify. We can do this with a method similar to what we used in 67-272 inside of the TempConverter class:
fun convert(unit: String): Int{
      if (unit == "F")
          return 5 * (this.temp - 32) / 9
      else
          return (9 * this.temp) / 5 + 32
  }

Android Studio will suggest lifting the return out of the if statement, which is perfectly valid and a great way to make code readable in Kotlin, feel free try it out!

You've now defined a function to convert temperature that takes in a parameter unit of the type String and that it will return an Int by typing : Int where the function is declared.

To test this in our playground, right below where we create t, add the following:

    println(t.convert("F"))

Note: You may have to click green play button on the top left to execute the worksheet.

  1. This is nice, but we'd like the method to default to "F" if no argument was passed. To make this change, simply specify the default value in method definition convert(unit: String = "F"). Now remove any arguments from the print statement and see that it works as expected for Fahrenheit. Note to use Celsius we need to explicitly set the unit to "C".

  2. Now, make the convert method into a static method. To change this around comment out the convert method (// or Cmd+/) and create a new one inside of the companion object. This is a Singleton that allows anything inside of it to be called as if TempConverter was an object and not a class.

  companion object {
  fun convert(temp: Int, unit: String = "F"): Int {
      if (unit == "F")
          return 5 * (temp - 32) / 9
      else
          return (9 * temp) / 5 + 32
  }
}

We can display the outputs through adding some print statements:

    println(TempConverter.convert(50))
    println(TempConverter.convert(10, "C"))
    println(TempConverter.convert(-460, "F"))

(No) Oops! In Swift, parameters require labels (e.g. temp: 10, unit: "C") for identification. Note that we do not use labels in Kotlin, as parameters are filled by order.

  1. The last case reminds us that we have to account for absolute zero like we did in 67-272. To make things easy for now, let's just say the temp of anything in the absolute zero range is -1000. So in the convert function add in the following code right at the top of the function:
  if (tempBelowAbsoluteZero(temp, unit))
  return -1000

As you’ve probably noticed this conditional will be calling a helper function. Add in the following code to the companion object to make this line work.

    fun tempBelowAbsoluteZero(temp: Int, unit: String): Boolean {
  return (temp < -454 && unit == "F") || (temp < -270 && unit == "C")
}

We now have a decent temp converter class and gotten our feet wet with Kotlin. We'll come back to this next week to do a little more, but for now let's move on to building our first Android app.

Part 2: Stopwatch (App)

For this part of the lab, we will now focus on creating our first actual Android app: a stopwatch. At the end of this portion of the lab, you will have a working Android stopwatch app that you can deploy onto your own Android device (more on deployment later).

The final app should look as follows:

New Android Project

Now to actually construct it.

  1. If you haven't already, create your project. Go to File > New > New Project, select Empty Activity, then click next and give a name of Stopwatch. Ensure Kotlin is selected as the language. Select Minimum SDK: API 25 and then click Finish.

  2. To run the device emulator click on the play button in the AVD Manager (Tools > AVD Manager) or that in the top right, which will also build & run your app. If you click on the files on the left, they will open in the middle pane of the UI. Check out the contents of some of the files, including MainActivity.kt and activity_main.xml, just skimming the files to get a sense of what they are about.

  3. Now, we will take a deeper look at the activity_main.xml file, which is used to, in short, graphically manage elements on the Android device's screen. It is an XML file that you can render and edit in the Design view or simply edit in the Code view, or both in Split with the options in the top right.

    Click the main canvas rectangle. Notice how there are now options on the right side (also known as the Attributes Pane) for you to investigate.

  4. With that done, now note that the palette pane contains a list of widget elements for you to use in your application. Here find the TextView object and drag it onto the interface. Now, go back to the attribute pane and change the id to clockTextView (accept the refactor), and the text to @string/reset. Then open res/values/strings.xml and add a tag that looks like <string name="reset">0:00.000</string>. It is common practice not to hardcode elements but save references to them in resource files like this to make it easier to swap things like languages.

  5. Now add two Button objects to the interface in the same manner you did with the TextView in step 4. Set one to have a green backgroundTint and have the label 'Start', and the other one to have a red backgroundTint and have the label 'Stop'.

  6. Now, this might not be 272, but that doesn't mean we can forget about source control! Put this project into a Git repository from the command line and commit our progress so far. You can use the VCS > Enable Version Control Integration or your favorite interface for Git.

  7. Android Studio features handy Android simulators which we can use to test our app without deployment. In the upper-right UI is a Play button (>) that will build the project and run it on your AVD simulator. Run the build and see it works on the simulator (aside from the buttons working as expected).

    One thing you will notice is that all your UI elements have collapsed and stacked onto one another. In mobile applications, we usually use layout elements to help arrange and space our UI according to changing screen sizes. For now we will just set constraints: have the clockTextView be 200 units from the top, and 0 from the left and right to be centered. Your buttons can be 75 units from the relevant left or right and 300 from the bottom.

    TextView Constraints
    TextView Constraints

  8. Now we are going to have to create functions which will be called upon clicking the Start and Stop buttons. These actions are written in the MainActivity.kt file. Go there and add the following functions and resolve the import for the View class:

fun onClickStart(view: View) {

}

fun onClickStop(view: View) {

}
  1. Now in our layout file, it's time to "wire up" each button to its respective action. To do this, click on a button then edit the onClick attribute to be the respective function.

  2. We now need a variable for the time display. Add this line near the beginning of the MainActivity class.

private var startTime: Long = 0
  1. We can save and build the project right now, but unfortunately our actions are pretty much useless and serving as just a placeholder right now. To make them more useful, we need a model that will handle the logic of finding the current time, calculating the elapsed time and converting that time into a string can be displayed through our outlet onto the time display label.

  2. Before we write the code for this model, let's experiment with dates and times in Kotlin using a scratch worksheet. Open a new scratch Kotlin file and call it something like 'DatePlayground' (what you actually call it is not important). Once it is open, add the following:

import java.util.*

val calendar = Calendar.getInstance()
println(calendar.toString())

We see that when we create a new instance of Calendar, it defaults to the current datetime. To create an older date is a little more effort, unfortunately, but it will give us a chance to play with GregorianCalendar. Let's create two more dates in our playground with the following code:

val newYearsDay = GregorianCalendar(2015,Calendar.JANUARY,1)
val valentinesDay = GregorianCalendar(2015,Calendar.FEBRUARY,14,9,0,0)

Note: if you were to inspect Calendar.JANUARY, you would find that months are internally zero-indexed. Beware!

We can find the difference between these dates by subtracting millisecond versions of those dates:

val newTime = newYearsDay.timeInMillis
val valTime = valentinesDay.timeInMillis

val diffVD2NYD = valTime - newTime
val diffNYD2VD = newTime - valTime

println(diffVD2NYD)
println(diffNYD2VD)

What this is giving us is the time between these as milliseconds. In the case of diffNYD2VD the value is negative because when I subtract a larger number (Valentines Day) from a smaller number (New Years Day) it is negative, but that is easy to correct by multiplying by -1 if needed. What if I wanted the number of days between these dates? And what if after I wanted the number of hours that remained between them (since one starts at midnight and the other at 9am)? We could do the following:

val diffDays = diffVD2NYD / 86400000
val diffHours = (diffVD2NYD - diffDays * 86400000) / 3600000
println(diffDays)
println(diffHours)

To combine these into a string (that I might send to update a view, for example) all we need to do is the following:

val diffVD2NYDAsString: String = String.format("%02d days, ",diffDays).plus(String.format("%02d hours",diffHours))
println(diffVD2NYDAsString)  

Likewise, we can easily find the time that has elapsed from startTime and now with the following:

var elapsedTime = Calendar.getInstance().timeInMillis - calendar.timeInMillis
println(elapsedTime)
  1. Having played with dates and times in the playground, let's return to the model and start filling it out. Replace the start and stop functions with the following:
fun onClickStart(view: View) {
  startTime = System.currentTimeMillis() //avoids creating a Calendar
}

fun onClickStop(view: View) {
  startTime = null
}

Like in Swift, datatypes are non-nullable by default, so we have no problem with start, but as soon as I set startTime to null in onClickStop() we would get an error. What do I have to do to correct that error? (Hint: in class on Thursday we discussed Optionals; that could be useful here. The shortform syntax for Swift is similar to Kotlin.)

  1. One thing I'd expect my model to do is calculate the elapsed time. I can do this by adding the following method to my model:
fun elapsedTime(): Long {
  val t : Long = System.currentTimeMillis()
      val clock = t - (startTime?: t)
  return clock //will be zero if startTime was null
}

Remember that since we just made startTime an optional in the previous step, I need to unpack that optional safely here with the Elvis operator approach discussed in class.

While this is nice, what we really need is a function that will return a string of format 00:00.0 (minutes,seconds,fractions of a second) that can be used by the TextView . Write that function using the following starter code and drawing on lessons from our playground (you can test your ideas in the playground as well before writing the model method):

fun elapsedTimeAsString(): String {
  // return the formatted string...
}
  1. One last thing we need in our model: a variable called isRunning which has two states: true if the stopwatch is running and false otherwise. The existence of what variable indicates to us that the stopwatch is running? Use that information to create this simple variable now and update it in your click handers.

  2. Build and run the project. Problem is that still nothing is happening when I press start. Why? Well, I haven't updated the clockTextView so there is no way to see what (if anything) I've done.

  3. To fix this, create a method called updateClock. This will be a little tricky because we need this label to continually update until we press stop. To do this creat a timer that calls updateClock at the end of onCreate

val mainHandler = Handler(Looper.getMainLooper())
mainHandler.post(object : Runnable {
  override fun run() {
      if(isRunning)
          updateClock()
      mainHandler.postDelayed(this, 20)
  }
})

Inside of your updateClockUI function add the following to update your TextView.

clockTextView?.text = elapsedTimeAsString()

However, we have not established the reference clockTextView to the TextView in the layout yet. To do this, we will first add a private TextView variable in your class.

private var clockTextView: TextView? = null

Then, in onCreate (before the handler initilization) we will use the findViewById function to attach a reference to the view. This is an essential function that you will use to locate Views, Layouts and other widgets. Notice that we can find the IDs we definte autopopulated in the (R)esources class, and therefore it is important to stick to a naming convention.

clockTextView = findViewById(R.id.clockTextView)

Styling Your App

Now that your app is working, it's time to add a little style to it.

  1. Let's start by adding a background image. You can grab any image you would like. I personally like unsplash.com. All of the images here are licensed under Creative Commons Zero which means that you are free to use them however you would like. Find an image and save it to your app>src>main>res>drawable folder. You can drop it in via Android Studio or File Explorer/Finder.

In order to handle different sized screens and layouts, Android allows you to upload multiple versions. For this assignment we will keep it simple with just one size. Go into the activity_main.xml and in ConstratinLayout add android:background="@drawable/[your_file_name_without_ext]" this is a good time to tell you that this image should not have uppercase or special characters in the name. Recommended filesize around 1280x1980

If this causes your app to run slowly you can add an image in the same way you added a button using the Design tool.

Change your clockTextView to include android:textAppearance="@style/TextAppearance.AppCompat.Display3". You can also set the textColor to white if your image is dark.

  1. Some experts say that rectangles with rounded corners are easier on the eyes than a rectangle with sharp edges. Create a new drawable file called button_green.xml that contains the following content:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#4CAF50" />
    <corners android:radius="15dp" />
</shape>

Then edit the background attribute of your start button to have a background of @drawable/button_green.xml. You may need to set app:backgroundTint="@null" to prevent the Material theme from overriding your colors. Do the same process for the red button.

  1. Finally, as a minor tweak, download the AppIcon for Stopwatch. Right-click on app in the project view, select New, then Image Asset. Under Source Asset, select Image, then edit the path to point to the large version of the icon image. Once that is selected you will see multiple previews. Select Next and then Finish.

Feel free to change any other fonts and colors to make your app unique!

  1. Optional: This lab has not been guided with best practices in mind. It has activity_main.xml as the View and MainActivity.kt as both the Model and Control. Split out the stopwatch Model into its own class called Stopwatch.kt that encapsulates all of the Model code and dedicate MainActivity.kt to be the Control.

Stopwatch (ChatGPT) -- Optional

In this first lab, we walked you through the details of building this first app. Since this is such a basic app, we can use ChatGPT to give us all the instructions we need. We will begin by opening ChatGPT and use the prompt: "How can I create a stopwatch app using Android Studio?" Follow the instructions given, being sure in this case that when you create the new app you call it StopwatchAI to differentiate. The directions aren't complete (at least when I tried this) but they given you a decent place to start. As we move through the course, you will find that generative AI tools can help and accelerate your pace, but can't (yet) really replace having fundamental knowledge of software development.


Stop

Be sure that the lab is checked-off by the TAs when complete. If for some reason you don't finish during this session, you may complete it on your own and show the TAs prior to the beginning of the next lab.