From the course: Kotlin Essential Training: Object-Oriented and Async Code
Top-level properties and functions - Kotlin Tutorial
From the course: Kotlin Essential Training: Object-Oriented and Async Code
Top-level properties and functions
- [Instructor] Let's talk code organization in Kotlin. More specifically, we're going to explore options for organizing our top-level properties and functions in a scalable way. Now, to quickly review, top-level properties and top-level functions refer to any property or function that is defined outside of any enclosing class. Now, throughout this course we've been using examples of both of these extensively. So, if we imagine a variable like this we might name it DEFAULT_CLICK_DELAY. This is an example of a top-level property. Or if we define a function like this where it's standalone within some Kotlin file, so maybe we'll name this log. So, this would be an example then of a top-level function. So, anytime that we create a standalone variable or a function in a Kotlin file and it's not within some other class you've created a top-level element. The use of top-level elements is a standard practice in Kotlin, but we should still use them pragmatically. Now, let's imagine that we have a file called TimeUtils.kt. So, I'm just going to create that file real quick here, TimeUtils.kt, there we go. Now, within TimeUtils here we want to define some constants and functions for working with time in our application. To start we're going to define a top-level property here called hourInMillis and set it equal to 60 minutes times 60 seconds times 1,000. This is a completely valid top-level property. We know that it has to do with time, so we've organized it into a file called TimeUtils. But there's still potentially an issue with the way this property is written and organized. Do you have any guesses as to what that issue might be? Well, if we come back over to Main.kt we can explore this. The potential problem here is that this property we've just defined within TimeUtils is public, and because it's public and because it's not scoped to any enclosing class that means it's available globally throughout our entire application. So, we could access that quite simply by referencing that name directly. Now, while this can be very convenient, especially in a small application, this can be problematic. Global variables are easy to misuse. They can be used for unrelated things, they can make it difficult to find other variables and functions as they pollute the global namespace, and they inherently break any kind of encapsulation we might have hoped for if we intended the top-level property to only be used within some given file or class. So, when we use top-level elements we should be mindful about the visibility applied to the property or function. If we return back to TimeUtils, in this case if we imagine we only wanted our hourInMillis property to be available within this TimeUtils file we could mark that property as private. Once we've made it private we'll see that we can no longer access that value outside of the file. So, in our Main.kt function here we now have an error indicating that hourInMillis is private within its file. Now, the same concept applies for top-level functions as well. So, again, we'll return back to TimeUtils here, and now let's imagine we're going to create some utility function here. So, we will name that millisForHours. It will take in an integer number of hours, and it will return that integer times our hourInMillis constant value. So, again, because this function is public we can call this from within Main without any issue. MillisForHours we could pass in a value of five here, and this will compile with no problem. Again, if we wanted to then make that function private only within the TimeUtils file we could do that. And again, we will now see the error in Main.kt indicating that the millisForHours function is private. Now, we've looked at public visibility and private visibility. However, there's also a middle ground that can be useful in certain situations, and that is internal. The internal modifier makes a property, function, or a class accessible within a given module, but not to the entire project. So, if we come back to TimeUtils and we replace the private with internal, now back in Main.kt we see that this Main.kt file has access to that millisForHours function. However, other modules, if this was a multi-module project, for example, they would not have access to this. Now, within the context of Kotlin and these visibility modifiers, a module essentially means a set of Kotlin files compiled together. Most basic projects that you might generate by default in IntelliJ are going to be single module, but as codebases scale it's quite common to split code into separate reusable modules, especially if using a build system like Gradle which is quite common with Kotlin projects. So, in these situations adding the internal modifier can be a good way to make constants and functions available to the specialized module, but not to the entire codebase. So, we'll return to TimeUtils here, and we'll just make that change to our hourInMillis value as well just to demonstrate that it works for properties in addition to functions. However, if we were going to be writing this in a production codebase we probably would want to keep that constant private. So, we'll remove it from our Main.kt file, and we'll add it back here. So, the idea here is that we have the constant private within the file, but the utility function is available more widely. So, now we've kind of encapsulated that constant within this file while still making the useful function available to more of our codebase. Understanding the different visibility modifiers is important when writing Idiomatic Kotlin with top-level properties and functions. As you write Kotlin code think carefully about how much of your application should be able to access the code. Is your property truly global? Then public might be the way to go. Should your function only be accessible within a file or within a class? Then you might consider making it private or protected. And if you're working in a multi-module project you'll find yourself using internal on many of your functions, classes, and properties.