Table of Contents
Kotlin Deep Dive
How to Count Lines in a File Using Kotlin (And the useLines Sequence Trap Nobody Documents)
Count lines in a file using Kotlin — File.readLines, useLines, BufferedReader, and Coroutines Flow. Covers the useLines Sequence escape trap, OOM risks, and Android/Spring Boot patterns with benchmarks.
Here is a Kotlin snippet that looks harmless:
fun getLines(path: String): Sequence<String> {
return File(path).useLines { it }
}
val count = getLines("data.csv").count()
It compiles. It has no warning. It fails at runtime with IOException: Stream closed.
Why? Because useLines closes the reader after the lambda completes. The Sequence escaped, so it is now trying to read from a closed stream.
This guide covers the practical kotlin count lines choices:
readLines().sizefor small files only.kotlin useLines count linesfor the normal streaming answer.bufferedReader(...).use { lineSequence() }for lower-level control.- byte scanning for unknown encodings and maximum throughput.
kotlin coroutines count linespatterns for Spring Boot and Android.kotlin android count linesexamples forcontent://URIs.
If you searched for kotlin count lines in file, the short answer is: keep the whole count inside useLines, and use a Long accumulator if the file can be truly huge.
That is also the shortest honest answer to count lines kotlin work in Android importers, Spring Boot jobs, and one-off JVM utilities.
Quick Method Guide
| I want to... | Use this | Main warning |
|---|---|---|
| Count a small file | File(path).readLines().size | loads all lines into memory |
| Stream a normal text file | File(path).useLines { lines -> lines.count() } | returns Int and must stay inside the block |
| Stream a huge text file safely | File(path).useLines { lines -> lines.fold(0L) { acc, _ -> acc + 1 } } | a little more verbose |
| Control buffer size | bufferedReader(bufferSize = ...).use { ... } | the Sequence must stay inside use |
| Count arbitrary bytes | byte scan with inputStream() | you lose decoded text semantics |
| Count in coroutine code | withContext(Dispatchers.IO) { ... } | do not return open resources |
Count Android content:// files | contentResolver.openInputStream(uri) | can return null or throw |
For most kotlin count lines in file code, the practical default is:
File(path).useLines { lines ->
lines.fold(0L) { acc, _ -> acc + 1 }
}
That solves three problems at once:
- it streams instead of reading the whole file
- it does not let the sequence escape
- it does not rely on
Sequence.count(), which returnsInt
For kotlin count lines in file tasks where the input size is unknown, this is a safer default than readLines().size.
Method 1: readLines().size - Simple but Not for Large Files
The shortest answer is:
import java.io.File
val count = File("data.txt").readLines().size
Path-based API:
import kotlin.io.path.Path
import kotlin.io.path.readLines
val count = Path("data.txt").readLines().size
With basic validation:
import java.io.File
fun countLinesSmallFile(filePath: String): Int {
val file = File(filePath)
require(file.isFile) { "File not found: $filePath" }
return file.readLines().size
}
The Kotlin API docs for Path.readLines() are explicit: it is not recommended for huge files, and you should use Path.forEachLine or Path.useLines for large or unknown-size files.
Why this matters:
readLines()builds aListof every line- every line becomes a JVM
String - the list itself also costs memory
| File size | Use readLines()? | Why |
|---|---|---|
| Under 50MB | usually yes | simplest code |
| 50MB to 200MB | maybe | heap pressure grows quickly |
| Over 200MB | avoid | read-all memory becomes expensive |
| Around 1GB | no | likely OOM or heavy GC |
The right way to think about kotlin readLines vs useLines is simple: one materializes the entire file, the other keeps line generation lazy.
That is the first decision every kotlin count lines in file implementation should make before worrying about benchmarks.
Method 2: useLines - The Recommended Streaming Approach
The official streaming pattern is:
import java.io.File
val count = File("data.txt").useLines { lines ->
lines.count()
}
That is the standard kotlin useLines count lines answer for normal files.
Path-based API:
import kotlin.io.path.Path
import kotlin.io.path.useLines
val count = Path("data.txt").useLines { lines ->
lines.count()
}
Count non-empty lines:
val nonEmpty = File("data.txt").useLines { lines ->
lines.count { it.isNotBlank() }
}
How useLines actually works
The docs say useLines gives your block a Sequence of lines and closes the reader once processing is complete.
That means:
- the
Sequenceis lazy - the reader stays open only during the block
- after the block returns, the stream is closed
This is why kotlin useLines count lines is safe only when the terminal operation stays inside the lambda.
That one rule explains most kotlin useLines IOException stream closed failures in production code.
The Sequence escape trap
This is the Kotlin-specific runtime trap.
import java.io.File
fun getLinesSequence(path: String): Sequence<String> {
return File(path).useLines { it }
}
val count = getLinesSequence("data.txt").count()
// IOException: Stream closed
The same bug appears in slightly different forms:
var escaped: Sequence<String>? = null
File("data.txt").useLines { lines ->
escaped = lines
}
val count = escaped!!.count()
// IOException: Stream closed
And in coroutine code:
suspend fun dangerous(path: String): Int {
val lines = withContext(Dispatchers.IO) {
File(path).useLines { it }
}
return lines.count()
}
The Kotlin coroutines withContext docs explicitly warn about returning closeable resources. A lazily backed Sequence from useLines has the same lifetime problem.
The Int count limitation
The Kotlin Sequence.count() docs say it returns Int.
So this:
val count: Int = File("data.txt").useLines { lines ->
lines.count()
}
is fine for ordinary files, but it is not ideal if the file can exceed 2.1 billion lines.
Use a Long accumulator instead:
val count: Long = File("data.txt").useLines { lines ->
lines.fold(0L) { acc, _ -> acc + 1 }
}
This is the safer kotlin useLines count lines pattern for very large datasets.
If you remember one phrase from this section, remember this: kotlin useLines count lines is correct only when the terminal operation stays inside the resource block.
Charset handling
Default text decoding is UTF-8:
val count = File("data.txt").useLines { lines ->
lines.fold(0L) { acc, _ -> acc + 1 }
}
Specify another charset when the file is known not to be UTF-8:
import java.nio.charset.Charset
val count = File("legacy.csv").useLines(Charset.forName("ISO-8859-1")) { lines ->
lines.fold(0L) { acc, _ -> acc + 1 }
}
If the encoding is unknown or bytes may be mixed with binary data, stop using text APIs and count newline bytes directly. That is the Kotlin equivalent of a binary-safe line counter.
Method 3: bufferedReader() and lineSequence() - More Control, Same Lifetime Rule
If you need buffer-size control or direct access to the reader, drop down one layer:
import java.io.File
val count = File("data.txt")
.bufferedReader(bufferSize = 1024 * 1024)
.use { reader ->
reader.lineSequence().fold(0L) { acc, _ -> acc + 1 }
}
The Kotlin docs for lineSequence() say:
- it returns a
Sequenceof lines - the caller must close the underlying
BufferedReader - the returned sequence can be iterated only once
That leads to the exact same escape trap:
import java.io.File
val sequence = File("data.txt").bufferedReader().use { reader ->
reader.lineSequence()
}
sequence.count()
// IOException: Stream closed
So the rule is identical:
kotlin useLines count lines: consume insideuseLineslineSequence(): consume insideuse
This is also where kotlin readLines vs useLines becomes more concrete. readLines() gives you a materialized list. lineSequence() gives you a lazy, one-shot sequence that lives only as long as the reader.
If you are building a reusable kotlin count lines in file helper, this lifetime rule matters more than shaving a few milliseconds off one benchmark.
Why use bufferedReader at all?
Three reasons:
- you can set
bufferSize - you can mix line counting with other reader operations
- it maps cleanly to Java interop, which matters in Spring Boot codebases
The Kotlin API exposes bufferedReader(bufferSize = ...), so a larger buffer is a legitimate tuning knob when kotlin count lines large file performance matters.
Method 4: Byte Stream Scanning - Maximum Performance and Any Encoding
If you only need the number, not decoded text, count bytes:
import java.io.File
fun countLinesFast(filePath: String): Long {
val file = File(filePath)
require(file.isFile) { "File not found: $filePath" }
var count = 0L
var sawData = false
var lastByte: Byte = '\n'.code.toByte()
file.inputStream().buffered().use { stream ->
val buffer = ByteArray(64 * 1024)
var bytesRead: Int
while (stream.read(buffer).also { bytesRead = it } != -1) {
if (bytesRead == 0) continue
sawData = true
for (i in 0 until bytesRead) {
if (buffer[i] == '\n'.code.toByte()) {
count++
}
}
lastByte = buffer[bytesRead - 1]
}
}
if (sawData && lastByte != '\n'.code.toByte()) {
count++
}
return count
}
This is the best answer when:
- the file may contain arbitrary bytes
- you want the fastest pure-Kotlin or pure-JVM counting path
- you do not need line contents
It is also the most robust answer for kotlin count lines large file cases where text decoding is unnecessary.
When people search for kotlin count lines large file, this byte-scanning version is usually the performance ceiling on the JVM side.
Method 5: Coroutines and Flow - Async Integration Without Fake Non-Blocking Claims
Kotlin coroutines do not make blocking file I/O non-blocking. They let you move blocking work off the caller context.
withContext(Dispatchers.IO) for one-shot counts
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun countLinesAsync(filePath: String): Long =
withContext(Dispatchers.IO) {
File(filePath).useLines { lines ->
lines.fold(0L) { acc, _ -> acc + 1 }
}
}
That is the practical kotlin coroutines count lines pattern for Spring Boot handlers, Android view models, and background services.
What not to do:
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun dangerousCount(filePath: String): Int {
val sequence = withContext(Dispatchers.IO) {
File(filePath).useLines { it }
}
return sequence.count()
}
This is the coroutine version of kotlin useLines IOException stream closed. The resource is gone before the sequence is consumed.
Flow with progress reporting
There is one subtle trap here: emit() is suspend, but use and useLines take non-suspending lambdas. So a progress-reporting Flow should manage the reader manually:
import java.io.File
import java.io.BufferedReader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
data class CountProgress(
val processed: Long,
val complete: Boolean,
val total: Long = -1
)
fun countLinesWithProgress(filePath: String): Flow<CountProgress> = flow {
val reader: BufferedReader = File(filePath).bufferedReader()
var count = 0L
try {
while (true) {
val line = reader.readLine() ?: break
count++
if (count % 10_000L == 0L) {
emit(CountProgress(processed = count, complete = false))
}
}
emit(CountProgress(processed = count, complete = true, total = count))
} finally {
reader.close()
}
}.flowOn(Dispatchers.IO)
The official flowOn docs say it changes the context of upstream operators only. That is exactly what you want here: the file read and counting happen on Dispatchers.IO, while downstream collection can stay on the UI or request context.
This is a clean answer for kotlin coroutines count lines when you need progress updates instead of a single number.
It also avoids the classic kotlin useLines IOException stream closed bug because the reader lifecycle is kept entirely inside the upstream flow.
Part 6: Counting Lines on Android from content:// URIs
Android file pickers usually give you a content:// URI, not a filesystem path.
The Android ContentResolver docs say openInputStream(uri) opens a stream for content, android.resource, and file URIs. That is the correct entry point.
Known-text version:
import android.content.ContentResolver
import android.net.Uri
import java.io.BufferedReader
import java.io.InputStreamReader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun countLinesFromUri(
contentResolver: ContentResolver,
uri: Uri
): Long = withContext(Dispatchers.IO) {
val input = contentResolver.openInputStream(uri)
?: throw IllegalArgumentException("Cannot open URI: $uri")
input.use { stream ->
BufferedReader(InputStreamReader(stream)).use { reader ->
reader.lineSequence().fold(0L) { acc, _ -> acc + 1 }
}
}
}
Any-encoding version:
import android.content.ContentResolver
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun countLinesFromUriFast(
contentResolver: ContentResolver,
uri: Uri
): Long = withContext(Dispatchers.IO) {
val input = contentResolver.openInputStream(uri)
?: throw IllegalArgumentException("Cannot open URI: $uri")
input.use { stream ->
val buffer = ByteArray(64 * 1024)
var count = 0L
var sawData = false
var lastByte: Byte = '\n'.code.toByte()
var bytesRead: Int
while (stream.read(buffer).also { bytesRead = it } != -1) {
if (bytesRead == 0) continue
sawData = true
for (i in 0 until bytesRead) {
if (buffer[i] == '\n'.code.toByte()) {
count++
}
}
lastByte = buffer[bytesRead - 1]
}
if (sawData && lastByte != '\n'.code.toByte()) count + 1 else count
}
}
That is the practical kotlin android count lines answer when users choose local or cloud-backed documents through the system picker.
viewModelScope and lifecycleScope
Android's lifecycle docs say:
viewModelScopeis tied to theViewModellifecycleScopeis tied to theLifecycleOwner
Example:
class FileViewModel : ViewModel() {
fun count(uri: Uri, resolver: ContentResolver) {
viewModelScope.launch {
val count = countLinesFromUriFast(resolver, uri)
// update state here
}
}
}
Use viewLifecycleOwner.lifecycleScope.launch when the work belongs to a Fragment view lifecycle rather than the ViewModel.
Benchmark: Representative Comparison
These figures are representative for Kotlin 2.0, JVM 21, Linux, SSD, and a 500MB text file with roughly 5 million lines. They describe the shape of the trade-off, not a locally reproduced benchmark on this machine.
| Method | Time | Peak memory | Safe on large files | Notes |
|---|---|---|---|---|
readLines().size | about 4.1s | about 1GB | no | read-all memory cost |
useLines { count() } | about 2.3s | about 8MB | mostly | still returns Int |
useLines { fold(0L) } | about 2.4s | about 8MB | yes | safer huge-file count |
bufferedReader(1MB).use { lineSequence().fold(0L) } | about 1.8s | about 1MB | yes | more control |
| byte stream scan | about 0.7s | about 64KB | yes | fastest and encoding-agnostic |
withContext(IO) wrapper | about 2.4s | about 8MB | yes | same core work, safer coroutine integration |
The practical conclusion:
- default text count:
useLineswith aLongfold - performance-focused text count: tuned
bufferedReader - any-encoding and maximum speed: byte scan
- coroutine integration:
withContext(Dispatchers.IO)
Part 8: A Production-Ready Kotlin Line Counter
import java.io.File
import java.nio.charset.Charset
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
object LineCounter {
private const val BUFFER_SIZE = 1024 * 1024
private const val SMALL_FILE_THRESHOLD = 50L * 1024 * 1024
fun count(
filePath: String,
skipEmpty: Boolean = false,
charset: Charset = Charsets.UTF_8
): Long {
val file = File(filePath)
require(file.isFile) { "File not found: $filePath" }
if (file.length() < SMALL_FILE_THRESHOLD) {
val lines = file.readLines(charset)
return if (skipEmpty) {
lines.count { it.isNotBlank() }.toLong()
} else {
lines.size.toLong()
}
}
return file
.bufferedReader(charset = charset, bufferSize = BUFFER_SIZE)
.use { reader ->
if (skipEmpty) {
reader.lineSequence().fold(0L) { acc, line ->
if (line.isNotBlank()) acc + 1 else acc
}
} else {
reader.lineSequence().fold(0L) { acc, _ -> acc + 1 }
}
}
}
suspend fun countAsync(
filePath: String,
skipEmpty: Boolean = false,
charset: Charset = Charsets.UTF_8
): Long = withContext(Dispatchers.IO) {
count(filePath, skipEmpty, charset)
}
fun countFast(filePath: String): Long {
val file = File(filePath)
require(file.isFile) { "File not found: $filePath" }
var count = 0L
var sawData = false
var lastByte: Byte = '\n'.code.toByte()
file.inputStream().buffered(BUFFER_SIZE).use { stream ->
val buffer = ByteArray(BUFFER_SIZE)
var bytesRead: Int
while (stream.read(buffer).also { bytesRead = it } != -1) {
if (bytesRead == 0) continue
sawData = true
for (i in 0 until bytesRead) {
if (buffer[i] == '\n'.code.toByte()) {
count++
}
}
lastByte = buffer[bytesRead - 1]
}
}
if (sawData && lastByte != '\n'.code.toByte()) {
count++
}
return count
}
}
Usage:
val total = LineCounter.count("data.csv")
val nonEmpty = LineCounter.count("data.csv", skipEmpty = true)
val totalAsync = LineCounter.countAsync("data.csv")
val fast = LineCounter.countFast("binary-log.bin")
Note the design choice:
- text APIs return
Longby folding, not by callingSequence.count() - the fast path works on bytes and any encoding
- async counting only moves the blocking work to
Dispatchers.IO; it does not magically become non-blocking I/O
Kotlin Version Compatibility
| Feature | Version support | Notes |
|---|---|---|
File.readLines() | Kotlin 1.0+ | read-all list of lines |
File.useLines() | Kotlin 1.0+ | streaming block over a lazy sequence |
BufferedReader.lineSequence() | Kotlin 1.0+ | caller must close the reader |
Path.readLines() | Kotlin 1.5+ | docs warn against huge files |
Path.useLines() | Kotlin 1.5+ | JVM Path extensions |
bufferedReader(bufferSize = ...) | Kotlin 1.0+ | custom reader buffer size |
Coroutines withContext | kotlinx.coroutines 1.0+ | dispatcher shift for blocking work |
Flow and flowOn | kotlinx.coroutines 1.3+ | upstream context control |
viewModelScope and lifecycleScope | AndroidX lifecycle KTX | Android lifecycle-bound scopes |
Production Checklist
- Use
useLines, notreadLines, for unknown or large files. - Keep the terminal operation inside the
useLinesblock. - Do not return a
SequencefromuseLinesoruse. - Remember that
Sequence.count()returnsInt. - Use
fold(0L)or a manualLongcounter if line count can be huge. - Wrap blocking file I/O in
withContext(Dispatchers.IO)in coroutine code. - Use
flowOn(Dispatchers.IO)for upstreamFlowwork, but do not pretend it makes file I/O non-blocking. - Use
ContentResolver.openInputStream(uri)for Androidcontent://files. - Use byte scanning when encoding is unknown or text decoding is unnecessary.
Sources Checked
- Kotlin
Path.readLines()docs, including the huge-file warning: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.io.path/read-lines.html - Kotlin
useLines()docs: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.io/use-lines.html - Kotlin
BufferedReader.lineSequence()docs: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.io/line-sequence.html - Kotlin
bufferedReader()docs: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.io/buffered-reader.html - Kotlin
Sequence.count()docs, which show the return type isInt: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.sequences/count.html - kotlinx.coroutines
withContextdocs, including the note about returning closeable resources: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html - kotlinx.coroutines
flowOndocs: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow-on.html - Android
ContentResolver.openInputStream()docs: https://developer.android.com/reference/android/content/ContentResolver - Android lifecycle coroutine scope docs for
viewModelScopeandlifecycleScope: https://developer.android.com/topic/libraries/architecture/views/coroutines-views and https://developer.android.com/reference/kotlin/androidx/lifecycle/package-summary
Related Guides and Tools
- Java line counting
- Python line counting
- C# async line counting
- Go line counting
- Scala line counting
- Swift line counting
- Line Counter tool
Building a CSV importer for Android?
Check the line count before you start the import job. Paste the file into the Line Counter. No Kotlin setup, no Sequence traps, no IOException surprises.
Frequently Asked Questions
How do I count lines in a file in Kotlin?
For most cases, use File(path).useLines { lines -> lines.fold(0L) { acc, _ -> acc + 1 } }. It streams the file and avoids the Int limitation of Sequence.count().
What is the difference between readLines and useLines in Kotlin?
readLines loads the entire file into a List, while useLines passes a lazy Sequence into a block and closes the reader when that block finishes.
Why does useLines throw IOException: Stream closed?
Because the Sequence escaped the useLines block. The reader is closed after the block completes, so later iteration touches a closed stream.
How do I count lines in a large file in Kotlin?
Use useLines or bufferedReader with lineSequence for normal text files, or scan bytes directly if encoding is unknown or performance is critical.
How do I count lines asynchronously in Kotlin?
Wrap blocking file reads in withContext(Dispatchers.IO), and finish counting before returning from that block.
How do I count lines with Coroutines Flow?
Use a flow builder with manual reader lifecycle management, then apply flowOn(Dispatchers.IO) to move upstream work to the IO dispatcher.
How do I count lines in Android from a URI?
Use ContentResolver.openInputStream(uri), then count lines from the returned stream with a BufferedReader or a byte scanner.
How do I count non-empty lines in Kotlin?
Use a predicate such as line.isNotBlank() inside the sequence fold or count call.
Related Guides
14 min read
How to Count Lines in a File Using Swift (And the autoreleasepool Trap That Crashes Your App)
Count lines in a file using Swift — String(contentsOfFile), FileHandle, and async/await. Covers the autoreleasepool memory trap, iOS memory warnings, and SwiftUI progress patterns with benchmarks.
20 min read
How to Count Lines in Python: 7 Methods, Benchmarked and Battle-Tested
Count lines in Python strings, text files, large files, and directories. Includes real performance benchmarks, empty file handling, splitlines vs split, and production-ready functions.
15 min read
How to Count Lines in a File Using C# (And the Int32 Overflow Trap Nobody Warns You About)
Count lines in a file using C# — File.ReadAllLines, File.ReadLines, StreamReader, and async methods. Includes the Int32 overflow trap, GC pressure benchmarks, and .NET 6+ best practices.
14 min read
How to Count Lines in a File Using Go (And the bufio.Scanner Trap You Need to Know)
Count lines in a file using Go — bufio.Scanner, bytes.Count, and manual byte scanning. Includes the critical 64KB buffer limit fix, benchmark results, and concurrent file processing.