• Labs icon Lab
  • Core Tech
Labs

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.

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 56m
Published
Clock icon Mar 01, 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 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:

    1. Your task involves implementing code within the files located in the src directory.
    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 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 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 Task data class with :

    • String property to represent the task name
    • Int property to represent the priority
    data class Task(val name: String, var priority: Int)
    

    Notice that name is prefixed with val and priority with var. 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 return null if dueDate is null, otherwise, it will call the toString() method on the dueDate.

    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 or dueDate is null, 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 a name, wateringFrequency, lastWateredDate, fertilizationFrequency, and lastFertilizedDate.

    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.

  3. 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 File

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

    fun main() {
        val gardenManager = GardenManager()
        gardenManager.start()
    }
    

    The GardenManager.kt File

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

    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 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 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 the GardenManager 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.

  4. 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.

  5. Challenge

    Implement Add Plant

    In this step, you will implement the addPlant method within the GardenManager 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 about Scanner and implemented the addPlant method.

    In the next step, you are going to learn about forEach loop and implement displayPlants method.

  6. Challenge

    Implement Display Plants

    In this step, you are going to complete the displayPlants method within the GardenManager 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 Loop

    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 lambda expression { element -> println("Element Value ${element}") } is executed. 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 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 of lastWateredDate and lastFertilizedDate.

    ${plant.lastWateredDate ?: "Never"}
    

    Now, you will use this understanding to complete the displayPlants 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 waterPlants function to water the plants in your garden.

  7. 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. The waterPlants 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 the lastWateringDate for the plant for tracking purposes.

    You are going to use the .minusDays method of LocalDate class.

    minusDays() method of java.time.LocalDate

    The minusDays() method is a feature provided by the java.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's lastWateredDate, 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 the LocalDate class.

    In the next step, you'll implement the fertilizePlants method similar to the waterPlants method.

  8. 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.

  9. 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 to myList.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 the indices 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.

  10. 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 using removeAt(index).

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

  11. 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 of FileOutputStream by passing the filename as a parameter to the constructor.

    val outputStream = FileOutputStream("example.txt")
    

    ObjectOutputStream

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

  12. 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 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.

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

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

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

    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, 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 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!

  13. 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!

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.