CS 346 (W23)
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Packages & Modules

Java and Kotlin have two different levels of abstraction when it comes to grouping code: packages and modules.

Packages

Packages are meant to be a collection of related classes. e.g. graphics classes. Packages are primarily a mechanism for managing dependencies between parts of an application, and encourages clear separation of concerns. They are conceptually similar to namepsaces in C++.

Use the package declaration to the top of a source file to assign a file to a namespace. Classes or modules in the same package have full visibility to each other.

Best practice is to use a reverse DNS name for a package name. e.g. com.sun.graphics if you developed the Graphics library at Sun Microsystems. Package names are always lowercase, dot-separateds with no underscores. If you want to use multiple-words, consider using camel case.

For example, in the file below, contents are contained in the ca.uwaterloo.cs346 package. The full name of the class could be qualified as ca.uwaterloo.cs346.ErrorMessage . If you were referring to it from a different package, you would need to use this fully qualified name.

package ca.uwaterloo.cs346

class ErrorMessage(val msg:String) {
  fun print() {
    println(msg)
  }
}

fun main() {
  val error = ErrorMessage("testing an error condition")
  error.print()
}

To use a class in a different namespace, we need to import the related class by using the import keyword. This applies to any class that we wish to use. In the example below, we import our ErrorMessage class into a different namespace so that we can instantiate and use it.

import ca.uwaterloo.cs346.ErrorMessage

class Logger {
  val error = ErrorMessage()
  error.printMessage()
}

Modules

Modules serve a different purpose than packages: they are intended to expose permissions for external dependencies. Using modules, you can create higher-level constructs (modules) and have higher-level permissions that describe how that module can be reused. Modules are intended to support a more rigorous separation of concerns that you can obtain with packages.

You can think of each project as being represented by a single module, and each module can contain one or more packages.

Let’s use our starting Gradle directory as the starting point. We’ll expand the source code subdirectory to include a package (net.codebot) containing a single class (Main.kt).

.
├── app
│   ├── build.gradle
│   └── src
│       └── main
│           └── java
│               └──  module-info.java
│           └── kotlin
│               └── net
│                   └── codebot
│                       └──  Main.kt
│           └── resources
│       └── test
│           ├── kotlin
│           └── resources
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle

Our top-level project is in the app/ directory.

To create a module for this project, we need to add a file named module-info.java in the src/main subdirectory. This will describe a module that contains this project, and also describes what classes will be exported and available to other projects.

Modules are particularly important when working with multi-project builds, since the module-info.java describes what classes are available to other projects.

module-info.java

module net.codebot.Main {
    exports net.codebot.Main;
    requires javafx.graphics;
}

The file opens with the name of the module. Module names have the same naming restrictions as package names, and the convention is to use the same name for the module and package that it contains.

  • exports lists the packages that should be available to other modules that wish to import this module.
  • requires lists other modules on which this module depends.

You’ll notice that the module-info.java file is located under the java/ folder, even though this is a Kotlin project! This is due to the relative newness of the Java module system. You can get around this by adding the following lines to your build.gradle file:

val compileKotlin: KotlinCompile by tasks
val compileJava: JavaCompile by tasks
compileJava.destinationDirectory.set(compileKotlin.destinationDirectory)