• Labs icon Lab
  • Core Tech
Labs

Guided: Build a Trip Tracking Journal in Kotlin

Embark on a coding adventure as you dive into crafting a Trip Tracking Journal application in Kotlin. This guided Code Lab will serve as your compass, navigating you through essential Kotlin concepts while focusing on travel-oriented functionalities. You'll discover how to model trip data efficiently using Kotlin's data classes, handle user interactions seamlessly within a command-line interface, and implement robust data persistence mechanisms for storing and retrieving travel information. Throughout the journey, you'll gain invaluable insights into Kotlin's features, including file I/O operations, collections management, and error handling. By the end of the lab, you'll have a tangible understanding of Kotlin programming, enabling you to build a comprehensive Trip Tracking Journal.

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 44m
Published
Clock icon Apr 22, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Scenario:

    Imagine you are a part of a startup dedicated to enhancing the travel experience for adventurers around the world. Your team has identified a gap in the market for a trip tracking journal that allows users to document their journeys through a command-line interface (CLI). The Trip Tracker application will enable users to record details of their trips, such as destinations, dates, experiences, and expenses. The application aims to be a personal travel diary that is quick to access and easy to use, even when internet access is unreliable.


    Code Lab

    In this Guided Code lab, you will embark on the journey of building a Trip Tracking Journal using Kotlin programming. Starting with the basics of Kotlin syntax, you will gradually progress to implement advanced features, resulting in a fully functional Trip Tracking Journal.

    Completing this Kotlin Trip Tracking Journal Lab will involve mastering the following key concepts:

    Kotlin Basics:
    Gain proficiency in fundamental Kotlin syntax and language features.

    Data Handling:
    Learn to model structured data using Kotlin data classes.

    Utilizing Collections for Data Management:
    Employ Kotlin's collections, such as lists and maps, to organize and manipulate trip data.

    User Interaction:
    Develop a console-based user interface for trip tracking. Manage user input and execute corresponding actions.

    Object-Oriented Programming (OOP):
    Apply OOP principles through the creation and organization of classes and methods. Understand the importance of data encapsulation in code organization.

    File Handling:
    Implement functionality to persist trip data to a file.

    By the end of this Lab, you will have a comprehensive Trip Tracking Journal application, showcasing your mastery of Kotlin programming concepts and application development. Before you begin, here are some key points:

    1. Your task involves implementing code in the Trip.kt and TripTracker.kt files.
    2. If you encounter any challenges along the way, feel free to consult the solution directory.
    3. To simplify the process, comments are included to help you find the necessary changes for each task according to the step you're working on. For instance, if you're currently on Step 3, you can locate the relevant changes by finding // T0-DO Step 3 in the file.
  2. Challenge

    Define the `Trip` Data Class

    To build your Trip Tracker application, you will need to understand data classes and learn how to leverage them to efficiently store and manage data. Your goal is to create a data class to represent Trip data.

    Get started by understanding what a data class is and how it can simplify the process of storing your trip-related information.


    Data Classes

    In Kotlin, a data class is a special class designed to hold data in a concise and effective way. It comes with built-in functionality, such as automatic generation of common methods like toString(), equals(), and hashCode(). This makes it an ideal choice for modeling simple data structures where the focus is on the data itself.

    Consider an example where you have a Meeting data class with the following:

    • String property to represent the meeting title
    • LocalDate property to represent the Date
    • LocalTime property to represent the Time
    • List<String> property to represent the List of agenda items
    • Map<String,Double> property to represent the participants and their respective score in the meeting topic
    data class Meeting(
        val title: String,
        var date: LocalDate,
        var time: LocalTime,
        val agendaItems: List<String>,
        val participantsScores: Map<String, Double>
    )
    

    Notice that title is prefixed with val and date with var.

    • val is used to declare immutable variables, ensuring that their values cannot be altered once initialized.
    • var is used to declare mutable variables, permitting changes to their values after initialization.

    Similar as the above example, you will utilize the LocalDate instance to retain the trip date, signifying the year, month, and day of the trip.

    The date string can be converted to LocalDate as shown below:

    // Parse a string representing the date 2024-03-20
    val parsedDate = LocalDate.parse("2024-03-20")
    

    You learned about data classes and the difference between var and val keyword.

    Next, you'll create a data class named Trip to encapsulate information about a Trip. A Trip data class should consists of the following:

    | Property | Type | | -------- | -------- | | destination | String | | date | LocalDate | | experiences | List<String> | | expenses | Map<String, Double> | In this step, you learned about data classes, the difference between var and val keywords, and how to define List and Map variables in a data class.

    In the next step, you will look at the TripTracker class and build a CLI menu.

  3. Challenge

    Explore `TripTracker` & Construct CLI Menu

    Now, you will review the current setup and the files which you will be working with.

    The Main.kt File

    This file acts as an entry point, instantiating the TripTracker class and calling the start() function of the TripTracker class.

    fun main() {
        val tripTracker = TripTracker()
        tripTracker.start()
    }
    

    The TripTracker.kt File

    This file contains the code for user interactions and trip management functionality.

    #####Private Variables

    Within the TripTracker class, you have two private properties:

    • trips: This is a mutable list which will be used to store trips.

    • reader: This is an instance of the BufferedReader class for handling user input in your program.

    class TripTracker {
      private val trips = mutableListOf<Trip>()
      private val reader = BufferedReader(InputStreamReader(System.`in`))
    

    The start Function

    This function provides users with a Menu Interface and captures the user inputs. Inside the while loop, the application continuously displays a menu to the user. You will use the when construct to create a switch-case statement.

    The when Expression

    In Kotlin, the when expression serves as a more powerful and flexible alternative to traditional switch-case statements found in other programming languages. It allows you to match a value against multiple cases and execute different code blocks based on the matching case.

    Here is an example:

    val reader = BufferedReader(InputStreamReader(System.`in`))
    
    println("Please enter a letter (A-C): ")
    when (reader.readLine()?.toUpperCase()) {
        "A" -> println("You entered A")
        "B" -> println("You entered B")
        "C" -> println("You entered C")
        else -> println("Invalid Input")
    }
    

    In the above example, the else option in a when expression acts as the default case, executing when none of the other option match. This helps handle unexpected inputs or conditions gracefully.


    Now, you will learn about the available options and their respective methods for the Trip Tracker application:

    1. Add Trip

    addTrip() : This function prompts the user to input details about a new trip they wish to add. It captures the trip's destination, date of travel, experiences, and expenses. Then it adds the trip to the list of trips stored within the TripTracker instance.

    2. View Trips

    viewTrips() : Displays a summary of all trips currently stored in the Trip Tracker. Each trip's destination, date, experiences, and expenses are presented.

    3. Search Trip

    searchTrip() : Enables users to search for trips based on a keyword. It prompts the user to enter a keyword, and then displays all trips whose destination or experiences contains the provided keyword.

    4. Update Trip

    updateTrip() : Users can update details of an existing trip. They are prompted to input the destination of the trip they wish to update, and then provide new details such as date, experiences, and expenses. Updating the trip replaces the previous values with the new ones.

    5. Add Experiences

    addExperiencesToTrip() : Users can select an existing trip to which they want to append experiences, and subsequently, they can add those experiences to the trip.

    6. Add Expenses

    addExpensesToTrip() : Users can choose an existing trip that they wish to add expenses to, and then they can input the incurred expenses for that trip.

    7. Delete Trip

    deleteTrip() : Allows users to remove a trip from the list. Users need to specify the destination of the trip they want to delete.

    6. Exit

    Option to exit the Trip Tracker.

    7. Save & Exit

    Gracefully exits the Trip Tracker while saving the current trip data to a file named trips.ser.


    Next, you will design the Menu for the Trip Tracker. Great! You've successfully created a CLI-Based menu.

    In the next step, you'll be implementing the addTrip function to facilitate the addition of trips to your Trip Tracker.

  4. Challenge

    Implement Add Trip

    In this step, you will implement the addTrip method within the TripTracker class. This method prompts you to enter the destination of the trip, trip date, experiences, and expenses. A trip instance should be created with these details and added to the trip in the list.

    You are going to use BufferedReader in order to get the user input. Next, you will learn about BufferredReader.

    The BufferedReader Class

    BufferedReader in Kotlin facilitates reading input from the standard input stream (System.in), offering enhanced flexibility and efficiency over Scanner, particularly when handling substantial input data volumes.

    You can create a BufferedReader object by wrapping an InputStreamReader around System.in, like this:

    val reader = BufferedReader(InputStreamReader(System.`in`))
    

    Once you have a BufferedReader instance, you can use its methods to read different types of input. Some commonly used methods include:

    • readLine() : Reads the next line of input as a string.
    • read(): Reads a single character from the input stream and returns its Unicode value as an Int.
    • readText(): Reads the entire input stream as a string.
    • readLineOrNull(): Reads the next line of input as a string, returning null if the end of the stream is reached.

    Here's an example of using BufferedReader to read input:

    val userInput = reader.readLine()
    

    In the above example, the user input from the console will be assigned to the variable userInput.

    --- The addTrip() function is responsible for allowing users to add a new trip to the Trip Tracker application.

    Since users will be typing the experiences delimited by commas, you need to know how to convert a String to a List<String>.

    Example:

    val str = "apple,banana,orange"
    val parts = str.split(",") // parts list containing 3 items
    
    println(parts)
    println(parts.joinToString())
    

    Output

    [apple, banana, orange]
    apple, banana, orange
    

    In this example, the split function takes a comma , as the delimiter, so it splits the string into three parts based on the comma.

    The joinToString() function concatenates all elements of the parts list into a single string, using a comma , as the default separator. So, "apple", "banana", and "orange" are joined together with commas in between each element.

    --- Similar to the above, you'll see how to parse and convert a String to a Map. This will be useful for creating the expenses Map.

    Example :

    // Example text containing key-value pairs separated by comma
    val text = "apple:2.5, banana:1.8, orange:3.2"
    
    // Splitting the text by comma and mapping it into a Map<String, Double>
    val fruitMap = text.split(",")
     .map { it.split(":") }
     .associate { it[0] to it[1].toDouble() }
    	
    // Printing the resulting map
    println(fruitMap)
    }
    

    Output :

    {apple=2.5, banana=1.8, orange=3.2}
    

    This code splits the text string into parts using , as the delimiter. Then, each part is further split using : as the delimiter. Finally, it creates a Map associating the first part (before :) with the second part (after :), converting the second part to a Double.


    Now, you will complete the implementation of addTrip() method. In this step, you learned about how to read data using a BufferedReader and how to parse text data into List and Map.

    In the next step, you'll implement the viewTrip method which will enable you to view the trips.

  5. Challenge

    Implement View Trips

    In this step, you are going to complete the viewTrips function within the TripTracker class. If there are trips added, this function prints the title of each trip along with description, trip date, and trip time.


    In order to correctly implement this method, you will learn how you iterate over a list using the forEach loop.

    The forEach Loop

    The forEach loop in Kotlin provides a convenient way to iterate over elements in a collection and perform specific actions for each element.

    Example:

    collection.forEach { element ->
      // Code to be executed for each element
      println("Element Value ${element}") 
    }
    

    For each element in the collection, the below lambda expression is executed:

    { element -> println("Element Value ${element}") } 
    

    The current element is represented by the parameter element within the lambda expression.

    Next, you will learn about a variation of forEach loop that is forEachIndexed.

    The forEachIndexed Loop

    The forEachIndexed loop in Kotlin is similar to the forEach loop, but provides additional functionality by allowing you to access both the index and the value of each element in the collection.

    collection.forEachIndexed { index, element ->
        // Code to be executed for each element with its index
        println("Element at index $index is $element")
    }
    

    In the forEachIndexed method, the lambda expression takes two parameters: index and element.

    The index parameter represents the index of the current element being iterated over. The element parameter represents the value of the current element.

    For each element in the collection, the lambda expression is executed, and you can access both the index and the value of the element within the loop body. This allows you to perform specific actions based on both the index and the value of each element in the collection.


    As there might be many trips, displaying the index alongside each trip details aids in clear identification and organization of the trip list.

    Now, you will use this understanding to complete the viewTrips method. In this step, you learned about forEach and forEachIndexed loops and now understand when to use them.

    In the next step, you are going to implement searchTrip function to quickly search for the trips that you are interested in.

  6. Challenge

    Implement Search Trips

    Given that the Trip Tracker may contain a significant number of trips over time, it's essential to allow users to search for trips they are interested by using criteria such as destination, date, or experiences. This approach ensures that the Trip Tracker enables users to easily discover trips of interest.


    In order to efficiently implement the searchTrip function, you will learn about how to filter a list.

    The filter Function

    In Kotlin, the filter function is a higher-order function available on collections. It allows you to create a new collection containing only the elements that satisfy a given predicate (a function that returns a Boolean value).

    Example :

    val numbers = listOf(1, 2, 3, 4, 5, 6)
    
    val evenNumbers = numbers.filter { it % 2 == 0 }
    println(evenNumbers) // Output: [2, 4, 6]
    

    In this example, filter is used to create a new list (evenNumbers) containing only the even numbers from the original list numbers.


    Now, you'll use this understanding to implement the searchTrip function.

    In this step, you learned how to filter a collection using the filter function.

    In the next step, you are going to update the trip.

  7. Challenge

    Implement Update Trip

    The Update Trip functionality within the Trip Tracker enables users to modify existing trip details, ensuring accurate and up-to-date information. With this feature, users can effortlessly adjust various aspects of their trip, including the destination, date, experiences, and expenses.

    In order to efficiently implement the updateTrip function, you will learn about range expressions in Kotlin.


    Range Expresssions

    In Kotlin, a range expression is used to represent a sequence of values within a specified range. It is created using the .. operator for inclusive ranges or the until keyword for exclusive ranges.

    For example:

    • 0..5 represents the range from 0 to 5, including both 0 and 5.
    • 0 until 5 represents the range from 0 to 4, including 0 but excluding 5.

    Range expressions can be used in various contexts, such as iterating over a range of values in loops, checking if a value falls within a certain range, or creating slices of collections.

    Inclusive Range Example :

    println("Inclusive range:")
     for (i in 1..5) {
         print("$i ")
     }
    // Prints 1 2 3 4 5 
    

    Exclusive Range Example :

    println("Inclusive range:")
    for (i in 1 until 5) {
         print("$i ")
     }
    // Prints 1 2 3 4
    

    Now, you'll use this understanding to complete the updateTrip function. In this step, you learned about the range expression in Kotlin.

    Next, you will implement the add experiences functionality in the Trip Tracker.

  8. Challenge

    Implement Add Experiences

    The addExperiencesToTrip function enables users to enhance their trip records by adding new experiences, facilitating the tracking of memorable moments during their travels.

    You will now understand about how to combine various collections in Kotlin.

    The plus Method in Collection

    In Kotlin, the plus method is a part of the Kotlin Standard Library and is available for various types of collections such as lists, sets, maps, and strings. It is used to create a new collection by combining the elements of two collections without modifying the original collections. Here's how it works for different types of collections:

    Lists:

    val list1 = listOf(1, 2, 3)
    val list2 = listOf(4, 5, 6)
    val combinedList = list1.plus(list2) // [1, 2, 3, 4, 5, 6]
    

    In this example, combinedList contains all elements of list1 followed by all elements of list2.

    Alternatively, if you want to append all items of list2 to list1 then you can write:

    val list1 = list1.plus(list2) // [1, 2, 3, 4, 5, 6]
    

    Sets:

    val set1 = setOf(1, 2, 3)
    val set2 = setOf(3, 4, 5)
    val combinedSet = set1.plus(set2) // [1, 2, 3, 4, 5]
    

    Here, combinedSet contains all unique elements from set1 and set2.

    Maps

    val map1 = mapOf("a" to 1, "b" to 2)
    val map2 = mapOf("b" to 3, "c" to 4)
    val combinedMap = map1.plus(map2) 
    // {a=1, b=3, c=4}
    

    In this case, combinedMap contains all entries from map1. For entries with the same keys, the values from map2 override those from map1. So in the above example the value of b has been overridden from 2 to 3.

    Strings

    The plus method also works for strings:

    val str1 = "Hello, "
    val str2 = "World!"
    val combinedStr = str1.plus(str2) // "Hello, World!"
    

    Here, combinedStr is the concatenation of str1 and str2.

    Now that you've learned how to combine collections, you will use this knowledge to complete the addExperiencesToTrip function. After successfully implementing the addition of collections using the plus method for a List, the next step involves using the same for a Map while implementing the addExpensesToTrip function.

  9. Challenge

    Implement Add Expenses

    The addExpensesToTrip function allows users to add expenses to an existing trip.

    Similar to the previous step where you added two lists using the plus method, you will add two maps in this function. Next, you will learn how to remove a previously added trip from the Trip Tracker.

  10. Challenge

    Implement Delete Trip

    Delete Trip functionality allows you to permanently remove the trips from your journal. This option aids in keeping your Trip Tracker neat and organized by eliminating unnecessary entries.

    In order to implement the delete event function, you need to understand how to remove items from a list.

    In Kotlin, the removeAt(index) function is used to remove an element from a mutable list at a specific index.

    When you call removeAt(index) on a mutable list, Kotlin removes the element at the specified index and shifts all subsequent elements to the left to fill the gap created by the removal. It also returns the removed item.

    Here is an example :

    val removedElement = xyzList.removeAt(2)
    // Deletes the 3rd item in the xyzList
    

    Now, you will finish the implementation of the deleteTrip function. In this step, you learned how to remove an item from a list using removeAt(index).

    In the next step, you're going to learn about saving the trip data in a file by implementing file handling.

  11. Challenge

    Save Trips to a File

    Saving trip data to a file ensures that the data persists even after the application is closed or the system is restarted. Without saving to a file, all trip data would be lost once the program terminates.


    File Management in Kotlin

    File management in Kotlin involves working with files and directories to perform tasks like reading from and writing to files, creating directories, deleting files, and more. Kotlin leverages Java's file I/O classes, which provide comprehensive functionality for file operations.

    The FileOutputStream Class

    FileOutputStream is a class in Java and Kotlin used to write data to a file as a stream of bytes. It's typically used to write raw binary data to files. You can create an instance of FileOutputStream by passing the filename as a parameter to the constructor.

    val outputStream = FileOutputStream("example.txt")
    

    The ObjectOutputStream Class

    ObjectOutputStream is a subclass of OutputStream in Java and Kotlin that provides the functionality to write objects to a file using serialization. Serialization is the process of converting objects into a byte stream that can be written to a file or transmitted over a network.

    The writeObject method is a function of ObjectOutputStream used to write objects to the output stream. It serializes the specified object and writes its contents to the underlying output stream.

    Here is an example :

    data class Person(
        val name: String,
        val age: Int
    ): Serializable
    
    fun main() {
        val personList = listOf(
            Person("Alice", 25),
            Person("Bob", 30),
            Person("Charlie", 35)
        )
    
        val file = "personList.ser"
       // Create FileOutputStream to write data to a file
       val fileOutputStream = FileOutputStream(file)
       // Create ObjectOutputStream to serialize objects and write them to the file
       val objectOutputStream = ObjectOutputStream(fileOutputStream)
       // Write the list of Person objects to the ObjectOutputStream
       objectOutputStream.writeObject(personList)
       // Close the ObjectOutputStream and FileOutputStream to release resources
       objectOutputStream.close()
       fileOutputStream.close()
    
        println("Person list saved to $file")
    }
    
    • Notice that the Person class implements the Serializable interface, indicating that instances of this class can be serialized.

    Serializable

    In Kotlin, the Serializable interface serves as a marker interface that indicates that instances of a class can be serialized. Serialization is the process of converting an object into a stream of bytes so that it can be stored in a file, sent over a network, or otherwise persisted in a byte-oriented format. Later, the serialized object can be deserialized back into its original form.


    Now, you will make the Trip data class serializable so that trips information can be saved in a file. In this step, the trip information is successfully stored in a file upon selecting the Save & Exit option.

    In the next step, you will explore how to reload the trips information when the Trip Tracker is restarted.

  12. Challenge

    Reload Trips from File

    In this step, you'll look at reloading the stored trips information as the application is restarted.

    First, you will learn about init block in Kotlin, which is useful to load the trips information as the application restarts.

    The init Block

    In Kotlin, the init block is a special type of initializer that is executed when an instance of a class is created. It's primarily used to initialize properties or perform additional setup logic that is necessary before the object is ready for use.

    init {
     // Load trips data from file on application startup
     loadTripsFromFile() 	
    }
    

    The init block is executed immediately after the primary constructor of the class is called. If there are multiple init blocks in a class, they are executed in the order they appear in the class body.


    Before loading the file, you need to check whether the file exists.

    The file.exists() Function

    The file.exists() function is used to check whether a file or directory exists at the specified path. It is a method of the File class, which represents a file system path.


    As in the previous step, you understood about FileOutputStream and ObjectOutputStream for saving files.

    In this step, you'll learn about the FileInputStream and ObjectInputStream classes for reading the files.

    The FileInputStream Class

    FileInputStream is a class in Java and Kotlin used for reading raw bytes from a file. It's particularly useful when you want to read binary data or text data in its raw byte format. While FileInputStream reads bytes from a file, it does not offer high-level methods for directly reading objects. However, you can use it in conjunction with ObjectInputStream to read serialized objects from a file.

    The ObjectInputStream Class

    ObjectInputStream is a higher-level class that allows objects to be read from an input stream. It's used for deserializing objects previously written using ObjectOutputStream.

    After initializing ObjectInputStream, you can use its readObject() method to read the next object from the input stream.

    Here is an example of reading Person objects from a file:

    al file = File("Persons.ser")
    val fileInputStream = FileInputStream(file)
    val objectInputStream = ObjectInputStream(fileInputStream)
    personList.addAll(objectInputStream.readObject() as MutableList<Person>)
    objectInputStream.close()
    fileInputStream.close()
    

    When deserializing objects using Kotlin's ObjectInputStream, the type you use to cast the deserialized object, whether MutableList or List, determines the mutability of the resulting collection.

    The choice between MutableList and List during deserialization depends on your requirements for mutability. If you need the flexibility to modify the collection, use MutableList. If you prefer immutability and want to ensure the collection remains unchanged, use List.

    After reloading the trips information, trips can be added or modified so MutableList is the correct choice in this case.


    Now, you will implement the loadTripsFromFile function. It's time to run your Trip Tracker application! In the bottom-right corner of the Terminal, click on the Run button.

    This will display the Trip Tracker menu and you can try the features of your application.

    Kudos!

  13. Challenge

    Conclusion and Next Steps

    Congratulations on creating a functional Trip Tracker application!

    Throughout this Code Lab, you've navigated through essential Kotlin programming concepts while crafting a robust tool for tracking your trips. You've addressed various tasks such as handling user input, managing trip data, collections management, implementing exception handling, and persisting data to files. These skills are fundamental in Kotlin development and have been applied effectively in building a functional Trip Tracker Journal.

    Next steps for enhancing your Trip Tracker include:

    • Trip End Date : Adding Trip End date provides users with a more comprehensive view of their trips and allows them to better track the duration of each trip.

    • Trip Statistics : Calculate and display statistics about the trips, such as the total number of trips taken, total expenses per trip, expense per day for the Trip. This can provide valuable insights to users.

    • Reminders for Trip Anniversary : Enhancing the Trip Tracker with a Trip Anniversary feature adds a touch of nostalgia and allows users to celebrate the memorable milestones of their past adventures.

    As you continue your Kotlin learning journey, consider exploring the Kotlin path on Pluralsight:

    Keep exploring, experimenting, and honing your Kotlin skills. With dedication and practice, you'll continue to grow as a proficient Kotlin developer. Best wishes on your coding endeavors!

Amar Sonwani is a software architect with more than twelve years of experience. He has worked extensively in the financial industry and has expertise in building scalable applications.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.