1. Multiline strings
When you are just starting to program, it seems that printing to the console is simply: println("something"), and that’s it. But as soon as you want to print a menu, help text, a “nice header,” or a mini report across multiple lines, the circus begins. You can insert \n manually, but the code becomes hard to read, and editing the text turns into a game of “find where the extra slash is.”
That is why Kotlin has multiline strings (also called raw strings): they let you write text in code almost exactly as it should look in the console, without a “hit parade” of \n. Kotlin conventions explicitly recommend using multiline strings instead of manually embedding \n, and using trimIndent() or trimMargin() to control indentation.
As a warm-up, here’s a small contrast. First we’ll do it the blunt way with \n, and then we’ll do it nicely.
fun main() {
val help = "Commands:\n" +
"1) add\n" +
"2) list\n" +
"3) exit\n"
print(help)
}
Yes, it works. But editing something like this is about as enjoyable as “cleaning your keyboard with a toothbrush.”
Multiline string syntax """...""" and how it differs from "..."
A multiline string in Kotlin is written with triple quotes: """ ... """. It looks a bit unusual, but the logic is simple: inside such a literal you can write line breaks as-is. That is, you press Enter—and a real newline ends up in the string. This dramatically improves readability, especially for large blocks of text: menus, report templates, “banners,” etc.
An important nuance: it is called a raw string not because it is undercooked, but because inside it you barely need escaping. In particular, \n and \t inside a raw string are just two characters \ and n (or t), not “newline” and “tab.” This is both convenient and tricky: convenient because there is less escaping; tricky because sometimes you expect one thing and get another.
Here is a basic example of a multiline string:
fun main() {
val help = """
Commands:
1) add
2) list
3) exit
"""
print(help)
}
Notice: the text in the console will be exactly the same as in the code. That is the whole point.
Why indentation “sticks” to the output and why you need trimIndent()
When you start writing multiline text inside a function, you almost always indent the code—simply because it looks nicer and follows formatting rules. But a raw string preserves spaces literally. This creates a classic situation: in the source everything is aligned, but in the console the menu suddenly shifts right by four spaces, because you wrote the string inside the block fun main().
trimIndent() solves exactly this problem: it removes the common (minimum) indent from all non-empty lines of the multiline literal. Kotlin conventions phrase it like this: “use trimIndent() when the result does not need special internal indentation.”
Example: first “bad” (indentation will go into the output), then “good.”
fun main() {
val help = """
Commands:
1) add
2) list
3) exit
"""
print(help)
}
This code will print lines with indentation. Now the same thing, but with trimIndent():
fun main() {
val help = """
Commands:
1) add
2) list
3) exit
""".trimIndent()
print(help)
}
Now the spaces that were needed only for a nice-looking source do not break the final text.
A simple way to remember it: trimIndent() is like “remove the code’s service indentation, but keep relative indentation inside the text if it actually differs.”
trimMargin() — when you need more precise control over indentation
Sometimes you want the text to have indentation inside (for example, you are printing a piece of pseudocode, an email template, or a report block), but you do not want to drag extra left-side “junk” into the output due to Kotlin code formatting. In such cases trimIndent() may not be controllable enough: it removes a common indent, but does not let you explicitly mark the text boundary.
That’s where trimMargin() comes in: you choose a marker character (by default it is |) and write lines so that everything “to the left of the marker” is cut off. In Kotlin conventions this is described as an option for cases where internal indentation of the text is needed and important.
Example with trimMargin():
fun main() {
val report = """
|User: Ira
|Status: OK
|Message: Data accepted
""".trimMargin()
println(report)
}
Here you can align the code indentation however you want, but the “text boundary” stays stable. If you look closely at examples from Kotlin guides, it’s the same idea: | helps neatly “cut off” the left part and keep the output clean.
A small bonus: trimMargin() can work not only with |. You can choose another marker (for example >), but most of the time | is the de facto standard, and it’s better not to reinvent the wheel unless you run a wheel factory.
Multiline strings and string templates: how to substitute values
Up to this point we used string templates in regular strings: $name, ${a + b} and so on. They work the same way inside """...""". This is convenient: you can build a multiline “report template” and insert variables into it without concatenation.
Reminder on template syntax: a simple identifier can be inserted as $a, and expressions as ${...}.
Here’s a short example of “multiline report + data”:
fun main() {
val user = "Ira"
val total = 3
val text = """
|Report for pol’user
|---------------------
|Name: $user
|Purchases: $total
""".trimMargin()
println(text)
}
In the previous lecture we discussed an interesting case: if you need to print the dollar sign as text, Kotlin will try to interpret $ as the start of a template. In a regular string you would write "\$". In a raw string this is also relevant, but today we will not go deeper—just remember that $ is a “special character” for substitutions, and it is not always “just a dollar.”
2. Simple text alignment: padStart() and padEnd()
How to make simple “columns” without turning into Excel
After you learn to print multiline text, you almost immediately want the next thing: “can we make the numbers line up neatly in a column, like in a table?” Yes. And no, we do not need Excel, a magical formatter, or printing on a 1997 dot-matrix printer (although it has its own charm).
padEnd(width) pads a string on the right with spaces (or another character) so it reaches the desired length. padStart(width) does the same, but on the left. As a result, you can align names to the left and numbers to the right, like in reports and receipts.
One important note right away: these functions work on strings. Therefore numbers (Int) usually need to be converted to strings via toString(). You already know how to do that.
Mini example of alignment:
fun main() {
val item = "Tea"
val qty = 3
val price = 90
val line = item.padEnd(10) +
qty.toString().padStart(3) +
" x " +
price.toString().padStart(4)
println(line) // Tea 3 x 90
}
Here we made a “very simple” table-like row. Yes, it is not a full report yet, but you can already see that values do not jump around, even if qty becomes 12, and price becomes 160.
3. Printing a receipt in a console app
Now we will assemble a small cohesive piece of a program that we will continue to develop later in the course. The idea is simple: a mini “Coffee shop in the console.” The user enters quantities (for now without complex commands and without keeping years of history), and the program prints a neat receipt: with a header, a list, and a total.
Why this is a good example for this lecture: we immediately have a multiline “header” and “footer,” and we have table-like lines where padStart()/padEnd() genuinely help.
First, a function that returns a nice header. We deliberately return a String so you can see: a multiline string is a regular string, just written more conveniently.
fun buildHeader(): String {
return """
|========================
| Kotlin Coffee CLI
|========================
""".trimMargin()
}
Now let’s make a function for formatting one receipt line. For now we’ll keep it as simple and “stringy” as possible, without bells and whistles.
fun formatLine(name: String, qty: Int, price: Int): String {
val sum = qty * price
return name.padEnd(12) +
qty.toString().padStart(3) +
" x " +
price.toString().padStart(4) +
" = " +
sum.toString().padStart(5)
}
Notice the style: first we computed sum, and then we formatted the line. This is almost always more readable than stuffing qty * price inside a long concatenation. In the previous lecture we discussed a similar idea for string templates: compute the complex part first into a val, then put it into the text.
Now—main, where we use the header, formatting, and a multiline input prompt. Here we use arrays because they are perfect for a “fixed set of products” at the start.
fun main() {
val names = arrayOf("Espresso", "Cappuccino", "Tea")
val prices = intArrayOf(120, 160, 90)
val qty = IntArray(names.size)
println(buildHeader())
for (i in names.indices) {
print("How many '${names[i]}'? ")
qty[i] = readln().toInt()
}
println()
println("Receipt:")
for (i in names.indices) {
println(formatLine(names[i], qty[i], prices[i]))
}
}
Let’s add the final touch: the receipt footer, also as a multiline string. And show the total amount. The total is just an Int, computed in a loop.
fun buildFooter(total: Int): String {
return """
|
|------------------------
|ITOGO: ${total.toString().padStart(5)} euros.
|Thank you! Come again :)
""".trimMargin()
}
And let’s update the end of main by adding the total calculation and printing the footer:
fun main() {
val names = arrayOf("Espresso", "Cappuccino", "Tea")
val prices = intArrayOf(120, 160, 90)
val qty = IntArray(names.size)
println(buildHeader())
for (i in names.indices) {
print("How many '${names[i]}'? ")
qty[i] = readln().toInt()
}
var total = 0
println("\nReceipt:")
for (i in names.indices) {
total += qty[i] * prices[i]
println(formatLine(names[i], qty[i], prices[i]))
}
println(buildFooter(total))
}
You now have a program where the header and footer are readable in the source (no need to mentally interpret \n), indentation does not drift thanks to trimMargin(), and the receipt lines look neat thanks to padStart()/padEnd(). That is exactly the style that is typically recommended: multiline strings plus explicit indentation control via trimIndent()/trimMargin().
To cement the picture, here is a small diagram of what we did (not code, but a “thought route”):
flowchart TD
A["Writing text in '''''' ...'''''' "] --> B["Removing indents trimIndent/trimMargin"]
B --> C["Substituting data via $var and ${expr}"]
C --> D["Aligning columns padEnd/padStart"]
D --> E["println(...) to console"]
4. Typical mistakes when working with multiline strings
Mistake #1: expecting """...""" to automatically “align nicely.”
A raw string preserves spaces and line breaks literally. So if you wrote text with indentation in the code, it will be printed with that indentation. The fix is not “think happy thoughts,” but specifically: trimIndent() or trimMargin()—depending on whether internal indentation is needed.
Mistake #2: writing \n inside a raw string and expecting a newline.
Inside """...""" the sequence \n does not turn into a line break; it is just two characters. A newline is made by a real Enter. If you want to “control” line breaks, do it either with real line breaks, or use a regular string "..." (but today the whole point is not to suffer with \n).
Mistake #3: confusing what exactly trimIndent() removes.
trimIndent() removes the common minimum indent from non-empty lines. If you want to explicitly control the boundary of each line (for example, part of the text must be “inside a block” and have its own indentation), then trimMargin() is usually clearer: you place | yourself and see where the useful text begins.
Mistake #4: trying to align numbers by eyeballing spaces.
When you type "Tea 3 90" by hand, it seems fine until you get "Cappuccino 12 160". Everything immediately “floats.” padStart()/padEnd() solve the problem because the column width is defined by a number, not by the author’s mood.
Mistake #5: aligning an Int while forgetting to convert it to a String.
padStart and padEnd are string operations. If you try to apply them directly to a number, the compiler will reasonably say: “I get your intent, but Int does not have such a method.” The solution is simple: qty.toString().padStart(3), and similarly for other numbers.
Mistake #6: overloading one huge string with computations and formatting.
Technically, you can write a 200-character monster and feel proud that it is “all in one line.” In practice, two days later you will start asking yourself: “who wrote this and why do they hate me?” A more grown-up style is to compute values first into a val (for example, sum), then format the string. This approach makes the code readable and reduces the risk of formatting errors.
GO TO FULL VERSION