While I was working on my previous article about Mastering Date and Time Management in iOS with Kotlinx DateTime: A Step-by-Step Guide, I made a surprising discovery: adjusting the visibility modifier of my functions had a much larger impact than I ever anticipated. Specifically, marking certain functions as internal
dramatically reduced the size of the platform-specific libraries generated by my project. Before this, I had never really considered how much of a difference visibility modifiers could make. In this blog post, I’ll share how you can use the “internal” modifier to optimize your KMP project by minimizing dependencies and keeping your build size under control.
What Does “Internal” Mean in Kotlin?
In Kotlin, the internal
visibility modifier means that a function, class, or property is only accessible within the module in which it is defined. This means that when you mark a function as internal
, it is not accessible to other modules or libraries outside your own.
1. Limit the Spread of Dependencies
When you mark a function that relies on an external library as internal
, you ensure that this dependency is not available to other modules. This prevents other modules from becoming unnecessarily heavy by accessing functions and libraries they don’t actually need.
Example: Suppose you’re dealing with date-time operations in your KMP project. Instead of making functions that handle conversions between platform-specific and shared date-time formats public, you can mark them as internal
. This ensures that only the internal implementation has access to the external date-time library, without making other modules dependent on it.
// Common code (shared module)
import kotlinx.datetime.Instant
import kotlin.time.Duration.Companion.days
expect class PlatformDateTime
internal expect fun PlatformDateTime.fromPlatform(): Instant
internal expect fun Instant.toPlatform(): PlatformDateTime
fun doSomething(date: PlatformDateTime): PlatformDateTime {
return date
.fromPlatform()
.plus(10.days)
.toPlatform()
}
In this example, the fromPlatform
and toPlatform
functions are marked as internal
, limiting their visibility to within the module. This prevents other modules from accessing these functions and becoming dependent on the kotlinx.datetime
library used for date-time conversions.
Impact: Without using internal, the generated XCFramework header file was 547 lines long. By making all the DateTime-related functions internal, those details are hidden from the outside, and the header file is reduced to just 154 lines—a reduction of more than 3.5 times. This not only reduces the size but also keeps the API surface clean and focused.
2. Protect the Abstraction of Your Implementation
Another reason to make functions internal
is to protect the abstraction of your implementation. When you make a function public, you expose the implementation to the rest of your codebase, which can lead to unwanted dependencies and an increased risk of breaking changes in the future.
Tip: Only make functions public that are absolutely necessary for other modules to interact with, and keep the rest of the implementation internal.
internal expect fun PlatformDateTime.fromPlatform(): Instant
internal expect fun Instant.toPlatform(): PlatformDateTime
Here, marking the functions as internal
ensures that the implementation details remain hidden, allowing you to modify or update the underlying date-time logic without affecting other parts of your project.
3. Reduce the Risk of Dependency Conflicts
By keeping dependencies internal, you prevent potential conflicts where different modules require different versions of the same dependency. This can reduce the complexity of dependency management and make your project more stable.
Scenario: Imagine you have both an iOS and an Android module in your KMP project, and both use different date-time libraries. By making the functions dependent on these libraries internal, you minimize the risk of conflicts if you ever need to update the library versions.
Job Offers
4. Reduce the Size of Your Build Output
Limiting the visibility of functions can reduce the size of your compiled output. This is because internal functions and their associated dependencies are not exported to other modules, resulting in a smaller binary.
# iOS directory size using internal
XCFrameworks/release:
16.560.327 bytes (16,6 MB on disk)
# iOS directory size without using internal
XCFrameworks/release:
17.630.737 bytes (17,7 MB on disk)
Impact: The size of the generated platform-specific library before using internal was significantly larger compared to after marking relevant functions as
internal. The difference is clear when looking at the final build sizes.
5. Simplify Maintenance of Your Codebase
An additional benefit of using internal
is that it simplifies the maintenance of your codebase. You can make changes to internal functions without worrying about the impact on other modules. This makes it easier to update or replace dependencies without causing widespread issues in your project.
Conclusion
Using the internal
visibility modifier in Kotlin Multiplatform projects is an effective way to limit the scope of dependencies and keep your project manageable. By restricting the visibility of functions that depend on external libraries, you ensure a leaner, more efficient codebase with fewer dependency conflicts. It’s a small adjustment that brings significant benefits to the performance and scalability of your project.
This article is previously published on proandroiddev.com