- Lab
- Core Tech

Guided: Build a Garden Manager in Kotlin
In this Code Lab, you'll delve into the creation of a Garden Manager application tailored for plant enthusiasts, designed to streamline garden management tasks such as adding plants with care details, displaying a comprehensive plant list, watering based on specified frequencies, fertilizing for optimal growth, adjusting watering schedules as plants develop, and removing plants when necessary. This lab will offer practical insights into Kotlin fundamentals, covering data handling with data classes, user interactions via a console-based interface, and file handling for persistent storage, ensuring seamless data retrieval across sessions while skillfully managing exceptions to uphold program integrity. By the end of the lab, you'll attain a concrete grasp of Kotlin programming, empowering you to construct straightforward yet impactful applications for various purposes.

Path Info
Table of Contents
-
Challenge
Introduction
Scenario
Imagine you have a beautiful garden with various types of plants and flowers, each requiring different care routines and attention. However, managing a garden effectively can be challenging, especially when you have multiple plants with distinct watering schedules and fertilization requirements. To simplify the task of garden management, a Garden Manager application can be incredibly useful! With this application, you can add your plants, set watering times, keep track of fertilization schedules, and receive reminders for plant care tasks.
Code Lab
In this Guided Code Lab, you will explore Kotlin programming to create a Garden Manager application tailored for plant enthusiasts. Your main focus will be on the
GardenManager.kt
file, where you'll implement essential functions necessary for building the Garden Manager 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. Implement date handling using
java.time.LocalDate
.User Interaction: Develop a console-based user interface for managing the garden. 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, allowing users to seamlessly manage their gardens across multiple sessions while ensuring their data is persisted and accessible.
By completing this lab, you'll not only enhance your Kotlin programming skills, but also gain practical experience in building a functional application for managing your garden effectively. Before you begin, here are some key points:
- Your task involves implementing code within the files located in the
src
directory. - If you encounter any challenges along the way, feel free to consult the
solution
directory. - 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.
- Your task involves implementing code within the files located in the
-
Challenge
Define the Data Classes for Garden Manager
To build your Garden Manager, 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 Plant data.
Get started by understanding what a data class is and how it can simplify the process of storing your plant-related information.
### Data ClassIn 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()
, andhashCode()
. 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
Task
data class with :String
property to represent the task nameInt
property to represent the priority
data class Task(val name: String, var priority: Int)
Notice that
name
is prefixed withval
and priority withvar
. In summary :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.
Default Values for Constructor Params
In Kotlin, you can also set default values for constructor parameters, including those used in data classes. Default values provide a convenient way to create instances of classes without needing to explicitly specify every parameter during object creation.
The priority property has a default value of 2, indicating a default priority level (e.g., Medium).
data class Task(val name: String, var priority: Int = 2)
Here's an example:
val task1 = Task("Complete assignment") println(task1) // Output: Task(name=Complete assignment, priority=2) val task2 = Task("Submit report", 1) println(task2) // Output: Task(name=Submit report, priority=1)
Nullable Properties
In Kotlin, nullable properties provide a way to express the possibility that a variable might not hold a value at some point during the program's execution. By default, in Kotlin, variables cannot hold null values unless explicitly declared as nullable using the
?
operator.Nullable properties are especially useful in scenarios where the absence of a value is a valid state or where the value might not be available at certain points in the program's flow. They offer a type-safe approach to handling potential null references and help prevent null pointer exceptions, a common source of bugs in many programming languages.
data class Task(val name: String, var priority: Int = 2, var dueDate: LocalDate? = null)
In the above example,
dueDate
is mutable and also can be assigned null.Now, you will look at different ways to access the
dueDate
property, which is nullable:1. Safe Access Operator (
?.
): You can use the safe access operator (?.
) to safely access properties or methods of nullable objects. This operator allows you to access a property only if the object is not null:// Using safe access operator to access the property val dueDateString: String? = task.dueDate?.toString()
In the above code,
task.dueDate?.toString()
will returnnull
ifdueDate
isnull
, otherwise, it will call thetoString()
method on thedueDate
.2. Elvis Operator (
?:
): You can use the Elvis operator (?:
) to provide a default value in case the expression on its left side is null:val task: Task? = Task("Example Task") val dueDateString: String = task?.dueDate?.toString() ?: "No due date"
In the above code, if
task
ordueDate
isnull
, the expression "No due date" will be used as a default value.3. Null Checks: You can perform explicit null checks to access nullable properties:
val task: Task? = Task("Example Task") if (task != null && task.dueDate != null) { println("Due Date: ${task.dueDate}") } else { println("No due date specified.") }
In this approach, you explicitly check if both task and
dueDate
are not null before accessing the property.These techniques ensure that you handle nullable properties safely, reducing the risk of null pointer exceptions in your code.
You learned about data classes, the difference between var and val , default values for constructor params and nullable values.
Next, you'll create a data class named
Plant
to encapsulate information about plants. A Plant typically consists of aname
,wateringFrequency
,lastWateredDate
,fertilizationFrequency
, andlastFertilizedDate
.In this step, you learned about data classes, difference between var and val keywords, default values for constructor params, and nullable values.
In the next step, you will understand about the
GardenManager
class and build a CLI menu. -
Challenge
Explore `GardenManager` Class & Construct the Menu
Now, you will review the current setup and the files which you will be working with.
The
Main.kt
FileThis file acts as an entry point, instantiating the
GardenManager
class and calling thestart()
function ofGardenManager
class.fun main() { val gardenManager = GardenManager() gardenManager.start() }
The
GardenManager.kt
FileThis file contains the code for user interactions and garden management functionality.
Private Variables
Within the
GardenManager
class, you have two private properties:-
plants
: This is a mutable list which will be used to store plants. -
scanner
: This is an instance of Scanner for handling user input in your program.
class GardenManager { private val plants = mutableListOf<Plant>() private val scanner = Scanner(System.`in`)
The
start
FunctionThis 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 thewhen
construct to create a switch-case statement.The
when
ExpressionIn 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:
println("Please enter a number (1-3): ") when (scanner.nextInt()) { 1 -> println("You entered 1") 2 -> println("You entered 2") 3 -> println("You entered 3") else -> println("Invalid Input") }
In the above example, the
else
option in awhen
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 Garden Manager:
1. Add Plant
addPlant()
: This function prompts the user to input details about a new plant they want to add to their garden. It captures the plant's name, watering frequency, and fertilization frequency, then adds the plant to the list of plants stored within theGardenManager
instance.2. Display Plants
displayPlants()
: This function presents a summary of all plants currently in the garden. It displays each plant's name, last watering date, watering frequency, last fertilization date, and fertilization frequency.3. Water Plants
waterPlants()
: This function simulates watering the plants in the garden. It checks each plant's last watering date and watering frequency to determine if watering is needed, updating the last watering date accordingly.4. Fertilize Plants
fertilizePlants()
: This function simulates fertilizing the plants in the garden. It checks each plant's last fertilization date and fertilization frequency to determine if fertilization is needed, updating the last fertilization date accordingly.5. Update Watering Frequency
updateWateringFrequency()
: This function allows users to update the watering frequency of a specific plant in the garden. It prompts the user to select the plant by its index and then input the new watering frequency.6. Remove Plant
removePlant()
: This function enables users to remove a plant from their garden. It prompts the user to select the plant by its index and removes it from the list of plants.7. Exit
This option exits the Garden Manager.
8. Save & Exit
This option exits the Garden Manager gracefully, saving the current plant data to a file named
plants.ser
.
Next, you will design the Menu for the Garden Manager. Great! You've successfully created a CLI-Based menu.
In the next steps, you are going to learn about exception handling in order to handle invalid inputs in your the garden manager program.
-
-
Challenge
Implement Exception handling in User Input
Exception handling in Kotlin allows you to gracefully handle unexpected situations that may arise during the execution of your program. In your case, you want to handle the scenario where the user enters invalid input (e.g., a non-integer value) when selecting options in the Garden Manager application.
Look at the following example:
val scanner = Scanner(System.`in`) try { println("Enter an integer:") val userInput = scanner.nextInt() println("You entered: $userInput") } catch (e: InputMismatchException) { println("Invalid input. Please enter a valid number.") }
- If the user enters a non-integer value, an
InputMismatchException
is thrown. - In the catch block, the
InputMismatchException
is handled by printing an error message indicating that the input provided by the user is invalid.
By using exception handling, you ensure that the program can gracefully handle scenarios where the user provides invalid input and continue executing without crashing.
Now, you will implement exception handling in your Menu, which ensures if the user enters a invalid input then it should display a message "Invalid input. Please enter a valid number." Great! You've successfully learned how to implement exception handling.
In the next step, you are going to implement the
addPlant
function, which will help you to add a plant to your garden. - If the user enters a non-integer value, an
-
Challenge
Implement Add Plant
In this step, you will implement the
addPlant
method within theGardenManager
class. This method prompts you to enter the name of a new plant, watering frequency, and fertilizing frequency. A plant instance should be created with these details and added to the plant list.You are going to use
Scanner
in order to get the user input. Next, you will learn about Scanners.Scanner
Scanners allow you to read input from the standard input stream (System.in). You can use them to retrieve various types of data, such as strings, numbers, or entire lines of text, entered by the user during program execution.
Some commonly used methods include:
next()
: Reads the next token (a word) from the input.nextInt()
: Reads the next token as an Int.nextLine()
: Reads the next line of input as a string.
Here is an example:
val name = scanner.next()
In the above example, the user input from the console will be assigned to variable
name
.The function
addPlant()
is responsible for allowing users to add a new plant to the Garden Manager application. Now, you will complete the implementation of this method. In this step, you learned aboutScanner
and implemented theaddPlant
method.In the next step, you are going to learn about
forEach
loop and implementdisplayPlants
method. -
Challenge
Implement Display Plants
In this step, you are going to complete the
displayPlants
method within theGardenManager
class. If there are plants added, this method prints the name of each plant along with watering frequency, fertilization frequency, last watered date, and last ferilization date.
In order to correctly implement this method, you will learn how you iterate over a list using the
forEach
loop.The
forEach
LoopforEach
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 lambda expression{ element -> println("Element Value ${element}") }
is executed. The current element is represented by the parameterelement
within the lambda expression.
Next, you will learn about a variation of
forEach
loop that isforEachIndexed
.The forEachIndexed Loop
forEachIndexed
loop in Kotlin is similar to theforEach
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
andelement
.The
index
parameter represents the index of the current element being iterated over. Theelement
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 plants, displaying the index alongside each plant's details aids in clear identification and organization of the plant list.
You will use the Elvis Operator (
?:
) to print the nullable properties oflastWateredDate
andlastFertilizedDate
.${plant.lastWateredDate ?: "Never"}
Now, you will use this understanding to complete the
displayPlants
method. In this step, you learned aboutforEach
andforEachIndexed
loops and now understand when to use them.In the next step, you are going to implement
waterPlants
function to water the plants in your garden. -
Challenge
Implement Water Plants
In your garden management system, watering plants is a fundamental aspect that directly influences the health and well-being of your greenery.
The primary purpose of the
waterPlants
method is to water the plants in the garden according to their individual watering requirements. ThewaterPlants
method typically iterates through the list of plants in the garden and checks each plant's last watered date and watering frequency.If a plant's last watered date is beyond its specified watering frequency, the
waterPlants
method initiates watering for that plant. It updates thelastWateringDate
for the plant for tracking purposes.You are going to use the
.minusDays
method ofLocalDate
class.minusDays() method of java.time.LocalDate
The
minusDays()
method is a feature provided by thejava.time.LocalDate
class in Java and Kotlin, designed for date manipulation.Here is an example:
val newDate = currentDate.minusDays(daysToSubtract)
Suppose today's date is 10th Jan and the watering frequency of a plant is every
3
days. The plant was last watered on 6th Jan.Does the plant need watering = ( (10th -
3
) >= 6th Jan )Does the plant need watering = ( (7th Jan) >= 6th Jan )
Does the plant need watering = true
In this case, if the plant was watered on or before 7th, then the plant needs watering.
Next, you will understand the code required for this. In your case, in order to check if the plant needs watering, you'll use the below code :
currentDate.minusDays(plant.wateringFrequency.toLong()) >= plant.lastWateredDate
Here's the logic behind the expression:
- If the calculated date (representing
currentDate
minus the watering frequency) is greater than or equal to the plant'slastWateredDate
, then it's time to water the plant again. - Vice versa, If the calculated date is not greater than or equal to the
lastWateredDate
, it means that the plant does not yet need watering.
Also if the plant has newly been added and has never been watered then you need to water those plants as well. This you can check using the expression:
plant.lastWateredDate == null
Now, you will use this understanding to complete the
waterPlants
method. In this step, you learned about date manipulation using the available functions in theLocalDate
class.In the next step, you'll implement the
fertilizePlants
method similar to thewaterPlants
method. - If the calculated date (representing
-
Challenge
Implement Fertilize Plant
Just like watering, fertilization is equally crucial to ensure plants receive adequate nutrients.
Utilize the same concepts learned in the previous step to implement the
fertilizePlants
method. -
Challenge
Implement Update Watering Frequency
Updating the watering frequency is a critical aspect of plant care that allows gardeners to adjust and optimize their watering routines based on plant needs and environmental conditions. By fine-tuning the watering frequency, gardeners can ensure that plants receive optimal moisture levels, promoting healthier growth and resilience.
updateWateringFrequency
The
updateWateringFrequency()
function allows users to adjust the watering frequency of plants in the garden management system. After displaying the list of plants, users are prompted to select a plant by its index and input a new watering frequency in days. If the selected plant index is valid, its watering frequency is updated accordingly. If the plant index is invalid, an error message is displayed, prompting the user to try again.Before you proceed for implementing the
updateWateringFrequency
method, it's good idea to understand some Kotlin features which will be useful in implementation.indices Property
The
indices
property is a convenient feature available in Kotlin collections that provides access to the indices of the elements in the collection such as lists, arrays, or other iterable data structures.Here is an example:
val myList = listOf("apple", "banana", "orange") for (i in myList.indices) { println("Element at index $i: ${myList[i]}") }
In this example,
myList.indices
provides a range of indices starting from 0 tomyList.size - 1
. It allows you to iterate over each element in the list and access its value using indexing.
Also the indices can also be used to check if the index is within the valid range of indices.
Here is example:
val fruits = listOf("apple", "banana", "orange") val indexToCheck: Int = ... // Check if the indexToCheck is within the valid range of indices for the fruits list if (indexToCheck in fruits.indices) { // The indexToCheck is valid, access the element at the specified index val selectedFruit = fruits[indexToCheck] println("Selected fruit: $selectedFruit") } else { // The indexToCheck is not within the valid range of indices println("Invalid index. Please choose a valid index.") }
Using
indices
in this way helps prevent index out-of-bounds errors and ensures safe access to elements in the collection. It's a concise and readable way to perform bounds checking when working with indices.
Now, you will implement the
updateWateringFrequency
method. In this step, you learned about theindices
property of collection class.In the next step. you are going to implement the
removePlant
method to remove a plant which you no longer want it in your Garden. -
Challenge
Implement Remove Plant
There is a possibility that a plant did not flourish or you are no longer interested in keeping a plant in your garden. In such cases you can use the
removePlant
functionality of the Garden Manager.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
deletePlant()
function. In this step, you learned about how to remove a item from a list usingremoveAt(index)
.In the next step, you're going to learn about saving the plant data in a file by implementing file handling.
-
Challenge
Save Plants Information to a File
Saving plant 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 plant 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.
FileOutputStream
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 ofFileOutputStream
by passing the filename as a parameter to the constructor.val outputStream = FileOutputStream("example.txt")
ObjectOutputStream
ObjectOutputStream
is a subclass ofOutputStream
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 ofObjectOutputStream
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 theSerializable
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
Plant
data class serializable so that plants information can be saved in a file. You have now successfully stored the plants' information in a file upon exiting the Garden Manager.In the next step, you will explore how to reload the plants' information when the Garden Manager is restarted.
- Notice that the
-
Challenge
Reload Plant Information from File
In this step, you'll look at reloading the stored plants information as the application is restarted.
Now, you will learn about
init
block in Kotlin, which is useful to load the plants information as the application restarts.--
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 { loadPlantsFromFile() // Load plants data from file on application startup }
The
init
block is executed immediately after the primary constructor of the class is called. If there are multipleinit
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.
file.exists()
The
file.exists()
function is used to check whether a file or directory exists at the specified path. It is a method of theFile
class, which represents a file system path.
As in the last step, you understood about
FileOutputStream
andObjectOutputStream
for saving files.In this step, you'll learn about the
FileInputStream
and `ObjectInputStream classes for reading the files.FileInputStream
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. WhileFileInputStream
reads bytes from a file, it does not offer high-level methods for directly reading objects. However, you can use it in conjunction withObjectInputStream
to read serialized objects from a file.ObjectInputStream
ObjectInputStream
is a higher-level class that allows objects to be read from an input stream. It's used for deserializing objects previously written usingObjectOutputStream
.After initializing
ObjectInputStream
, you can use itsreadObject()
method to read the next object from the input stream.Here is an example of reading Person objects from a file :
val 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, whetherMutableList
orList
, determines the mutability of the resulting collection.The choice between
MutableList
andList
during deserialization depends on your requirements for mutability. If you need the flexibility to modify the collection, useMutableList
. If you prefer immutability and want to ensure the collection remains unchanged, useList
.After reloading the plants information, plants can be added or modified so
MutableList
is the correct choice in this case.
Now, you will implement the
loadPlantsFromFile
method. It's time to run your Garden Manager application! In the bottom-right corner of the Terminal, click on the Run button.This will display the Garden Manager menu and you can try the features of your application.
Kudos!
-
Challenge
Conclusion & Next Steps
Congratulations on creating a functional Garden Manager application!
Throughout this Code Lab, you've delved into key Kotlin programming concepts while constructing a practical tool for managing your garden. You've tackled tasks such as handling user input, managing lists of plants, implementing exception handling, and persisting data to files, all of which are essential skills in Kotlin development.
Next steps for enhancing your Garden Manager include:
-
Plant Details Enrichment : Provide users with the ability to add additional details to each plant, such as plant variety, preferred sunlight exposure, soil type, and more. These details can help users better understand and care for their plants.
-
Reminder System : Implement a reminder system that notifies users when it's time to water or fertilize their plants based on the specified watering and fertilization frequencies. Integrating notifications can help users stay on top of their garden maintenance tasks.
-
Plant Health Monitoring : Introduce features to track the health and growth of plants over time. Users can input observations about their plants' health, growth milestones, and any issues they encounter, allowing for better management and troubleshooting.
As you continue your Kotlin learning journey, consider exploring the following courses 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!
-
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.