Lab 1A: Stopwatch / TempConverter (Android)
Due Date: September 08
Objectives
- Learn the basics of Kotlin Programing
- learn to build basic Android apps from scratch
- learn to work with dates and times in Kotlin
Due Date: September 08
Objectives
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.
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.
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.
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.
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)
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.
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"
.
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.
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.
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:
Now to actually construct it.
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.
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.
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.
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.
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'.
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.
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.
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) {
}
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.
We now need a variable for the time display. Add this line near the beginning of the MainActivity
class.
private var startTime: Long = 0
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.
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)
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.)
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...
}
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.
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.
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)
Now that your app is working, it's time to add a little style to it.
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.
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.
Feel free to change any other fonts and colors to make your app unique!
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.
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.