Kotlin

Begin with one Kotlin file that stores a value, checks it, and prints an exact result. Learn how to run that file, how Kotlin knows what kind of value each name holds, and why a possibly missing value must be handled explicitly. Functions, collections, classes, variants, reusable type relationships, and Java interaction follow in dependency order. Only after those foundations are working do suspended tasks, streams of values, data encoding, tests, Android interfaces, services, and shared-platform design appear. Every platform-specific claim keeps its required device, runtime, and release evidence visible.

Course details and reading size
Tutorial
Reading comfortAdjust lesson text without changing code or interface size.

Toolchain, Gradle, and project setup

Objective Build, test, run, and package a reproducible Kotlin project.

Core explanation

Kotlin compilers target JVM, Android, JavaScript, Native, and Wasm. Gradle defines plugins, source sets, dependencies, toolchains, tests, and packaging. Use the wrapper, version catalogs or locked versions, explicit repositories, and CI verification. Package names and source sets form module boundaries. Kotlin language, Gradle plugin, serialization plugin, Android plugin, JDK, and target platform versions form one compatibility matrix. Commit the wrapper, lock dependency resolution, restrict repositories, select a JVM toolchain, and make compiler warnings and explicit API mode deliberate. Understand source sets, generated code, task inputs, configuration cache, and build variants before optimizing Gradle. CI should build from a clean cache as well as a warm one, test the packaged artifact, inspect dependency provenance, and publish the same immutable output that passed verification.

Apply toolchain, gradle, and project setup with explicit types, ownership, cancellation, and testable boundaries.

Build a reproducible Kotlin toolchain before writing features

A Kotlin project is executed by a chain of tools, not by the source file alone. The Gradle wrapper selects a Gradle distribution, plugins select Kotlin and platform compilers, a Java toolchain supplies the JDK used for JVM compilation, repositories supply dependencies, and tasks transform source sets into test and production artifacts. Record each version explicitly. Commit the wrapper scripts and wrapper metadata, declare the Java toolchain, pin plugin versions, and avoid relying on whichever JDK happens to be first on a developer machine. Run the wrapper from a clean checkout and treat a successful IDE run as convenience evidence rather than proof that the documented build is reproducible.

Start a project by identifying its target: JVM command line, server, Android, JavaScript, Native, or Multiplatform. That decision changes available plugins, source sets, test engines, packaging, and runtime assumptions. A minimal JVM application normally separates production code under src/main/kotlin from tests under src/test/kotlin. Multiplatform adds common and target-specific source sets with declared dependency relationships. Do not copy platform APIs into common code merely because an IDE can currently resolve them. The build model should make the supported targets and their boundaries visible to both the compiler and a new maintainer.

Read Gradle as an executable dependency and task model

A build script configures projects and tasks. Plugins contribute task types, conventions, dependency configurations, and generated sources. Dependencies belong to scopes with different visibility: implementation hides a dependency from downstream compile classpaths, api intentionally exposes it, runtimeOnly supplies it only for execution, and testImplementation limits it to tests. Choose the narrowest truthful scope. An accidental api dependency becomes part of a published contract, while a missing runtime dependency can compile successfully and fail only when launched. Inspect the dependency graph instead of resolving conflicts by repeatedly changing versions until the error disappears.

Gradle has a configuration phase that constructs the task graph and an execution phase that runs selected work. Code that reads files, calls networks, or computes changing values during configuration makes every command slow and can invalidate the configuration cache. Register tasks lazily, declare inputs and outputs, and defer work to task actions or providers. A task is up to date only when Gradle can compare declared inputs, implementation, and outputs. Hidden environment inputs produce stale artifacts. If a build uses a release flag, schema file, generated catalog, or external tool version, model it as an input so cache reuse remains correct rather than merely fast.

Control dependency resolution and supply-chain inputs

Repository order and dynamic versions affect which bytes enter a build. Prefer a small approved repository list, exact versions or a governed version catalog, dependency locking, and checksum verification. Avoid version selectors such as latest.release in a production project because two clean builds can resolve different artifacts. A lock file is not a guarantee that an artifact is trustworthy, but it makes the selected graph reviewable and repeatable. When updating, change a bounded group, inspect release notes and resolved differences, run compatibility tests, and retain the previous lock state so rollback is an intentional artifact decision.

Plugins are executable build dependencies and deserve the same scrutiny as libraries. Pin their versions, remove unused repositories, and understand what generated code or packaging behavior they add. Credentials belong in protected environment or Gradle credential providers, never in source, wrapper properties, generated reports, or command lines captured by process listings. CI should expose only the credentials required by the selected task and should not grant pull-request builds publication authority. Generate an inventory of resolved components for releases, preserve licenses and provenance where required, and scan the actual resolved graph rather than a handwritten list.

Separate compiler policy from editor preference

The compiler establishes language and API behavior. Configure the language version, API version, JVM target, warnings policy, opt-ins, and explicit API mode where a published library benefits from declared visibility and types. Keep those settings in the build so command-line and IDE compilation agree. A warning promoted to an error can protect a team only if its compiler version is pinned; otherwise a tool update can introduce an unplanned build break. Opting into an experimental API acknowledges that its contract may change. Isolate that usage behind a narrow adapter and test it rather than scattering opt-in annotations across domain code.

Formatting and static analysis answer different questions. A formatter removes style debate, while a linter or analyzer can report complexity, unused declarations, unsafe patterns, or architecture rules. Pin these tools and run them through Gradle so local and CI behavior match. Generated sources should be clearly separated and normally excluded from hand-authored style rules. Suppression requires a local reason and the narrowest possible scope. Do not measure quality by a zero-warning screenshot from one IDE; preserve machine-readable check results, make failures reproducible with one wrapper command, and verify that the packaged artifact came from the same checked source revision.

Design fast feedback without weakening clean-build evidence

Incremental compilation and caches shorten feedback by reusing outputs whose inputs have not changed. They are optimizations, not separate build semantics. A warm developer build should agree with a clean build. Test both paths because undeclared inputs can make warm builds pass while clean CI fails, and stale generated files can make local results appear correct. Keep build directories disposable, generated source ownership explicit, and tasks deterministic where practical. Do not solve a stale-output defect by teaching developers to delete every cache; locate the missing input, output, or dependency relationship so the build system can make a correct reuse decision.

Structure the feedback loop by cost. Formatting, compilation, and focused unit tests should run quickly. Broader integration tests, platform packaging, dependency review, and browser or device journeys can follow while still gating publication. Parallel execution must respect finite memory and external service capacity. More workers can increase contention and make failures less reproducible. Record task timings on representative machines, investigate regressions, and optimize the slow owner rather than disabling checks. A useful build gives a learner a small command for the current edit and a complete command that proves the release candidate from clean inputs.

Laboratory: prove the project works outside the IDE

Create a new directory from the project template, stop the IDE, and use only the committed wrapper. Print the Java and Gradle versions, run a clean test, build the distribution, and execute the packaged entry point rather than an IDE-specific run configuration. Save the commands and expected observable output. Inspect the artifact contents and verify that runtime dependencies are present through the selected packaging model. Then change one source file and compare the incremental task report with a second unchanged run. The changed source should invalidate the necessary tasks; the unchanged run should reuse eligible outputs without skipping required verification.

Introduce controlled failures one at a time: request an unavailable toolchain, remove a required runtime dependency, add an undeclared file input to a custom task, and replace an exact dependency with a dynamic selector. Predict whether failure occurs during configuration, dependency resolution, compilation, packaging, or launch. Capture the earliest useful message and repair the owning build contract. Finally clone or copy only tracked files into a fresh location and repeat the documented journey. The chapter is complete when another learner can build, test, package, and run the same artifact without inheriting your IDE state, global Gradle configuration, cached generated files, or undocumented JDK selection.

CURRICULUM CONTEXTRelated courses and the course concept model