Hamburger Icon
  • Labs icon Lab
  • Core Tech
Labs

Guided: Build an Event Scheduler in Kotlin

Embark on a hands-on journey to develop an Event Scheduler application using Kotlin. This Guided Code Lab will introduce you to essential Kotlin features such as object-oriented programming, exception handling, and date and time handling. You'll learn how to interact with the console, schedule tasks using timers, and build a robust event management system. By the end, you'll be equipped with the skills to create your own Kotlin applications with ease.

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 36m
Published
Clock icon Apr 12, 2024

Contact sales

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

Table of Contents

  1. Challenge

    Introduction

    Scenario:

    Imagine being part of a dynamic event management company where orchestrating various events is a daily affair. From corporate conferences to weddings, each event requires meticulous planning and coordination. To streamline this process, you've been entrusted with the task of developing a software tool named EventKeeper. This tool will empower users to effortlessly create, update, and manage events with ease, while also providing timely reminders to ensure no event is overlooked.


    Code Lab

    In this Guided Code Lab, immerse yourself in Kotlin programming as you embark on the journey to develop the EventKeeper, an event scheduler tailored for busy professionals. Your focal point will be the EventKeeper.kt file, where you'll implement crucial functionalities pivotal to the EventKeeper application.

    Completing this 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.

    Functional Programming: Leveraging Kotlin's functional programming capabilities to write concise, expressive, and maintainable code, enhancing readability and efficiency.

    User Interaction: Develop a console-based user interface for managing the Events. Manage user input and execute corresponding actions.

    Error Handling: Implement robust error handling mechanisms to ensure a smooth user experience.

    File Handling: Learn to read from and write to files using Kotlin's file handling APIs, facilitating persistent storage of event data and seamless access across multiple sessions.

    Time and Scheduling: Explore implementing features that enable scheduling tasks, setting reminders with timers, and ensuring timely notifications and management of events.

    By completing this lab, you'll not only enhance your proficiency in Kotlin programming, but also acquire insights about modern event management. You'll gain the skills to efficiently manage event scheduling, handle user interactions, implement robust error handling mechanisms, and ensure seamless data persistence which are essential elements for building functional and reliable software solutions.

    Before you begin, here are some key points:

    1. Your task involves implementing code in the Event.kt and EventKeeper.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 Event Data Class

    To build your EventKeeper 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 Event data.

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


    Data Class

    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:

    • A String property to represent the meeting title
    • A LocalDate property to represent the Date
    • A LocalTime property to represent the Time
    data class Meeting(val name: String, var date: LocalDate, var time: LocalTime)
    

    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 to the above example, you will utilize the LocalDate instance to retain the eventDate, signifying the year, month, and day of the event. Meanwhile, LocalTime will be used to capture the eventTime, specifying the hour and second of the event time.

    // Parse a string representing the date 2024-03-20
    val parsedDate = LocalDate.parse("2024-03-20")
    
    // Parse a string representing the time 15:30:00
    val parsedTime = LocalTime.parse("15:30:00")
    

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

    Next, you'll create a data class named Event to encapsulate information about events. An Event typically consists of a title, description, eventDate, and eventTime.

    In this step, you learned about data classes and the difference between var and val keywords.

    In the next step, you will understand about the EventKeeper class and build a CLI menu.

  3. Challenge

    Explore `EventKeeper` & Construct the CLI Menu

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

    The Main.kt File

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

    fun main() {
        val eventKeeper = EventKeeper()
        eventKeeper.start()
    }
    

    The EventKeeper.kt File

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

    Private Variables

    Within the EventKeeper class, you have three private properties:

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

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

    • timer: A Timer object that you'll utilize for scheduling tasks such as reminders within the EventKeeper class. You will look at it in more detail in subsequent steps.

    class EventKeeper {
        private val events = mutableListOf<Event>()
        private val reader = BufferedReader(InputStreamReader(System.`in`))
        private var timer: Timer? = null
    

    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 EventKeeper:

    1. Add Event

    addEvent() : This function prompts the user to input details about a new event they wish to add. It captures the event's title, description, date, and time, adding it to the list of events stored within the EventKeeper instance.

    2. View Events

    viewEvents() : Displays a summary of all events currently stored in the EventKeeper. Each event's title, description, date, and time are presented.

    3. Search Event

    searchEvent() : Enables users to search for events based on a keyword. It prompts the user to enter a keyword, and then displays all events whose title or description contains the provided keyword.

    4. Update Event

    updateEvent() : Users can update details of an existing event. They are prompted to input the title of the event they wish to update, and then provide new details such as description, date, and time.

    5. Delete Event

    deleteEvent() : Allows users to remove an event from the list. Users need to specify the title of the event they want to delete.

    6. Exit

    Option to exit the EventKeeper.

    7. Save & Exit

    Gracefully exits the EventKeeper while saving the current event data to a file named events.ser.


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

    In the next step, you'll be implementing the addEvent function to facilitate the addition of events to your EventKeeper.

  4. Challenge

    Implement Add Event

    In this step, you will implement the addEvent method within the EventKeeper class. This method prompts you to enter the title of the event, description, event date, and event time. An event instance should be created with these details and added to the event to the list.

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

    BufferedReader

    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 function addEvent() is responsible for allowing users to add a new event to the EventKeeper application. Now, you will complete the implementation of this method. In this step, you learned about how to read data using a BufferedReader.

    Prior to saving an event, it is imperative to verify that two events cannot occur simultaneously, thereby preventing conflicts. In the subsequent step, you will proceed to implement code to mitigate such conflicts.

  5. Challenge

    Implement Conflict Resolution

    A fundamental functionality of EventKeeper is to ensure that no two events occur at the same time to avoid conflicts and confusion among your users. Without this check, users might inadvertently schedule multiple events for the same date and time, leading to overlapping schedules.

    To implement the isEventConflict function, you will need to understand about the functions present in collections.

    In Kotlin, the any function is part of the standard library and is commonly used with collections to determine if at least one element satisfies a given condition. It's often used in conjunction with lambda expressions or other predicates.

    The any function is an extension function defined for collections. Its signature is as follows:

    inline fun <T> Iterable<T>.any(predicate: (T) -> Boolean): Boolean
    

    Example :

    val numbers = listOf(1, 2, 3, 4, 5)
    
    // Check if any number in the list is even
    val anyEven = numbers.any { it % 2 == 0 } 
    // Returns true, as there are even numbers in the list
    
    // Check if any number in the list is negative
    val anyNegative = numbers.any { it < 0 } 
    // Returns false, as all numbers are positive
    

    Similar Functions:

    1. all: Checks if all elements in the collection satisfy the given predicate.

    inline fun <T> Iterable<T>.all(predicate: (T) -> Boolean): Boolean
    

    2. none: Checks if no elements in the collection satisfy the given predicate.

    inline fun <T> Iterable<T>.none(predicate: (T) -> Boolean): Boolean
    

    3. count: Counts the number of elements in the collection that satisfy the given predicate.

    inline fun <T> Iterable<T>.count(predicate: (T) -> Boolean): Int
    

    4. find: Returns the first element in the collection that satisfies the given predicate, or null if no such element is found.

    inline fun <T> Iterable<T>.find(predicate: (T) -> Boolean): T?
    

    These functions provide powerful tools for working with collections in Kotlin, allowing for concise and expressive code when dealing with conditions and filtering elements.


    Now, you will use this understanding to implement the isEventConflict method. In the next step, you'll implement the viewEvents method which will enable you to view the events.

  6. Challenge

    Implement View Events

    In this step, you are going to complete the viewEvents method within the EventKeeper class. If there are events added, this method prints the title of each event along with description, event date, and event 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

    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 events, displaying the index alongside each event details aids in clear identification and organization of the event list.

    Now, you will use this understanding to complete the viewEvents 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 searchEvent function to quickly search for the events that you are interested in.

  7. Challenge

    Implement Search Events

    Given that the EventKeeper may contain a significant number of events over time, it's essential to allow users to search for events they are interested in using criteria such as title or description. This approach ensures that the EventKeeper enables users to easily discover events of interest.


    In order to efficiently implement the searchEvent 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 searchEvent 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 event.

  8. Challenge

    Implement Update Event

    There may be a chance that an Event is rescheduled. In that case, the EventKeeper must have a capability to update the Event information to accommodate those changes.


    In order to efficiently implement the updateEvent function, you will learn about how to find a item in a list.

    The find Function

    In Kotlin, the find function is a convenient method used to search for an element in a collection that satisfies a given predicate. It is primarily utilized with iterable data structures such as lists, arrays, or other collections. The find function returns the first element that matches the specified predicate or null if no such element is found.

    Example :

    data class Person(val name: String, val age: Int)
    
    fun main() {
        val people = listOf(
            Person("Alice", 30),
            Person("Bob", 25),
            Person("Charlie", 35),
            Person("David", 40)
        )
    
        // Searching for a person with age 25
        val personWithAge25 = people.find { it.age == 25 }
    
        if (personWithAge25 != null) {
            println("Person found: ${personWithAge25.name})
        } else {
            println("Person with age 25 not found.")
        }
    }
    
    

    In this above example, the find function returns a person object Bob with age 25.


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

    In this step, you learned about the find function in collection class.

    Next, you will implement the delete event functionality in the EventKeeper.

  9. Challenge

    Implement Delete Event

    Delete Event functionality allows you to permanently remove the event from your schedule, ensuring there are no remaining reminders for it. Whether the event has been canceled, rescheduled, or simply lost its significance, the Delete Event option helps you maintain a tidy and organized schedule by eliminating unnecessary clutter.

    In order to implement the delete event function, you will 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 deleteEvent() 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 event data in a file by implementing file handling.

  10. Challenge

    Save Events to a File

    Saving event 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 event 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 Subclass

    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.

    The Serializable Interface

    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 Event data class serializable so that events information can be saved in a file. In this step, the events information is successfully stored in a file upon exiting the EventKeeper.

    In the next step, you will explore how to reload the events information when the EventKeeper is restarted.

  11. Challenge

    Reload Events from File

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

    Now, you will learn about the init block in Kotlin, which is useful to load the events 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 events data from file on application startup
     loadEventsFromFile() 	
    }
    

    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() Functions

    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 last 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 events information, events can be added or modified so MutableList is the correct choice in this case.


    Now, you will implement the loadEventsFromFile method. Having successfully implemented event persistence, your next task involves incorporating reminder functionality into the EventKeeper, ensuring you don't miss out on important events.

  12. Challenge

    Implement Event Reminders

    Event reminders are vital in the EventKeeper application as they provide timely notifications about upcoming events. They prevent missed events, improve user experience, reduce scheduling conflicts, enhance engagement, and increase accountability. Overall, reminders help users stay organized and productive, ensuring they are aware of their commitments and can plan their time effectively.

    You will now understand about implementing Timers in Kotlin, which will be useful for your implementation of Event reminders.


    The Timer and TimerTask Classes

    In Kotlin, as in many programming languages, you can use timers to execute code at specified intervals or after a certain delay. There are multiple ways to implement timers in Kotlin, but one common approach is using the Timer class from the java.util package.

    Here's how you can use it:

    1. Create a TimerTask:

    Extend the TimerTask class and override its run() method with the code you want to execute when the timer triggers.

    class MyTimerTask : TimerTask() {
        override fun run() {
            // Your code to be executed when the timer triggers
            println("Timer task executed!")
        }
    }
    

    2. Create a Timer instance:

    Instantiate a Timer object and schedule your TimerTask to execute at specific intervals or after a delay.

    fun main() {
        val timer = Timer()
        val delay = 1000L // 1 second delay
        val period = 2000L // 2 seconds interval
    
        // Schedule the task to run after the delay and then repeatedly at the specified interval
        timer.schedule(MyTimerTask(), delay, period)
    }
    

    In this example, the Timer schedules the MyTimerTask to run after a 1-second delay (delay) and then repeatedly every 2 seconds (period).

    Remember to handle exceptions properly, especially when dealing with tasks that may throw exceptions during execution. Additionally, ensure that you cancel the timer when it's no longer needed to prevent resource leaks. You can cancel the timer by calling its cancel() method.

    // Cancel the timer when it's no longer needed
    timer.cancel()
    

    You will now implement the Event reminder functionality in the EventKeeper. It's time to run your EventKeeper application! In the bottom-right corner of the Terminal, click on the Run button.

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

    Kudos!

  13. Challenge

    Conclusion & Next Steps

    Congratulations on creating a functional EventKeeper application!

    Throughout this Code Lab, you've delved into key Kotlin programming concepts while constructing a practical tool for Event Keeping. You've tackled tasks such as handling user input, managing lists of events, implementing exception handling, reminders using Timer, and persisting data to files, all of which are essential skills in Kotlin development.

    Next steps for enhancing your EventKeeper include:

    • Add End Time to the Event: Currently only if the Event start time is matching with other event then only you are not allowing it to add. To make it more practical you can add End Time and evaluate if any new event overlaps with any other event using the Start and End time.

    • Customize Reminder Time: In your implementation, you are reminding before 15 minutes to the event. This can be made configurable by adding a new property in Event data class and then considering the same while sending reminders.

    • Mark Events as Notified: You can enhance the Event data class to send reminders only once and mark the Event has notified once the notification has been sent.

    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.