1. Why do we even need import if there is package
When a project is small, it can feel like import is “some kind of bureaucracy”. But as soon as you add a second package and a couple of utilities, you start writing spells like this: app.util.normalizeCommand(...). And it’s kind of fine… until you do it 20 times in one file. Then the code starts to look like the address book of a post office.
The point of import is very simple: it lets you use a short name instead of a full name (fully qualified name). A full name is an “address + name” separated by dots. For example, app.util.normalizeCommand. If the file has import app.util.normalizeCommand, then afterwards you just write normalizeCommand(...).
Let’s compare with a mini example.
Suppose we have a function in the app.util package:
package app.util
fun normalizeCommand(raw: String): String {
return raw.trim().lowercase()
}
Without an import, in another package you’d have to call it like this:
package app
fun main() {
val cmd = app.util.normalizeCommand(" HeLP ")
println(cmd) // help
}
With an import, it’s more readable:
package app
import app.util.normalizeCommand
fun main() {
val cmd = normalizeCommand(" HeLP ")
println(cmd) // help
}
In plain human terms: import means “I’m going to use this name a lot in this file, let’s skip the extra prefixes”.
Fully qualified name instead of import: when it can be even better
Sometimes you just don’t want to add an import. For example, you use some function exactly once. Or you’re writing a learning example and want to explicitly show where the name came from. In such cases, a fully qualified name is a perfectly normal tool.
For example, you needed a square root:
package app
fun main() {
val x = kotlin.math.sqrt(9.0)
println(x) // 3.0
}
This is a completely valid style. Moreover, sometimes it’s even more useful than an import: you see “a root from kotlin.math”, not “some sqrt that came from who-knows-where”.
A practical rule: if a name is used rarely, a fully qualified name is totally fine. If it’s used often, an import saves your eyes and keyboard.
2. Where import lives and why it doesn’t “include a file”
There’s a common (and very understandable) misconception: “I did an import, so I included the file”. In Kotlin, you import not a file and not even a “folder”, but a specific name (a function, class, object, property) that lives in another package.
Even more important: an import is local to the file. It only applies inside the .kt where it is written. In a neighboring file, you start over: its own package, its own import.
In Kotlin, the order in a file is usually:
- package ... (if you use packages)
- import ...
- declarations: functions, classes, etc.
Schematically it looks like this:
┌──────────────────────────────┐
│ package app │
├──────────────────────────────┤
│ import app.util.normalize... │
│ import kotlin.math.sqrt │
├──────────────────────────────┤
│ fun main() { ... } │
│ fun helper() { ... } │
└──────────────────────────────┘
If you try to write an import in the middle of a file after fun main(), the compiler won’t be happy. Kotlin is strict here, like a teacher who caught you with a cheat sheet: “imports go at the top”.
Import as a file “dependency”
A useful habit is to look at the list of import statements as “what this file needs in order to live”.
For example:
package app
import app.util.normalizeCommand
import app.format.formatAmount as prettyAmount
import kotlin.math.round as mathRound
It’s almost like the top tool rack in a workshop: “here’s a hammer, here’s a screwdriver, here’s electrical tape”. If there are too many imports and they feel random, the file often does too many things. If there are almost no imports, but inside the file you have huge constructs like a.b.c.d.e.f(...), you’re most likely suffering from missing imports (or the code needs to be split into functions and packages).
An IDE usually helps: it can add imports automatically, remove unused ones, and optimize the list.
3. import in a learning app: extracting command normalization
From this point on, let’s pretend we’re writing a small console app called BudgetBuddy. It doesn’t use collections (that comes later), doesn’t use classes (that’s even later), but it already knows how to read commands, normalize them, and run simple scenarios.
Let’s arrange it so that main lives in app, and utilities live in app.util.
File src/app/util/Command.kt:
package app.util
fun normalizeCommand(raw: String): String {
return raw.trim().lowercase()
}
Now file src/app/Main.kt:
package app
import app.util.normalizeCommand
fun main() {
print("Enter a command: ")
val cmd = normalizeCommand(readln())
println("Command after normalization: $cmd")
}
Notice the “contract”: normalizeCommand takes a String and returns a String. It does not call readln() itself. This is convenient: the function becomes pure and predictable, and main controls input/output.
4. import ...*: convenient, but turns code into a detective story
The star in an import (*) means: “import all available names from this package”. It looks like a cheat code:
import kotlin.math.*
After that you can write sqrt, round, ceil, floor without separate imports.
The problem is that the star makes code less obvious: you open a file and see round(...). But round—is that our round? Is it kotlin.math.round? Is it something else entirely? The bigger the project, the more often you’ll answer that question not with your brain but by searching the project.
That’s why in learning projects and in most team code styles, people prefer explicit imports:
import kotlin.math.sqrt
import kotlin.math.round
Star import is sometimes justified, but usually only when you fully understand the context. For example, a file is entirely about math and it truly uses a dozen functions from kotlin.math. In typical application logic, the star often turns into “why is it named this way and where did it come from”.
5. Name conflicts and alias
A name conflict is when in a single file you end up with two candidates for the same short name.
The most realistic scenario: you wrote your own round function, and then wanted to use kotlin.math.round. Or you wrote format, and then discovered another format in the project. Kotlin won’t guess “which one you meant”, because guessing is a path to bugs of the “it worked yesterday” variety.
Let’s intentionally create a conflict.
File src/app/util/Round.kt:
package app.util
fun round(x: Double): Int {
return x.toInt()
}
And now in Main.kt we want both ours and the math one:
package app
import app.util.round
import kotlin.math.round
fun main() {
println(round(3.6))
}
This code won’t compile: two imports with the same name round create ambiguity.
Kotlin has three sensible strategies: use the fully qualified name, change the import list, or use an alias (an import with a nickname). Below is the most readable and “grown-up” option: alias.
Alias import: a local name via as
Alias import looks like this:
import kotlin.math.round as mathRound
It’s like you’re saying: “in this file I will call kotlin.math.round by the name mathRound”. Important: you didn’t rename the function in the library and you didn’t change the project. You just created a local nickname for that name inside one specific file.
Let’s rewrite Main.kt so it works and stays clear:
package app
import app.util.round
import kotlin.math.round as mathRound
fun main() {
val x = 3.6
val a = round(x) // our version: Int, just truncation
val b = mathRound(x) // kotlin.math version: Double, proper rounding
println("a = $a") // a = 3
println("b = $b") // b = 4.0
}
Here the code is readable even for a beginner: you can see that mathRound is “mathematical rounding”, and round is something of our own.
Alias with two formatAmount: an example closer to a real app
With round the example is clear, but a bit artificial. In application code, conflicts more often involve words like format, parse, normalize, printReport. In other words: totally normal names you’d want to use in different places.
Let’s create two money amount formatters: one “raw” (just a number), another “pretty” (with currency). And we’ll intentionally give them the same name formatAmount to see alias in action.
File src/app/util/AmountFormat.kt:
package app.util
fun formatAmount(amount: Int): String {
return amount.toString()
}
File src/app/format/AmountFormat.kt:
package app.format
fun formatAmount(amount: Int): String {
return "$amount USD"
}
Now in Main.kt we want both: “raw” for debugging, and “pretty” for the user.
package app
import app.format.formatAmount as prettyAmount
import app.util.formatAmount as rawAmount
fun main() {
val income = 1200
println("DEBUG: income=${rawAmount(income)}") // DEBUG: income=1200
println("For the user: ${prettyAmount(income)}") // For the user: 1200 USD
}
Notice two things.
First: alias imports help not only to “resolve a conflict”, but also to add meaning. prettyAmount reads like “pretty formatting”, and rawAmount like “raw for debug”.
Second: an alias only applies within Main.kt. In another file you can choose different aliases—or not use alias at all if there are no conflicts.
6. Mini-refactoring BudgetBuddy: making main thinner
Now let’s assemble a small “practical” piece that looks like a real application (but still without collections and OOP).
The idea is: main reads a command, normalizes it, and prints a response. Input/normalization utilities live in app.util. Formatting lives in app.format.
File src/app/util/Input.kt:
package app.util
fun readTrimmedLine(prompt: String): String {
print(prompt)
return readln().trim()
}
File src/app/util/Command.kt:
package app.util
fun normalizeCommand(raw: String): String {
return raw.trim().lowercase()
}
File src/app/format/Amount.kt:
package app.format
fun formatAmount(amount: Int): String {
return "$amount USD"
}
File src/app/Main.kt:
package app
import app.format.formatAmount
import app.util.normalizeCommand
import app.util.readTrimmedLine
fun main() {
val raw = readTrimmedLine("Enter a command (income/exit): ")
val cmd = normalizeCommand(raw)
if (cmd == "income") {
println("Your income: ${formatAmount(1200)}") // Your income: 1200 USD
} else if (cmd == "exit") {
println("Bye!") // Bye!
} else {
println("Unknown command: $raw") // example: Unknown command: qwe
}
}
What matters: Main.kt became shorter and clearer. It doesn’t have to know how exactly we trim a string or how we format an amount. It simply “brings names in” via import and uses them as building blocks.
7. Typical mistakes when working with import and alias
Mistake #1: thinking import applies to the entire project.
An import is a per-file setting. If you added import app.util.normalizeCommand in Main.kt, that doesn’t mean it also appeared “by itself” in Report.kt. Each file has its own set of imports—and that’s normal: files should be independent and honestly declare what they need.
Mistake #2: turning import ...* into a default style.
The star saves a couple lines today, but it often creates confusion tomorrow: it becomes unclear where a name came from and why it’s exactly that. This is especially painful with name conflicts, when you suddenly discover that format exists “somewhere else” and you didn’t know it. It’s better to start with explicit imports and use * deliberately and rarely.
Mistake #3: trying to “rename a function in the project” via an alias import.
import ... as ... renames the name only inside one file. This is not refactoring and not renaming the declaration. If you imported kotlin.math.round as mathRound, you didn’t create a new function and didn’t change the library—you simply gave it a local nickname to make the code unambiguous.
Mistake #4: fixing a name conflict with random renaming instead of meaning.
When a conflict appears, beginners often make aliases like round1 and round2. The code compiles, but it becomes impossible to read. It’s better to choose an alias that explains the role: mathRound, prettyAmount, rawAmount, cliNormalize. This isn’t pedantry—it’s an investment so that a week later you don’t hate your past self.
Mistake #5: leaving junk (unused) imports.
Sometimes after experiments, imports remain that are no longer needed. This doesn’t break the program, but it clutters the file and hides the code’s real dependencies. A good habit is to periodically remove unused imports (an IDE can usually do this automatically), especially if you refactor actively and move functions across packages.
GO TO FULL VERSION