Gradle, a powerful build automation tool, offers a plethora of features that help streamline the development and deployment process. One of these features issystem properties, which allow you to pass configuration values to your Gradle build scripts from the command line or other external sources. In this blog, we’ll delve into the concept of system properties in Gradle, understand their significance, and provide practical examples to ensure a crystal-clear understanding.
Understanding System Properties
System properties are a way to provide external configuration to your Gradle build scripts. They enable you to pass key-value pairs to your build scripts when invoking Gradle tasks. These properties can be utilized within the build script to modify its behavior, adapt to different environments, or customize the build process according to your needs.
The syntax for passing system properties to a Gradle task is as follows:
Here, <taskName> represents the name of the task you want to execute, <propertyName> is the name of the property you want to set, and <propertyValue> is the value you want to assign to the property.
The -P flag is used to pass project properties to a Gradle task when invoking it from the command line.
Kotlin
gradle build -Penvironment=staging
Here, the command is invoking the build task, and it’s passing a project property named environment with the value staging. Inside your build.gradle script, you can access this property’s value using project.property('environment').
So, What are system properties in Gradle?
System properties are key-value pairs that can be used to control the behavior of Gradle. They can be set in a variety of ways, including:
On the command line using the -D option
In a gradle.properties file
In an environment variable
When Gradle starts, it will look for system properties in the following order:
The command line
The gradle.properties file in the user’s home directory
The gradle.properties file in the current project directory
Environment variables
If a system property is defined in multiple places, the value from the first place (command line) it is defined will be used.
How to set system properties in Gradle
There are three ways to set system properties in Gradle:
Using the -D option
You can set system properties on the command line using the -D option. For example, to set the db.url system property to localhost:3306, you would run the following command:
Kotlin
gradle -Ddb.url=localhost:3306
Using a gradle.properties file
You can also set system properties in a gradle.properties file. This file is located in the user’s home directory. To set the db.url system property in a gradle.properties file, you would add the following line to the file:
Kotlin
db.url=localhost:3306
Using an environment variable
You can also set system properties using environment variables. To set the db.url system property using an environment variable, you would set the DB_URLenvironment variable to localhost:3306.
How to access system properties in Gradle
Once you have set a system property, you can access it in Gradle using the System.getProperty() method. For example, to get the value of the db.url system property, you would use the following code:
Kotlin
String dbUrl = System.getProperty("db.url");
Difference between project properties and system properties in Gradle
Project properties and system properties are both key-value pairs that can be used to control the behavior of Gradle. However, there are some important differences between the two:
Project properties are specific to a particular project, while system properties are global and can be used by all projects.
Project properties are defined in the gradle.properties file in the project directory, while system properties can be defined in a variety of ways, including on the command line, in an environment variable, or in a gradle.properties file in the user’s home directory.
Project properties are accessed using the project.getProperty() method, while system properties are accessed using the System.getProperty() method.
Use Cases for System Properties
System properties can be immensely valuable in various scenarios:
Environment-Specific Configurations:You might have different configurations for development, testing, and production environments. System properties allow you to adjust your build process accordingly.
Build Customization: Depending on the requirements of a particular build, you can tweak various parameters through system properties, such as enabling/disabling certain features or modules.
Versioning:You can pass the version number as a system property to ensure that the build uses the correct version throughout the process.
Integration with External Tools: If your build process requires integration with external tools or services, you can provide the necessary connection details or credentials as system properties.
Implementation with Examples
Let’s explore system properties in action with some examples:
Example 1: Environment-Specific URL
Imagine you’re working on a project where the backend API URL differs for different environments. You can use a system property to specify the API URL when invoking the build.
In the Kotlin DSL, the register function is used to define tasks, and the doLast block is used to specify the task’s action. The project.findProperty function is used to retrieve the value of a project property, and the as String? cast is used to ensure that the property value is treated as a nullable string. The Elvis operator (?:) is used to provide a default value if the property is not set.
In the above examples, you might have noticed the use of the project.property or project.findProperty method with a null coalescing operator (?:) to provide default values if the property isn’t passed. This is important to ensure that your build script doesn’t break when a property isn’t provided.
Conclusion
System properties in Gradle offer a versatile mechanism to inject external configuration into your build scripts, promoting flexibility and reusability. By utilizing system properties, you can easily adapt your build process to various environments, customize build parameters, and integrate with external services without modifying the actual build script. This results in a more efficient and maintainable build automation process for your projects.
Kotlin, an impressive and modern programming language, has rapidly gained popularity in the developer community since its release. One of its standout features is the ability to create Domain-Specific Languages (DSLs), which are specialized programming languages tailored to solve specific problems within particular domains. In this blog, we will delve into Kotlin DSLs in detail, exploring what they are, how they work, and why they are so beneficial. By the end, you’ll be equipped with a solid understanding of Kotlin DSLs and how to leverage them effectively in your projects.
At its core, the focus here is on designing expressive and idiomatic APIs using domain-specific languages (DSLs) in Kotlin. We will highlight the differences between traditional APIs and DSL-style APIs, emphasizing the advantages of using DSLs. Kotlin’s DSL design relies on two important language features:
Lambdas with Receivers:Lambdas with receivers enable you to create a DSL structure by changing the name-resolution rules within code blocks. This allows for a more natural and concise syntax when working with DSLs, making the code more readable and expressive.
Invoke Convention:The invoke convention is a powerful feature introduced in Kotlin. It enhances the flexibility of combining lambdas and property assignments in DSL code. The invoke convention allows you to call an object as if it were a function, making the code more intuitive and fluent.
Throughout the article, we will explore these language features in detail, explaining how they contribute to creating powerful and user-friendly DSLs. Moreover, we will demonstrate practical use cases of DSL-style APIs in various domains, including:
Database Access:Simplify database interactions by crafting a DSL for database queries and transactions.
HTML Generation:Build dynamic HTML content using a DSL to create templates and components.
Testing: Create DSLs for writing concise and expressive test cases.
Build Scripts: Design build scripts with a DSL that enhances readability and maintainability.
By the end of this article, you will have a strong grasp of Kotlin DSLs and be ready to leverage them in your projects. The combination of lambdas with receivers and the invoke convention will provide you with a powerful toolkit to design DSLs that are both intuitive and efficient. Building expressive and readable APIs in Kotlin will become second nature to you, enabling you to tackle various tasks with ease.
So, let’s start the journey of Kotlin DSLs, a thrilling adventure that will unlock the power of expressive APIs!
From APIs to DSLs
Before we delve into DSLs (Domain-Specific Languages), let’s first understand the problem we aim to solve. Our ultimate goal is to create code that is easy to read and maintain. To achieve this, we must not only focus on individual classes but also consider how these classes interact with one another, which means examining their APIs (Application Programming Interfaces).
The API of a class is like a contract that defines how other classes can communicate and work with it. Creating well-designed APIs is crucial not only for library authors but for every developer. Just like a library provides an interface for its usage, each class within an application offers ways for other classes to interact with it.
Ensuring that these interactions are easy to understand and expressed clearly is vital for maintaining a project over time. By prioritizing good API design, we can contribute to the overall readability and maintainability of our codebase.
Kotlin has various features that help create clean APIs for classes. But what does it mean for an API to be clean? There are two main aspects to it:
Clarity: A clean API should make it easy for readers to understand what’s happening in the code. This is achieved through well-chosen names and concepts, which is crucial in any programming language.
Conciseness:The code should look clean and straightforward, avoiding unnecessary syntax and boilerplate. This blog’s primary focus is on achieving this aspect of cleanliness. In fact, a clean API can even appear as if it’s a built-in feature of the language itself.
Kotlin provides several features that empower developers to design clean APIs. Some examples of these features include extension functions, infix calls (which enable a more natural and readable syntax for certain operations), shortcuts in lambda syntax (making lambda expressions more concise), and operator overloading (allowing operators to be used with custom types).
The below table shows how these features help reduce the amount of syntactic noise in the code.
Kotlin support for clean syntax
By leveraging these features effectively, developers can create APIs that are not only clear but also elegant and concise.
In this article, we will explore Kotlin’s support for constructing DSLs (Domain-Specific Languages). DSLs in Kotlin take advantage of the clean-syntax features we discussed earlier and go a step further by allowing you to create structured code using multiple method calls. This makes DSLs even more expressive and enjoyable to work with compared to APIs constructed solely with individual method calls.
An essential point to note is that Kotlin DSLs are fully statically typed, which means all the benefits of static typing, like catching errors at compile-time and improved IDE support, still apply when you use DSL patterns for your APIs.
To give you a quick preview of what Kotlin DSLs can achieve, consider these examples:
To get the previous day, you can write:
Kotlin
val yesterday = 1.days.ago
2. For generating an HTML table, you can use a function like this:
Throughout the article, we will explore how these examples are built and understand the concepts behind DSLs. But before we dive into the details, let’s first explore what DSLs actually are in programming.
The concept of domain-specific languages
The concept of Domain-Specific Languages (DSLs) has been around for a long time, dating back almost as far as the idea of programming languages itself. When discussing DSLs, we distinguish between two types of languages:
General-Purpose Programming Language:This type of language is designed to have a comprehensive set of capabilities, allowing it to solve virtually any problem that can be addressed with a computer. Examples of general-purpose programming languages include Java, Python, and C++.
Domain-Specific Language:In contrast, a DSL is tailored to focus on a specific task or domain. It deliberately omits functionality that is irrelevant to that particular domain, which makes it more efficient and concise for tasks within its specialized scope.
Two well-known examples of DSLs are SQL (Structured Query Language) and regular expressions. SQL is excellent for working with databases, while regular expressions are designed for manipulating text strings. However, these DSLs are not suitable for building entire applications; theyexcel at their specific tasks but are limited when it comes to broader programming needs.
The strength of DSLs lies in their ability to effectively accomplish their objectives by reducing the set of available functionality. For instance, when writing SQL statements, you don’t start by declaring classes or functions. Instead, you begin with a keyword that specifies the type of operation you want to perform, and each operation has its own distinct syntax and set of keywords specific to its task.
Similarly, with regular expressions, you directly describe the text pattern you want to match using compact punctuation syntax, making it very concise compared to equivalent code in a general-purpose language.
An essential characteristic of DSLs is that they often follow a declarative approach, in contrast to the imperative nature of most general-purpose programming languages. The distinction lies in how they describe operations:
Imperative Languages
General-purpose languages are usually imperative, where you explicitly define the exact sequence of steps required to perform an operation. It specifies how to achieve a result through a series of commands or instructions.
Declarative Languages
On the other hand, DSLs tend to be declarative. They focus on describing the desired result rather than the step-by-step process to achieve it. The execution details are left to the underlying engine that interprets the DSL. This can lead to more efficient execution because optimizations are implemented once in the execution engine, while an imperative approach requires optimizations for each individual implementation of the operation.
However, there is a trade-off to consider with declarative DSLs. While they offer numerous benefits, they also come with a significant disadvantage: it can be challenging to seamlessly integrate them into a host application written in a general-purpose language. DSLs often have their own specific syntax, which cannot be directly embedded into programs written in another language. To use a program written in a DSL, you usually need to store it in a separate file or embed it as a string literal.
This separation can lead to difficulties in validating the correct interaction of the DSL with the host language at compile time, debugging the DSL program, and providing IDE code assistance when writing it. Additionally, the different syntax can make code harder to read and understand.
To address these challenges while retaining most of the benefits of DSLs, the concept of internal DSLs has gained popularity. Internal DSLs are designed to be embedded within a host language, taking advantage of the host language’s syntax and tools while still providing a domain-specific expressive power. This approach helps overcome the integration and tooling issues associated with traditional external DSLs.
What are external DSLs?
External Domain-Specific Languages (DSLs) are a type of domain-specific language that is distinct from the host programming language in which it is embedded. A domain-specific language is a language designed for a specific problem domain or application context, tailored to address the unique requirements and challenges of that domain.
External DSLs are created to facilitate a more intuitive and expressive way of defining solutions for specific domains. Instead of using the syntax and constructs of a general-purpose programming language, developers create a new language with syntax and semantics that are closely aligned with the problem domain. This allows users (often non-programmers) to express solutions using familiar terminology and concepts, making the code more readable and less error-prone.
Key characteristics of external DSLs include:
Separation from host language:External DSLs have their own syntax and grammar, independent of the underlying host programming language. This means that the DSL code is not written directly in the host language but in a separate file or structure.
Domain-specific abstractions:The syntax and semantics of the external DSL are tailored to the specific domain, making it more natural for domain experts to understand and work with the code.
Readability and simplicity:External DSLs are designed to be easily readable and writable by domain experts, even if they do not have extensive programming knowledge.
Specific scope and focus: Each external DSL is designed to tackle a particular problem domain, ensuring it remains concise and focused.
Custom tools and parsers:To work with external DSLs, custom tools and parsers are developed to interpret and transform the DSL code into executable code or other desired outputs.
Examples of External DSLs:
Regular expressions:Regular expressions are a classic example of an external DSL used for pattern matching in strings. They have a concise and domain-specific syntax for expressing text patterns.
SQL (Structured Query Language): SQL is a popular external DSL used for querying and managing relational databases. It provides a language-specific syntax for expressing database operations.
HTML (HyperText Markup Language):While HTML is commonly used within web development, it can be considered an external DSL as it has its own specific syntax and is used to describe the structure and content of web pages.
Creating an external DSL typically involves designing the language’s grammar, specifying the semantics, and building the necessary tools (e.g., parsers, interpreters, code generators) to work with the DSL effectively. External DSLs can be a powerful tool for improving productivity and collaboration between domain experts and programmers, as they allow domain experts to focus on their expertise without being overwhelmed by the complexities of a general-purpose programming language.
Internal DSLs
As opposed to external DSLs, which have their own independent syntax, An internal DSL (Domain-Specific Language) is a type of DSL that is embedded within a general-purpose programming language and utilizes the host language’s syntax and constructs. In other words, it’s not a separate language but rather a specific way of using the main language to achieve the benefits of DSLs with an independent syntax. The code written in an internal DSL looks and feels like regular code in the host language but is structured and designed to address a particular problem domain more intuitively and efficiently.
To compare the two approaches, let’s see how the same task can be accomplished with an external and an internal DSL. Imagine that you have two database tables, Customer and Country, and each Customer entry has a reference to the country the customer lives in. The task is to query the database and find the country where the majority of customers live. The external DSL you’re going to use is SQL; the internal one is provided by the Exposed framework (https://github.com/JetBrains/Exposed), which is a Kotlin framework for database access.
As you can see, the internal DSL version in Kotlin closely resembles regular Kotlin code, and the operations like slice, selectAll, groupBy, and orderByare just regular Kotlin methods provided by the Exposed framework. The query is expressed using these methods, making it easier to read and write than the SQL version. Additionally, the results of the query are directly delivered as native Kotlin objects, eliminating the need to manually convert data from SQL query result sets to Kotlin objects.
The internal DSL approach provides the advantages of DSLs, such as improved readability and expressiveness for the specific domain, while leveraging the familiarity and power of the host language. This combination makes the code more maintainable, less error-prone and allows domain experts to work more effectively without the need to learn a completely separate syntax.
Structure of DSLs
Generally speaking, there’s no well-defined boundary between a DSL and a regular API. The distinction between a Domain-Specific Language (DSL) and a regular Application Programming Interface (API) can be somewhat subjective, often relying on an “I know it’s a DSL when I see it” intuition. DSLs often utilize language features commonly used in other contexts, like infix calls and operator overloading. However, DSLs possess a key characteristic that sets them apart: a well-defined structure or grammar.
A typical library consists of many methods, and the client uses the library by calling the methods one by one. There’s no inherent structure in the sequence of calls, and no context is maintained between one call and the next. Such an API is sometimes called a command-query API. In contrast, the method calls in a DSL exist in a larger structure, defined by the grammar of the DSL. In a Kotlin DSL, structure is most commonly created through the nesting of lambdas or through chained method calls. You can clearly see this in the previous SQL example: executing a query requires a combination of method calls describing the different aspects of the required result set, and the combined query is much easier to read than a single method call taking all the arguments you’re passing to the query.
This grammar is what allows us to call an internal DSL a language. In a natural language such as English, sentences are constructed out of words, and the rules of grammar govern how those words can be combined with one another. Similarly, in a DSL, a single operation can be composed out of multiple function calls, and the type checker ensures that the calls are combined in a meaningful way. In effect, the function names usually act as verbs (groupBy, orderBy), and their arguments fulfill the role of nouns (Country.name).
An internal Domain-Specific Language (DSL) offers several advantages, one of which is the ability to reuse context across multiple function calls, avoiding unnecessary repetition.
With the DSL structure, you can list dependencies without repeating the “compile” keyword for each one. This results in cleaner and more concise code.
On the other hand, when using a regular command-query API for the same purpose, you would have to duplicate the “compile” keyword for each dependency. This leads to more verbose and less readable code.
Chained method calls are another way DSLs create structure, as seen in test frameworks. They allow you to split assertions into multiple method calls, making the code more readable.
In the example from kotlintest (https://github.com/ kotlintest/kotlintest), the DSL syntax allows you to express the assertion concisely using the “should” keyword:
Kotlin
str should startWith("kot") // Structure through chained method calls
while the equivalent code using regular JUnit APIs is more cumbersome and harder to comprehend:
Java
assertTrue(str.startsWith("kot"))
Now let’s look at an example of an internal DSL in more detail.
Building HTML with an internal DSL
At the beginning of this article, we use a DSL for building HTML pages also. In this section, we will discuss it in more detail. The API used here comes from the kotlinx.html library (https://github.com/Kotlin/kotlinx.html). Here is a small snippet that creates a table with a single cell:
BTW, Why would you want to build this HTML with Kotlin code, rather than write it as text? here are the answers:
By building HTML with Kotlin code rather than writing it as plain text, you gain several advantages. Firstly, the Kotlin version is type-safe, ensuring that you use the correct HTML tags in their appropriate contexts. For instance, the td tag can only be used inside a tr tag; otherwise, the code won’t compile, preventing common HTML structure mistakes.
The main advantage of DSLs is that they are regular code, allowing you to leverage the full power of the Kotlin language constructs. This means you can generate HTML elements dynamically based on conditions or data, making your code more flexible and expressive.
To illustrate this, consider the createAnotherTable() function. It generates an HTML table containing data from a map, where each entry in the map corresponds to a table row with two cells. By using a loop and Kotlin constructs, you can easily create the table structure and populate it with the desired data in a concise and readable manner.
here is an example of creating a table with dynamic content from a map:
Kotlin
import kotlinx.html.*import kotlinx.html.stream.createHTMLfuncreateAnotherTable(): String = createHTML().table {val numbers = mapOf(1 to "one", 2 to "two")for ((num, string) in numbers) {tr {td { +"$num" }td { +string } } }}
The example showcased HTML as a canonical markup language, but the same approach can be used for other languages with a similar structure, such as XML. This demonstrates the versatility of DSLs in Kotlin, as you can adapt the concept to various contexts and languages.
To create DSLs in Kotlin, one key feature that aids in establishing the grammar and syntax is “lambdas with receivers.” This feature allows you to define lambdas in a way that they can access the properties and functions of a designated receiver object within their scope. In the HTML DSL example, the tablefunction is the receiver, enabling the nested lambdas for tr and td to access its properties and construct the HTML elements in a natural, hierarchical way.
The use of DSLs in these examples not only results in more readable and expressive code but also provides type safety and error checking. By leveraging the language’s features, like lambdas with receivers, you can create custom syntaxes that make your code more readable, maintainable, and error-resistant. Whether it’s for generating HTML, XML, or other structured languages. DSLs are a powerful tool in the Kotlin developer’s arsenal.
Building structured APIs: lambdas with receivers in DSLs
Lambdas with receivers are a helpful tool in Kotlin that lets you design APIs with a clear structure. We’ve talked about how having structure is important in making Domain-Specific Languages (DSLs) different from normal APIs. Now, let’s take a closer look at this concept and explore some DSL examples that make use of it.
Lambdas with receivers and extension function types
In Kotlin programming, lambdas with receivers and extension function types are powerful concepts. They allow you to manipulate objects within a lambda expression’s scope, and they’re often used in conjunction with standard library functions like buildString, with, apply, and custom extension functions. Now, we’ll see how they work by looking at the buildString function as an example. This function lets you create a string by putting together different parts of content into a temporary StringBuilder.
To start, let’s understand the buildString function. It takes a regular lambda as input:
Kotlin
funbuildString( builderAction: (StringBuilder) -> Unit// Declares a parameter of a function type): String {val sb = StringBuilder()builderAction(sb) // Passes a StringBuilder as an argument to the lambdareturn sb.toString()}funmain() {val s = buildString { it.append("Hello, ") // Uses “it” to refer to the StringBuilder instance it.append("World!") }println(s) // Output: Hello, World!}
This function takes a lambda as an argument, allowing you to manipulate a StringBuilder within the lambda’s scope and then return the resulting string.
Let’s first see how the code works for better understanding, so here is a breakdown of the code working:
The buildString function is defined, which takes a lambda named builderAction as an argument. The lambda has a single parameter of type StringBuilder and returns Unit (void).
Inside the buildString function, a StringBuilder named sb is created.
The builderAction lambda is invoked with the sbStringBuilder as its argument. This lambda is where you can manipulate the StringBuilder to build the desired string content.
Finally, the StringBuilder‘s contents are converted to a string using sb.toString() and returned by the buildString function.
Outside the buildString function, the code snippet demonstrates how to use it. A lambda is passed to buildString using the trailing lambda syntax. This lambda appends “Hello, ” and “World!” to the StringBuilder.
The resulting string is assigned to the variable s.
The println statement outputs the value of s, which contains “Hello, World!”.
This code is quite understandable, but it seems a bit more complex to use than we’d prefer. Notice that you have to use “it” inside the lambda to refer to the StringBuilder instance. You could use your own parameter name instead of “it,” but it still needs to be explicit.
The main goal of the lambda is to fill the StringBuilder with text. So, it would be better to remove the repeated “it.” prefixes and directly use the StringBuilder methods like “append” instead of “it.append.”
To achieve this, you can transform the lambda into a lambda with a receiver. Essentially, you can give one of the lambda’s parameters a special role as a receiver. This lets you refer to its parts directly without needing any qualifier. The following example demonstrates how you can do this:
Kotlin
funbuildString( builderAction: StringBuilder.() -> Unit // Declares a parameter of a function type with a receiver): String {val sb = StringBuilder() sb.builderAction() // Passes a StringBuilder as a receiver to the lambdareturn sb.toString()}funmain() {val s = buildString { this.append("Hello, ") // The “this” keyword refers to the StringBuilder instance.append("World!") // Alternatively, you can omit “this” and refer to StringBuilder implicitly }println(s) // Output: Hello, World! }
In this version:
The builderAction lambda is defined with a receiver type of StringBuilder. This means that the lambda can directly access and manipulate the functions and properties of the StringBuilder instance that it is called on.
Inside the buildString function, a StringBuilder named sb is created.
The builderAction lambda is invoked on the sbStringBuilder instance, which allows you to use the append function directly within the lambda’s scope.
The resulting string is returned by the buildString function and printed using println.
Both versions of the buildString function achieve the same goal: creating a string by manipulating a StringBuilder instance within a lambda’s scope.
Let’s break down those differences:
First, let’s focus on the improvements in how you use buildString. In the first version, you were passing a regular lambda as an argument. This means you needed to use “it” inside the lambda to refer to the StringBuilder instance. However, in the second version, you’re passing a lambda with a receiver. This allows you to get rid of “it” within the lambda’s body. So instead of “it.append()”, you simply use “append()”. The full form could be “this.append()”, but typically, “this” is only used for clarification when needed (Like regular members of a class, you typically use the explicit keyword ‘this’ only to remove ambiguity).
Now, let’s look at the change in how the buildString function is declared. In the first version, you used a regular function type for the parameter type. In the second version, you use an extension function type instead. This involves taking one of the function type’s parameters out of the parentheses and placing it in front, separated by a dot. In this case, you replace (StringBuilder) -> Unit with StringBuilder.() -> Unit. This special type is referred to as the “receiver type.”The value of this type that’s passed to the lambda becomes the “receiver object”.
For a more intricate extension function type declaration, take a look at the below Figure.
An extension function type with receiver type String and two parameters of type Int, returning Unit
Have you ever wondered why to use an extension function type?
Think about accessing parts of another type without needing a clear label. This might remind you of extension functions, which let you add your own methods to classes from different parts of the code. Both extension functions and lambdas with receivers work with a receiver object. You provide this object when you call the function, and it’s available inside the function’s code. In simple terms, an extension function type describes a block of code that can be used like an extension function.
When you change a variable from a regular function type to an extension function type, the way you use it also changes. Instead of passing an object as an argument, you treat the lambda variable like an extension function. With a regular lambda, you pass a StringBuilder instance like this: builderAction(sb). But with a lambda having a receiver, it becomes: sb.builderAction(). Here, builderAction isn’t a method declared in the StringBuilder class. It’s a parameter of a function type, and you call it using the same style as extension functions.
Consider the relationship between an argument and a parameter in the buildString function. This helps you see the idea better. It also shows how the receiver in the lambda body comes into play. You can take a look at the below Figure for a visual representation of this concept. It clarifies how the lambda body is called on the receiver.
Connecting Argument, Parameter, and Receiver
The argument of the buildString function (a lambda with a receiver) corresponds to the parameter of the extension function type (builderAction). The receiver (sb) becomes an implicit receiver (this) when the lambda body is invoked. This means that in the buildString function with a lambda that has a receiver, the argument you provide corresponds to the parameter in the extension function type (builderAction). When you call the lambda’s body, the receiver (sb) becomes an implicit receiver (this).
You can also declare a variable of an extension function type, as shown in the following example. Once you do that, you can either invoke it as an extension function or pass it as an argument to a function that expects a lambda with a receiver.
Kotlin
val appendExcl: StringBuilder.() -> Unit = { //appendExcl is a value of an extension function type.this.append("!") }funmain() {val stringBuilder = StringBuilder("Hi") stringBuilder.appendExcl() // You can call appendExcl as an extension function.println(stringBuilder)val result = buildString(appendExcl) // You can also pass appendExcl as an argumentprintln(result)}
This example code defines a lambda with a receiver, stores it in a variable appendExcl, and demonstrates its usage with a StringBuilder instance as well as the buildString function.
Distinguishing Lambda with Receiver
It’s important to know that a lambda with a receiver and a regular lambda looks the same in the source code. To figure out if a lambda has a receiver, you should examine the function where you’re using the lambda. Check its signature to see if the lambda has a receiver and what type that receiver is. For instance, you can analyze the buildStringdeclaration or look it up in your coding tool (IDE). Seeing that it accepts a lambda of typeStringBuilder.() -> Unit, you’ll realize that within the lambda, you can directly use StringBuilder methods without needing a qualifier.
The buildString function shown above is even simpler in the standard library. The implementation of the standard library’s buildString is more concise. Instead of directly calling builderAction, it’s provided as an argument to the apply function. This approach condenses the function into just one line.
The apply function works by using the object it’s called on (like a new StringBuilder) as a hidden receiver to execute the provided function or lambda (like builderAction). It’s defined as an extension function to that receiver.
Kotlin
inlinefun <T> T.apply(block: T.() -> Unit): T {block() // Equivalent to this.block(); invokes the lambda with the receiver of “apply” as the receiver objectreturnthis// Returns the receiver}
The with function does a similar thing. It takes the receiver as its first argument and applies the function or lambda to it. The key difference is that apply returns the receiver itself, while with returns the result of the lambda.
If you don’t need the result of the operation, you can use either apply or with interchangeably. For example:
Kotlin
val map = mutableMapOf(1 to "one")map.apply { this[2] = "two" }with(map) { this[3] = "three" }println(map) // {1=one, 2=two, 3=three}
In Kotlin, both apply and with functions are frequently used due to their concise nature. They can make our code cleaner and more efficient.
Using lambdas with receivers in HTML builders
We’ve discussed lambdas with receivers and extension function types. Now, let’s explore how these concepts are applied in the context of DSLs (Domain Specific Languages).
A Kotlin DSL for HTML is usually called an HTML builder, and it represents a broader concept called type-safe builders. Initially, the idea of builders gained popularity in the Groovy community. Builders offer a method to create an organized structure of objects in a descriptive manner, which is helpful for creating things like XML or arranging UI components.
Kotlin adopts this idea but makes ittype-safe. This approach makes these builders more user-friendly, secure, and in a way, more appealing compared to Groovy’s dynamic builders. Now, let’s delve into the specifics of how HTML builders work in Kotlin.
Here we are creating a basic HTML table using a Kotlin HTML builder:
This is standard Kotlin code; there’s no specialized template language involved. The functions table, tr, and td are regular functions. Each of them is a higher-order function, meaning they take a lambda with a receiver as input.
What’s fascinating is that these lambdas alter the way names are understood. Inside the lambda given to the table function, you can use the tr function to create an HTML <tr> tag. Outside of this lambda, the tr function wouldn’t be recognized. Similarly, the td function is only accessible within the tr function. (The API design enforces adherence to the HTML language structure.)
The naming context within each block is determined by the receiver type of the lambda. The lambda for table has a receiver of a special type TABLE, which defines the tr method. Similarly, the tr function expects an extended lambda for TR.
The following listing is a greatly simplified view of the declarations of these classes and methods. Here we are declaring tag classes for the HTML builder
Kotlin
// Placeholder for the Tag classopenclassTag// Define the TABLE classclassTABLE : Tag {// Define a function to add TR tags to TABLEfuntr(init: TR.() -> Unit) { // The tr function expects a lambda with a receiver of type TR// Implementation of tr function }}// Define the TR classclassTR : Tag {// Define a function to add TD tags to TRfuntd(init: TD.() -> Unit) { // The tr function expects a lambda with a receiver of type TR// Implementation of td function }}// Define the TD classclassTD : Tag {// Implementation of TD class}
In this code, you are creating a basic structure for building an HTML table using Kotlin’s DSL-like capabilities. The Tag class (whose implementation is not shown in the above code snippet) likely serves as a base class or interface for HTML tags. The TABLE class has a function tr that accepts a lambda expression as an argument, allowing you to configure TR elements. Similarly, the TR class has a function td that accepts a lambda expression to configure TD elements.
The classes TABLE, TR, and TD are utility classes that don’t need to be directly mentioned in the code. That’s why they are in uppercase letters. They all inherit from the Tag superclass. Each of these classes defines methods for generating tags that are allowed within them. For instance, TABLE has the tr method, while TR has the td method.
Pay attention to the types of the init parameters in the tr and td functions: they are extension function types TR.() -> Unit and TD.() -> Unit. These determine the types of receivers expected in the argument lambdas: TR and TD, respectively.
To make the process clearer, you can rewrite the previous example while being explicit about all the receivers. Just remember, you can access the lambda’s receiver argument in the foo function by using this@foo.
table { ... }: This block defines the structure of the HTML table. It’s a lambda expression that’s executed within the context of the table tag.
(this@table).tr { ... }: Inside the table block, there’s a call to the tr function. (this@table) refers to the current table tag instance, and the tr function is called within its context.
(this@tr).td { ... }: Similarly, within the tr block, the td function is called with the context of the current tr tag instance.
Advantages of Lambdas with Receivers
Using regular lambdas instead of lambdas with receivers for builders would result in less readable code. You’d need to use the “it” reference to call tag-creation methods or assign new parameter names for each lambda. Making the receiver implicit and hiding the “this” reference is what makes the builder syntax clean and similar to the original HTML.
Nested Lambdas and Receivers
If you have one lambda with a receiver nested within another one (as seen in the above example), the receiver defined in the outer lambda remains accessible in the inner lambda. For instance, within the lambda argument of the td function, you have access to all three receivers: this@table, this@tr, and this@td. However, starting from Kotlin 1.1, you can use the @DslMarker annotation to control the availability of outer receivers in Lambdas.
Generating HTML to a string
We’ve explained how HTML builder syntax is built upon the concept of lambdas with receivers. Next, we’ll delve into how the desired HTML content is actually generated.
The above example uses functions from the kotlinx.html library. Now, we’ll create a simpler version of an HTML builder library. We’ll extend the declarations of TABLE, TR, and TD tags, and add support for generating the resulting HTML. Our starting point will be a top-level table function, which will generate an HTML fragment with <table> as the top tag.
Kotlin
import kotlinx.html.*import kotlinx.html.stream.createHTMLfuncreateTable(): String = createHTML().table {tr {td {// You can add content or other HTML elements here } }}funmain() {val tableHtml = createTable()println(tableHtml) // <table><tr><td></td></tr></table>}
The table function creates a fresh instance of the TABLE tag, initializes it (by calling the function provided as the init parameter on it), and then returns it. Here’s how it’s done:
In the createTable example, the lambda given as an argument to the table function contains the call to the tr function. To make everything as clear as possible, you could rewrite the call like this: table(init = { this.tr { ... } }). This will result in the tr function being invoked on the newly created TABLE instance, similar to writing TABLE().tr { ... }.
In this simplified example, <table> is the top-level tag, and other tags are nested inside it. Each tag keeps a list of references to its children. Because of this, the tr function needs to not only create a new TR tag instance but also add it to the list of children of the outer tag.
fun tr(init: TR.() -> Unit): This defines a function called tr that takes a lambda as a parameter. The lambda takes an instance of TR as its receiver and has a return type of Unit (i.e., it doesn’t return any value).
val tr = TR(): This creates an instance of the TR class, which represents an HTML table row.
tr.init(): This invokes the lambda passed to the tr function. The lambda is invoked in the context of the tr instance, allowing you to configure the properties of the tr element using the lambda’s receiver (i.e., this).
children.add(tr): This adds the configured tr instance as a child to some parent element. The children property likely refers to a list of child elements that the parent element contains.
The logic of initializing a tag and adding it to the children of the outer tag is shared among all tags. So, it’s possible to extract this logic into a doInit member method within the Tag superclass. The doInit function has two responsibilities: storing the reference to the child tag and executing the lambda provided as an argument. Then, different tags can call it. For instance, the tr function generates a new TR class instance and then hands it over to the doInit function, along with the init lambda: doInit(TR(), init).
Here’s the complete code implementation that demonstrates how the desired HTML is generated:
Kotlin
openclassTag(val name: String) {privateval children = mutableListOf<Tag>() // Stores all nested tags// Function to initialize a child tag and add it to the children listprotectedfun <T : Tag> doInit(child: T, init: T.() -> Unit) { child.init() // Call the init lambda on the child tag and Initializes the child tag children.add(child) // Add the child tag to the list and Store a reference to the child tag }// Generate the HTML representation of the tag and its childrenoverridefuntoString() ="<$name>${children.joinToString("")}</$name>"// Returns the resulting HTML as String}// Function to create a top-level <table> tagfuntable(init: TABLE.() -> Unit) = TABLE().apply(init)// Subclass representing the <table> tagclassTABLE : Tag("table") {// Function to add a <tr> tag as a childfuntr(init: TR.() -> Unit) = doInit(TR(), init) // Creates, initializes, and adds to the children of TABLE a new instance of the TR tag}// Subclass representing the <tr> tagclassTR : Tag("tr") {// Function to add a <td> tag as a childfuntd(init: TD.() -> Unit) = doInit(TD(), init) // Adds a new instance of the TD tag to the children of TR}// Subclass representing the <td> tagclassTD : Tag("td")// Function to create the HTML table structurefuncreateTable() =table {tr {td {// No content here } } }funmain() {println(createTable()) // Output the generated HTML}
The output of println(createTable()) is:
HTML
<table><tr><td></td></tr></table>
Each tag in this simplified implementation maintains a list of nested tags and renders itself accordingly. When rendered, it displays its name and recursively includes all the nested tags. It’s important to note that this version doesn’t handle text inside tags or tag attributes. For a complete and comprehensive implementation, you can explore the kotlinx.html library as mentioned earlier.
Also, it’s worth mentioning that the tag-creation functions are designed to automatically add the appropriate tag to the list of children of its parent. This allows you to dynamically generate tags, enhancing the flexibility of the HTML builder.
Generating tags dynamically with an HTML builder
Kotlin
funcreateAnotherTable() = table {for (i in1..2) {tr {td {// No content here } } }}funmain() {println(createAnotherTable()) // Output the generated HTML}
When you run this code and call createAnotherTable(), the output will be:
As you’ve seen, Lambdas with receivers are highly valuable for constructing DSLs. By altering the name-resolution context within a code block, they enable you to establish a structured API. This capability is a fundamental aspect that sets DSLs apart from mere sequences of method calls.
Kotlin builders: enabling abstraction and reuse
Now, let’s delve into the advantages of integrating such DSLs within statically typed programming languages.
Code Reusability with Internal DSLs
In regular programming, you can avoid repetition and enhance code readability by extracting repetitive chunks into separate functions with meaningful names. However, this might not be straightforward for languages like SQL or HTML. However, by utilizing internal DSLs in Kotlin, you can achieve the same goal of abstracting repeated code into new functions and reusing them effectively.
Example: Adding Drop-Down Lists with Bootstrap
Let’s consider an example from theBootstrap library, a popular framework for web development. The example involves adding drop-down lists to a web application. When you want to include such a list in an HTML page, you usually copy the required snippet and paste it where needed. This snippet typically includes references and titles for the items in the drop-down menu.
Here’s a simplified version of building a drop-down menu in HTML using Bootstrap:
This HTML code snippet demonstrates the creation of a dropdown menu using Bootstrap classes. It includes a button that triggers the dropdown, a list of menu items, separators, and a dropdown header. This manual approach is the standard way to create such dropdowns in HTML and CSS.
Next, we’ll see how Kotlin’s internal DSL can help streamline the process of generating this kind of HTML code.
Building a drop-down menu using a Kotlin HTML builder
In Kotlin with the kotlinx.html library, you can replicate the same HTML structure using functions like div, button, ul, li, and more. This is the power of Kotlin’s internal DSL approach for creating structured content like HTML. It allows you to build the same structure as the provided HTML code using functions that closely resemble the HTML tags and attributes. This approach can lead to cleaner and more maintainable code.
You can enhance the readability and reusability of the code by extracting repetitive logic into separate functions. This approach makes the code more concise and easier to maintain. Here’s the improved version of the code:
In this code, you’ve encapsulated the entire dropdown creation logic using functions that closely mimic the HTML structure. This approach enhances readability and reduces repetition, leading to more maintainable and modular code. The code now clearly expresses the intention of creating a dropdown, a dropdown button, dropdown menu items, a divider, and a dropdown header. This example shows how Kotlin’s internal DSL can greatly improve the way structured content is created in a statically typed programming language.
Now, let’s explore the implementation of the item function and how it simplifies the code.
The item function is designed to add a new list item to the dropdown menu. Inside the function, it uses the existing li function (which is an extension to the UL class) to create a list item with an anchor (a) element containing the provided reference and name.
Here’s the code snippet demonstrating the item function’s implementation:
By defining the item function as an extension to the UL class, you can call it within any UL tag, and it will generate a new instance of a LI tag containing the anchor element. This encapsulates the creation of dropdown menu items and simplifies the code.
This approach allows you to transform the original version of the code into a cleaner and more readable version, all while maintaining the generated HTML structure. This showcases the power of Kotlin’s internal DSLs in abstracting away implementation details and creating more expressive APIs.
Using the item function for drop-down menu construction
In this version, the code looks cleaner and more declarative. The item function abstracts the creation of list items with anchor elements, and the rest of the code clearly represents the structure of the dropdown menu. The use of the li and ul functions provided by the kotlinx.html library allows you to create the desired structure while hiding low-level implementation details.
The extension functions defined on the UL class follow a consistent pattern, which allows you to easily replace the remaining li tags with more specialized functions. This pattern involves encapsulating the creation of specific list items using extension functions that leverage the power of Kotlin’s internal DSL.
By providing functions like item, divider, and dropdownHeader as extensions to the UL class, you’re able to abstract away the lower-level HTML tag creation and attributes. This not only enhances the readability of the code but also promotes code reusability and maintainability.
"divider” Function
This function creates a list item with the role attribute set to “separator” and a class of “divider.” It adds the list item using the li function.
Kotlin
funUL.divider() = li { role = "separator"; classes = setOf("divider") }
"dropdownHeader" Function
This function creates a list item with a class of “dropdown-header” and the provided text as its content. It also adds the list item using the li function.
Now, let’s explore the implementation of the dropdownMenu function, which creates a ul tag with the specified dropdown-menu class and takes a lambda with a receiver as an argument to fill the tag with content. This approach enables you to build the dropdown menu content using a more concise and structured syntax.
Kotlin
dropdownMenu {item("#", "Action")// ... other menu items}
In this code, you’re calling the dropdownMenu function and providing a lambda with a receiver as its argument. Inside this lambda, you’re able to use specialized functions like item, divider, and dropdownHeader to construct the content of the dropdown menu.
Certainly, you’re referring to the concept of using extension lambdas within the dropdownMenu function. This approach allows you to keep the same context and easily call functions that were defined as extensions to the UL class, such as UL.item. Here’s the declaration and usage of the dropdownMenu function:
In this declaration, the dropdownMenu function takes a lambda with a receiver of type UL.() -> Unit as an argument. This lambda can contain calls to functions like item, divider, and dropdownHeader that were defined as extensions to the UL class. The ul function creates the actual <ul> tag with the “dropdown-menu” class, and the provided lambda fills the content of the dropdown menu.
The dropdownButton function is implemented similarly. While we’re not providing the details here, you can find the complete implementation in the samples available for the kotlinx.html library.
Now, let’s explore the dropdown function. This function is more versatile since it can be used with any HTML tag. It allows you to place drop-down menus anywhere within your code.
The top-level function for building a drop-down menu
In this implementation, the dropdown function is defined as an extension function on StringBuilder. It takes a lambda with a receiver of type DIV.() -> Unit as an argument. This lambda is used to construct the content of the dropdown menu within a DIV container.
Inside the function, you’re calling the div function provided by the kotlinx.html library. The first argument is the class name “dropdown”, which applies the necessary styling. The second argument is the lambda with a receiver that you pass into the div function. This lambda allows you to construct the content of the dropdown menu within the context of the DIV tag.
This version is simplified for printing HTML as a string. In the complete implementation in kotlinx.html, an abstract TagConsumer class is used as the receiver, allowing support for various destinations for the resulting HTML output. This example highlights how abstraction and reuse can enhance your code and make it more comprehensible.
More flexible block nesting with the “invoke” convention
The “invoke convention” lets you treat custom objects like functions. Just like you can call functions by using parentheses (like function()), this convention allows you to call your own objects in a similar way.
This might not be something you use all the time, because it can make your code confusing. For example, writing something like 1() doesn’t make much sense. However, there are cases where it’s helpful, especially when creating Domain-Specific Languages (DSLs) which are specialized languages for specific tasks. We’ll explain why this is useful, but before that, let’s talk more about how this convention works.
The “invoke” convention: objects callable as functions
As we know Kotlin’s “conventions” are special functions with specific names. These functions are used in a different way than regular methods. For instance, we know the “get” convention that lets you use the index operator to access objects. If you have a variable called “foo” of a type called “Foo,” writing “foo[bar]” is the same as calling “foo.get(bar).” This works if the “get” function is defined as part of the “Foo” class or as an extra function attached to “Foo.”
Now, the “invoke” convention is similar, but it uses parentheses instead of brackets. When a class has an “invoke” method with the “operator” keyword,you can call an object of that class as if it were a function. Here’s an example to help understand this concept better.
Kotlin
classGreeter(val greeting: String) {operatorfuninvoke(name: String) { // Defines the “invoke” method on Greeterprintln("$greeting, $name!") }}funmain() {val bavarianGreeter = Greeter("Hello")bavarianGreeter("softAai") // Calls the Greeter instance as a function}
This code introduces the “invoke” method in the context of the “Greeter” class. This method allows you to treat instances of “Greeter” as if they were functions. Behind the scenes, when you write something like bavarianGreeter("softAai"), it’s actually translated to the method call bavarianGreeter.invoke("softAai"). It’s not complicated; it’s just like a normal rule: it lets you swap a wordy expression with a shorter and clearer one.
The “invoke” method isn’t limited to any specific setup. You can define it with any number of inputs and any output type.You can evenmake multiple versions of the “invoke” method with different types of inputs. When you use the class instance like a function, you can choose any of those versions for the call. Now, let’s examine when this approach is practically used. First, we’ll look at its usage in regular programming situations and then in a Domain-Specific Language (DSL) scenario.
The “invoke” convention and functional types
We can call a variable that holds a nullable function type by using the syntax “lambda?.invoke()”. This is done with the safe-call technique, combining the “invoke” method name.
Now that you’re familiar with the “invoke” convention, it should make sense that the regular way of calling a lambda (using parentheses like “lambda()”)is essentially an application of this convention. When not inlined, lambdas are turned into classes that implement functional interfaces like “Function1” and others.These interfaces define the “invoke” method with the appropriate number of parameters:
Kotlin
interfaceFunction2<inP1, inP2, outR> { // This interface denotes a function that takes exactly two argumentsoperatorfuninvoke(p1: P1, p2: P2): R}
When you treat a lambda like a function and call it, this action is transformed into a call to the “invoke” method, thanks to the convention we’ve been discussing. Why is this knowledge valuable? It offers a way to break down a complex lambda into multiple methods, while still allowing you to use it along with functions that require parameters of a function type.
To achieve this, you can create a class that implements an interface for a function type. You can define the base interface explicitly, such as “FunctionN,” or you can use a more concise format like “(P1, P2) -> R,” as shown in the following example. In this example, a class is used to filter a list of issues based on a complicated condition:
Kotlin
dataclassIssue(val id: String, val project: String, val type: String,val priority: String, val description: String)classImportantIssuesPredicate(val project: String) : (Issue) -> Boolean {overridefuninvoke(issue: Issue): Boolean {return issue.project == project && issue.isImportant() }privatefunIssue.isImportant(): Boolean {return type == "Bug" && (priority == "Major" || priority == "Critical") }}funmain() {val i1 = Issue("IDEA-154446", "IDEA", "Bug", "Major", "Save settings failed")val i2 = Issue("KT-12183", "Kotlin", "Feature", "Normal", "Intention: convert several calls on the same receiver to with/apply")val predicate = ImportantIssuesPredicate("IDEA")for (issue inlistOf(i1, i2).filter(predicate)) {println(issue.id) }}
Let’s first break down the code step by step:
Data Class Definition (Issue):
Kotlin
dataclassIssue(val id: String, val project: String, val type: String,val priority: String, val description: String)
This defines a data class called Issue. Data classes are used to store and manage data. In this case, each Issue has properties like id, project, type, priority, and description.
Custom Function-Like Class Definition (ImportantIssuesPredicate):
The ImportantIssuesPredicate class implements the (Issue) -> Booleanfunction type, which means it can be treated as a function taking an Issue parameter and returning a Boolean.
The class constructor takes a project parameter and initializes it.
The invoke function is overridden from the (Issue) -> Boolean function type. It checks whether the issue’s project matches the instance’s project and whether the issue is important using the isImportant function.
The isImportant function checks if an issue’s type is “Bug” and if the priority is “Major” or “Critical”.
Main Function (main):
Kotlin
funmain() {val i1 = Issue("IDEA-154446", "IDEA", "Bug", "Major", "Save settings failed")val i2 = Issue("KT-12183", "Kotlin", "Feature", "Normal", "Intention: convert several calls on the same receiver to with/apply")val predicate = ImportantIssuesPredicate("IDEA")for (issue inlistOf(i1, i2).filter(predicate)) {println(issue.id) }}
In the main function, two instances of Issue are created: i1 and i2.
An instance of the ImportantIssuesPredicate class is created with the project name “IDEA”.
The filter function is used with the predicate to filter the list of issues (i1 and i2) and retrieve those that match the predicate’s condition.
In the loop, the id of each filtered issue is printed.
When thecode is run, it filters the issues and prints the id of the important issues from the “IDEA” project:
IDEA-154446
In this case, the logic within the predicate is too intricate to fit into a single lambda. So, we divide it into several methods to ensure each check has a clear purpose. Transforming a lambda into a class that implements a function type interface and then overriding the “invoke” method is a way to perform this kind of improvement. This method offers a key benefit: the methods you extract from the lambda body have the smallest possible scope. They are only visible within the predicate class. This is advantageous when there’s substantial logic both within the predicate class and surrounding code. This separation of concerns helps maintain a clean distinction between different aspects of the code.
The “invoke” convention in DSLs: declaring dependencies in Gradle
Now, let’s explore how the “invoke” convention can enhance the flexibility of creating structures for your Domain-Specific Languages (DSLs).
Let’s see the example of the Gradle DSL for configuring the dependencies of a module. Here’s the code :
Kotlin
dependencies {compile("junit:junit:4.11")}
You might often need to support two different ways of organizing your code using either a nested block structure or a flat call structure within the same API. In simpler terms, you’d like to enable both of the following approaches:
In this design, users of the DSL can employ the nested block structure when configuring multiple items, and the flat call structure to keep the code concise when configuring only one thing.
For the first case, they call the compile method on the dependencies variable. The second notation can be expressed by defining the invoke method on dependencies to accept a lambda as an argument. This call looks like dependencies.invoke({ ... }).
The dependencies object is an instance of the DependencyHandler class, which defines both the compile and invoke methods. The invoke method takes a lambda with a receiver as an argument, and the type of receiver for this method is once again DependencyHandler. Inside the lambda’s body, you’re working with a DependencyHandler as the receiver, allowing you to directly call methods like compile on it. Here’s a simple example illustrating how this part of DependencyHandler might be implemented:
In this code, you define a class named DependencyHandler. This class has two main functions:
The compile function takes a coordinate parameter, which represents a dependency coordinate (e.g., “org.jetbrains.kotlin:kotlin-stdlib:1.0.0”). It prints a message indicating that a dependency has been added.
The invoke function takes a lambda with receiver of type DependencyHandler.This lambda allows you to use a block of code with a different syntax for adding dependencies.
Using the Custom DSL-like Syntax:
Kotlin
val dependencies = DependencyHandler()dependencies.compile("org.jetbrains.kotlin:kotlin-stdlib:1.0.0")dependencies {compile("org.jetbrains.kotlin:kotlin-reflect:1.0.0")}
You create an instance of DependencyHandler named dependencies.
You use the compile function directly on the dependencies instance to add a dependency on "org.jetbrains.kotlin:kotlin-stdlib:1.0.0".
You use the custom syntax made possible by the invoke function. Inside the block, you use the compile function as if it were a regular method, passing the dependency coordinate "org.jetbrains.kotlin:kotlin-reflect:1.0.0".
As a result, when you run this code, you’ll see the following output:
Kotlin
Added dependency on org.jetbrains.kotlin:kotlin-stdlib:1.0.0Added dependency on org.jetbrains.kotlin:kotlin-reflect:1.0.0
When you add the first dependency, you directly call the compile method. The second call, on the other hand, is essentially transformed into the following:
In simpler terms, what’s happening is that you’re treating the dependencies as a function and providing a lambda as an input. This lambda’s parameter type is a function type with a “receiver,” where the receiver type is the same as the DependencyHandler type. The invoke method then executes this lambda. Since it’s a method of the DependencyHandler class, an instance of that class is automatically available as a kind of “hidden” receiver, so you don’t have to mention it explicitly when you call body() within the lambda.
By making this small change and redefining the invoke method, you’ve significantly increased the flexibility of the DSL API. This pattern is versatile and can be reused in your own DSLs with minimal adjustments.
Kotlin DSLs in practice
By now, you’ve become acquainted with various Kotlin features that are employed when creating DSLs. Some of these features, like extensions and infix calls, should be familiar to you. Others, such as lambdas with receivers, were thoroughly explained in this article. It’s time to apply all this knowledge and explore a range of practical examples for constructing DSLs. Our examples will cover a variety of topics, including testing, expressing dates more intuitively, querying databases, and building user interfaces for Android applications.
Chaining infix calls: “should” in test frameworks
As we’ve previously mentioned, one of the key characteristics of an internal DSL is its clean syntax, achieved by minimizing punctuation in the code. Most internal DSLs essentially come down to chains of method calls. Any features that help reduce unnecessary symbols in these method calls are highly valuable. In Kotlin, these features include the shorthand syntax for invoking lambdas (which we’ve discussed in detail) and infix function calls. Here we’ll focus on their application within DSLs.
Let’s consider an example that uses the DSL of “kotlintest,” a testing library inspired by Scalatest. You encountered this library earlier in this article.
Expressing an assertion with the kotlintest DSL:
Kotlin
s should startWith("kot")
This call will fail with an assertion if the value of the s variable doesn’t start with “kot”. The code reads almost like English: “The s string should start with this constant.”To accomplish this, you declare the should function with the infix modifier.
The function should requires a Matcher instance, which is a versatile interface used for making assertions about values. The function startWith is a specific implementation of this Matcher interface. It verifies if a given string begins with a particular substring.
Defining a matcher for the kotlintest DSL
Kotlin
interfaceMatcher<T> {funtest(value: T)}classStartsWith(val prefix: String) : Matcher<String> {overridefuntest(value: String) {if (!value.startsWith(prefix)) {throwAssertionError("String '$value' does not start with '$prefix'") } }}funmain() {val startsWithHello: Matcher<String> = StartsWith("Hello")try { startsWithHello.test("Hello, World!") // No exception will be thrown. startsWithHello.test("Hi there!") // Throws an AssertionError. } catch (e: AssertionError) {println("Assertion error: ${e.message}") }}
In regular code, you usually capitalize class names like “StartWith.” However, in DSLs, naming rules can be different. In above code, using infix calls in the DSL context is easy and makes your code less cluttered. With some clever tricks, you can make it even cleaner. The kotlintest DSL allows for this.
Chaining calls in the kotlintest DSL
Kotlin
"kotlin" should start with "kot"
At first glance, this doesn’t look like Kotlin. To understand how it works, let’s convert the infix calls to regular ones.
Kotlin
"kotlin".should(start).with("kot")
This demonstrates that there were two infix calls in a row. The term “start” was the argument for the first call. Specifically, “start” represents the declaration of an object. On the other hand, “should” and “with” are functions that are used with infix notation.
The “should” function has a unique version that takes the “start” object as a parameter type. It then returns an intermediate wrapper on which you can utilize the “with” method.
Defining the API to support chained infix calls
Kotlin
objectstartinfixfunString.should(x: start): StartWrapper = StartWrapper(this)classStartWrapper(valvalue: String) {infixfunwith(prefix: String) {if (!value.startsWith(prefix)) {throwAssertionError("String does not start with $prefix: $value") } }}funmain() {val testString = "Hello, World!" testString should start with "Hello"}
The object being passed (start) is utilized not to transmit data to the function, but rather to play a role in the grammar of the DSL. By providing start as an argument, you can select the appropriate overload of the should function and obtain an instance of StartWrapper as the result. The StartWrapper class includes the with member, which takes the actual value as an argument.
The library supports other matchers as well, and they all read as English:
Kotlin
"kotlin" should end with "in""kotlin" should have substring "otl"
To enable this functionality, the should function offers additional overloads that accept object instances like end and have, and they return instances of EndWrapper and HaveWrapper, respectively.
This example might have seemed a bit tricky, but the outcome is so elegant that it’s worth understanding how this approach functions. The combination of infix calls and object instances empowers you to build relatively intricate grammatical structures for your DSLs. Consequently, you can use these DSLs with a clear and concise syntax. Additionally,it’s important to note that the DSL remains fully statically typed. If there’s an incorrect combination of functions and objects, your code won’t even compile.
Defining extensions on primitive types: handling dates
Kotlin
val yesterday = 1.days.agoval tomorrow = 1.days.fromNow
To implement this DSL using the Java 8 java.time API and Kotlin, you need just a few lines of code. Here’s the relevant part of the implementation.
Defining a date manipulation DSL
Kotlin
val Int.days: Periodget() = Period.ofDays(this)val Period.ago: LocalDateget() = LocalDate.now() - thisval Period.fromNow: LocalDateget() = LocalDate.now() + thisfunmain() {println(1.days.ago) // Prints a date 1 day ago.println(1.days.fromNow) // Prints a date 1 day from now.}
In this code snippet, the days property is an extension propertyon the Int type. Kotlin allows you to define extension functions on a wide range of types, including primitive types and constants. The days property returns a value of the Period type, which is a type from the JDK 8’s java.time API representing an interval between two dates.
To complete the functionality and accommodate the use of the word “ago,” you’ll need to define another extension property, this time on the Period class. The type of this property is a LocalDate, which represents a specific date. It’s worth noting that the use of the - (minus) operator in the implementation of the ago property doesn’t rely on any Kotlin-specific extensions. The LocalDate class from the JDK includes a method called minus with a single parameter, which matches the Kotlin convention for the - operator. Kotlin maps the operator usage to that method automatically.
Now that you have a grasp of how this straightforward DSL operates, let’s progress to a more intricate challenge: the creation of a DSL for database queries.
If you’re interested in exploring the complete implementation of the library, which supports various time units beyond just days, you can find it in the “kxdate” library on GitHub at this link: https://github.com/yole/kxdate.
Member extension functions: internal DSL for SQL
In DSL design, extension functions play a significant role. In this section, we’ll explore a further technique we’ve mentioned before: declaring extension functions and extension properties within a class. Such functions or properties are both members of their containing class and extensions to other types simultaneously. We refer to these functions and properties as “member extensions.”
Let’s explore a couple of examples of member extensions from the internal DSL for SQL using the Exposed framework that we mentioned earlier. Before we delve into those examples, let’s first understand how Exposed allows you to define the structure of a database.
When working with SQL tables using the Exposed framework, you’re required to declare them as objects that extend the Table class. Here’s an example declaration of a simple Country table with two columns.
Declaring a table in Exposed
Kotlin
objectCountry : Table() {val id = integer("id").autoIncrement().primaryKey()val name = varchar("name", 50)}
The declaration you provided corresponds to a table in a database. To actually create this table, you can use the SchemaUtils.create(Country) method. When you invoke this method, it generates the appropriate SQL statement based on the structure you’ve declared for the table. This SQL statement is then used to create the table in the database.
SQL
CREATETABLEIFNOTEXISTS Country ( id INT AUTO_INCREMENT NOT NULL,nameVARCHAR(50) NOT NULL,CONSTRAINT pk_Country PRIMARY KEY (id));
Just like when generating HTML, you can observe how the declarations in the original Kotlin code become integral components of the generated SQL statement.
When you inspect the types of the properties within the Country object, you’ll notice that they have the type Column with the appropriate type argument: id has the type Column<Int>, and name has the type Column<String>.
In the Exposed framework, the Table class defines various types of columns that you can declare for your table. This includes the column types we’ve just seen:
Kotlin
classTable {funinteger(name: String): Column<Int> {// Simulates creating an 'integer' column with the given name// and returning a Column<Int> instance. }funvarchar(name: String, length: Int): Column<String> {// Simulates creating a 'varchar' column with the given name and length// and returning a Column<String> instance. }// Other methods for defining columns could be here...}
The integer and varchar methods are used to create new columns specifically meant for storing integers and strings, respectively.
Now, let’s delve into specifying properties for these columns. This is where member extensions come into action:
Kotlin
val id = integer("id").autoIncrement().primaryKey()
Methods like autoIncrement and primaryKey are utilized to define the properties of each column. Each of these methods can be invoked on a Column instance and returns the same instance it was called on. This design allows you to chain these methods together. Here are simplified declarations of these functions:
Kotlin
classTable {fun <T> Column<T>.primaryKey(): Column<T> {// Adds primary key behavior to the column and returns the same column. }funColumn<Int>.autoIncrement(): Column<Int> {// Adds auto-increment behavior to an integer column and returns the same column. }// Other extension functions for columns could be here...}
These functions are part of the Table class, which means you can only use them within the scope of this class. This explains why it’s logical to declare methods as member extensions: doing so confines their usability to a specific context. You can’t specify column properties outside the context of a table because the required methods won’t be accessible.
Another excellent aspect of extension functions comes into play here — the ability to limit the receiver type. While any column within a table could potentially be a primary key, only numeric columns can be designated as auto-incremented. This constraint can be expressed in the API by declaring the autoIncrement method as an extension on Column<Int>. If you attempt to mark a column of a different type as auto-incremented, it will not compile.
Furthermore, when you designate a column as a primary key, this information is stored within the containing table. By having this function declared as a member of the Table class, you can directly store this information in the table instance.
Member extensions are still members
Member extensions indeed come with a notable limitation: the lack of extensibility. Since they’re part of the class, you can’t easily define new member extensions on the side.
Consider this example: Let’s say you want to expand Exposed’s capabilities to support a new type of database that introduces additional attributes for columns. Achieving this would require modifying the Table class definition and incorporating the member extension functions for the new attributes directly there. Unlike regular (non-member) extensions, you wouldn’t be able to add these necessary declarations without altering the original class. This is because the extensions wouldn’t have access to the Table instance where they could store the new definitions.
Overall, while member extensions provide clear advantages by keeping the context constrained and enhancing the syntax, they do come with the trade-off of reduced extensibility.
Let’s look at another member extension function that can be found in a simple SELECT query. Imagine that you’ve declared two tables, Customer and Country, and each Customer entry stores a reference to the country the customer is from. The following code prints the names of all customers living in the USA.
Joining two tables in Exposed
Kotlin
val result = (Country join Customer) .select { Country.name eq "USA" }result.forEach { println(it[Customer.name]) }
The select method can be invoked on a Table or on a join of two tables. It takes a lambda argument that specifies the condition for selecting the desired data.
The eq method is used as an infix function here. It takes the argument "USA". As you might have guessed, it’s another member extension.
In this case, you’re encountering another extension function, this time on Column. Just like before, it’s a member extension, so it can only be used in the appropriate context. For example, when defining the condition for the select method. The simplified declarations of the select and eq methods are as follows:
The SqlExpressionBuilder object offers various ways to express conditions in the Exposed framework. These include comparing values, checking for null values, performing arithmetic operations, and more. While you won’t explicitly refer to it in your code, you’ll frequently invoke its methods with it as an implicit receiver.
In the select function, a lambda with a receiver is used as an argument. Inside this lambda, the SqlExpressionBuilder object serves as an implicit receiver. This means you can utilize all the extension functions defined within this object, such as eq.
You’ve encountered two kinds of extensions on columns: those meant to declare a Table, and those intended for comparing values within conditions. If it weren’t for member extensions, you’d need to declare all of these functions as either extensions or members of Column. This would allow you to use them in any context. However, the approach of using member extensions enables you to exercise control over their scope and application.
Note: Delegated properties are a powerful concept that often plays a significant role in DSLs. I already discussed Kotlin Delegation & Delegated Propertiesin detail. The Exposed framework provides a great illustration of how delegated properties can be applied effectively within DSL design.
While we won’t reiterate the discussion on delegated properties here, it’s worth remembering this feature if you’re enthusiastic about crafting your own DSL or enhancing your API to make it more concise and readable. Delegated properties offer a convenient and flexible mechanism to simplify code and improve the user experience when working with DSLs or other specialized APIs.
Anko: creating Android UIs dynamically
Let’s explore how the Anko library can simplify the process of building user interfaces for Android applications by utilizing a DSL-like structure.
To illustrate, let’s take a look at how Anko can wrap Android APIs in a more DSL-like manner. The following example showcases the definition of an alert dialog using Anko, which displays a somewhat annoying message along with two options (to continue or to halt the operation).
Let’s identify the three lambdas in the above code snippet:
The first lambda is the third argument of the alert function. It is used to build the content of the alert dialog.
The second lambda is passed as an argument to the positiveButton function. It defines the action to be taken when the positive button is clicked.
The third lambda is passed as an argument to the negativeButton function. It specifies the action to be executed when the negative button is clicked.
The receiver type of the first (outer) lambda is AlertDialogBuilder. This means that you can access members of the AlertDialogBuilder class within this lambda to add elements to the alert dialog. In the code, you don’t explicitly mention the name of the AlertDialogBuilder class; instead, you interact with its members directly.
You add two buttons to the alert dialog. If the user clicks the Yes button, the process action will be called. If the user isn’t sure, the operation will be canceled. The cancel method is a member of the DialogInterface interface, so it’s called on an implicit receiver of this lambda.
Kotlin
import android.content.Contextimport android.content.DialogInterfaceclassAlertDialogBuilder {funpositiveButton(text: String, callback: DialogInterface.() -> Unit) {// Simulate positive button configurationprintln("Configured positive button: $text") }funnegativeButton(text: String, callback: DialogInterface.() -> Unit) {// Simulate negative button configurationprintln("Configured negative button: $text") }}funContext.alert( message: String, title: String, process: () -> Unit) {val builder = AlertDialogBuilder() builder.positiveButton("Yes") {process() } builder.negativeButton("No") {cancel() }// Simulate displaying the alert with configured optionsprintln("Alert title: $title")println("Alert message: $message")}funmain() {val context: Context = /* Obtain a context from your Android application */ context.alert("Are you sure?", "Confirmation") {// Simulate positive button actionprintln("User clicked 'Yes' and the process action is executed.") }}
Now let’s look at a more complex example where the Anko DSL acts as a complete replacement for a layout definition in XML. The next listing declares a simple form with two editable fields: one for entering an email address and another for putting in a password. At the end, you add a button with a click handler.
verticalLayout { ... }: This defines a vertical layout. All the UI components within the curly braces will be arranged vertically.
val email = editText { ... }: This creates an EditText for entering an email. The hint attribute sets the placeholder text to “Email”. The email variable will hold a reference to this EditText.
val password = editText { ... }: This creates an EditText for entering a password. The hint attribute sets the placeholder text to “Password”. The transformationMethod is set to hide the password characters. The password variable will hold a reference to this EditText.
button("Log In") { ... }: This creates a “Log In” button. The onClick block specifies what should happen when the button is clicked. In this case, the logIn function (assumed to be defined elsewhere) is called with the email and password text from the EditText fields.
The Anko library simplifies Android UI creation by providing a DSL that closely resembles the structure of UI components. It enhances readability and reduces the amount of boilerplate code needed for UI creation. Please note that you need to include the Anko library in your project to use these DSL functions.
Lambdas with receivers are a powerful tool in creating concise and structured UI elements. By declaring these elements in code instead of XML files, you can extract and reuse repetitive logic. This approach empowers you to distinctly separate the UI design and the underlying business logic into separate components, all within the realm of Kotlin code. This alignment results in more maintainable and versatile codebases for your Android applications.
Conclusion
In conclusion, Kotlin DSLs are a powerful tool that enables developers to build expressive, concise, and type-safe code for specific problem domains. By leveraging Kotlin’s features such as extension functions, lambda expressions, and infix notation, you can design a DSL that reads like a natural language, improving code readability and maintainability. Whether you’re developing Android apps, configuring build scripts, or building web applications, mastering Kotlin DSLs will undoubtedly boost your productivity and make your code more elegant and efficient. So, go ahead and explore the world of Kotlin DSLs to take your programming skills to new heights!
Kotlin, known for its concise syntax and powerful features, has gained immense popularity among developers. One of its notable features is the ability to declare kotlin inline properties. Kotlininline properties combine the benefits of properties and inline functions, providing improved performance and better control over code structure. In this blog post, we’ll dive deep into Kotlin inline properties, covering their definition, benefits, and use cases, and providing detailed examples to solidify your understanding.
Understanding Inline Properties
An inline property is a property that is backed by an inline function. This means that when you access the property, the code of the getter function is directly inserted at the call site, similar to how inline functions work. This has significant implications for performance, as it eliminates the overhead of function calls.
What are Kotlin inline properties?
Inline properties are a Kotlin feature that allows you to improve the performance of your code by inlining the property accessors into the code that uses them. This means that the compiler will copy the body of the accessors into the call site, instead of calling them as separate functions.
Inline properties can be used for both read-only (val) and mutable (var) properties. However, they can only be used for properties that do not have a backing field.
When to use Kotlin inline properties?
Inline properties should be used when you want to improve the performance of your code by reducing the number of function calls. This is especially useful for properties that are accessed frequently or that are used in performance-critical code.
Inline properties should not be used when the property accessors are complex or when the property is not accessed frequently. In these cases, the performance benefits of inlining may not be worth the added complexity.
Declaring Kotlin Inline Properties
To declare an inline property in Kotlin, you’ll use the inline keyword before the property definition. Here’s the general syntax:
inline:This keyword indicates that the property is inline, allowing its getter code to be inserted at the call site.
val:Indicates that the property is read-only.
propertyName: The name you give to your property.
PropertyType: The data type of the property.
propertyValue: The value that the property holds.
Few Simple Declarations of Kotlin inline properties
Here are some simpleexamplesof how to use Kotlin inline properties:
Kotlin
// A read-only inline propertyinlineval foo: Stringget() = "Hello, softAai!"// A mutable inline propertyinlinevar bar: Intget() = TODO() // You need a getter function for a mutable propertyset(value) {// Do something with the value. }// An inline property with a custom getter and setterinlineval baz: Stringget() = "This is a custom getter."set(value) {// Do something with the value. }
In the above code snippet:
For the bar property, you need to provide a getter function since it’s a mutable property. In this case, I’ve used TODO() to indicate that you need to replace it with an actual getter implementation.
The baz property is defined with a custom getter and setter. The getter provides a string value, and the setter is a placeholder where you can implement custom logic to handle the incoming value.
Use Cases for Kotlin Inline Properties
Simple Properties: Inline properties are ideal for cases where you have simple read-only properties that involve minimal computation. For instance, properties that return constant values or perform basic calculations can benefit from inlining.
Performance-Critical Code:In scenarios where performance is crucial, such as in high-frequency loops, using inline properties can significantly reduce function call overhead and lead to performance improvements.
DSLs (Domain-Specific Languages):Inline properties can be used to create more readable and expressive DSLs. The inlined properties can provide syntactic sugar that enhances the DSL’s usability.
Custom Accessors:Inline properties are useful when you want to customize the getter logic for a property without incurring function call overhead.
Examples of Kotlin Inline Properties
Let’s explore a few examples to solidify our understanding.
Example 1: Constant Inline Property
Kotlin
inlineval pi: Doubleget() = 3.141592653589793
In this example, the pi property always returns the constant value of Pi.
Example 2: Performance Optimization
Kotlin
dataclassPoint(val x: Int, val y: Int) {val magnitude: Doubleinlineget() = Math.sqrt(x.toDouble() * x + y.toDouble() * y)}funmain() {val point = Point(3, 4)println("Magnitude of the point: ${point.magnitude}")}
In this example, the inline property magnitude allows you to access the magnitude of the Point instance without invoking a separate function. The getter’s code is expanded and copied directly at the call site, eliminating the function call overhead.
Here, the cssClass property enhances the readability of constructing CSS class names within an HTML DSL.
Rules for Kotlin Inline Properties
1. Inline Modifier
To declare an inline property, use the inline modifier before the property definition. This indicates that the property getter’s code will be inserted directly at the call site.
Inline properties are read-only; they don’t support custom setters(as don’t have backing fields). You can only define the getter logic. It might feel confusing but don’t worry, you will get a clearer idea as we proceed. So, bear with me.
Example:
Kotlin
dataclassPoint(val x: Int, val y: Int)inlineval Point.magnitude: Doubleget() = Math.sqrt(x.toDouble() * x + y.toDouble() * y)
3. Limited Logic in Getters
Keep the logic inside the inline property getter minimal and straightforward. Avoid complex computations or excessive branching.
Example:
Kotlin
inlineval half: Intget() = 100 / 2
4. No Property Initialization
You can’t directly initialize the inline property’s value within the property declaration.
Example:
Kotlin
// Not allowedinlineval invalid: Int = 42// We will get this compilation error: Inline property cannot have backing field
5. Interaction with Inline Functions
When an inline property is accessed within an inline function, the property’s code is also inlined. This can create a hierarchy of inlining that affects performance and code size.
Example:
Kotlin
inlineval greeting: Stringget() = "Hello"inlinefunprintGreeting() {println(greeting) // The code of 'greeting' property will be inlined here}
By marking both the property and the function as inline, the property’s getter code is directly placed into the function’s call site. This can optimize performance by avoiding the function call overhead. However, it might lead to larger compiled code if the same property’s getter logic is used in multiple locations.
6. Parameterization with Higher-Order Functions
Inline properties can’t take parameters directly. You can use higher-order functions or lambdas for parameterized behavior.
Example:
Kotlin
inlineval greeting: (String) -> Stringget() = { name ->"Hello, $name!" }funmain() {val greetFunction = greeting // Assign the lambda to a variableval message = greetFunction("softAai") // Call the lambda with a nameprintln(message) // o/p : Hello, softAai!}
Inline Modifier and Inline Properties
The inline modifier can be used on accessors of properties that don’t have backing fields. You can annotate individual property accessors. That means we can mark entire property or individual accessors (getter and setter) as inline:
Inline Getter
Kotlin
var ageProperty: Intinlineget() { ... }set(value) { ... }
Inline Setter
Kotlin
var ageProperty: Intget() { ... }inlineset(value) { ... }
Inline Entire Property
You can also annotate an entire property, which marks both of its accessors as inline:
Remember, when you use inline accessors in Kotlin, whether for getters or setters, their code behaves like regular inline functions at the call site. This means that the code inside the accessor is inserted directly where the property is accessed or modified, similar to how inline functions work.
KotlinInline Properties and Backing Fields
In Kotlin, properties are usually associated with a backing field — a hidden field that stores the actual value of the property. This backing field is automatically generated by the compiler for properties with custom getters or setters. However, inline properties differ in this aspect.
What Does “No Backing Field” Mean?
When you declare an inline property, the property’s getter code is directly inserted at the call site where the property is accessed. This means that there’s no separate backing field holding the value of the property. Instead, the getter logic is inlined into the code that accesses the property, eliminating the need for a distinct memory location to store the property value.
Implications of No Backing Field
Memory Efficiency:Since inline properties don’t require a backing field, they can be more memory-efficient compared to regular properties with backing fields. This can be especially beneficial when dealing with large data structures or frequent property accesses.
Direct Calculation: The absence of a backing field means that any calculations performed within the inline property’s getter are done directly at the call site. This can lead to improved performance by avoiding unnecessary memory accesses.
Example: Understanding No Backing Field
Consider the following example of a regular property with a backing field and an inline property:
In this example, the area property has a backing field that stores the result of the area calculation. On the other hand, the perimeter inline property doesn’t have a backing field; its getter code is directly inserted wherever it’s accessed.
When to Use Kotlin Inline Properties without Backing Fields
Inline properties without backing fields are suitable for cases where you want to perform direct calculations or return simple values without the need for separate memory storage. They are particularly useful when the logic within the getter is straightforward and lightweight.
However, remember that inline properties are read-only and can’t have custom setters. We cannot set values to inline properties in the same way we do with regular properties. However, we can use custom setters for performing additional operations other than simple value assignment to it. Therefore, they’re most appropriate for scenarios where the value is determined by a simple calculation or constant.
Restriction: No Backing Field with inline Accessors
In Kotlin, when you use the inline modifier on property accessors (getter or setter), it’s important to note that this modifier is only allowed for accessors that don’t have a backing field associated with them. This means that properties with inline accessorscannot have a separate storage location (backing field) to hold their values.
Reason for the Restriction
The restriction on using inline with properties that have backing fields is in place to prevent potential issues with infinite loops and unexpected behavior. The inlining process could lead to situations where the inlined accessor is calling itself, creating a loop. By disallowing inline on properties with backing fields, Kotlin ensures that this kind of situation doesn’t occur.
Hypothetical Example
Consider the following hypothetical scenario, which would result in an infinite loop if the restriction wasn’t in place:
Kotlin
classInfiniteLoopExample {privatevar _value: Int = 0inlinevarvalue: Intget() = value// This could lead to an infinite loopset(v) { _value = v }}
In this example, if the inline modifier were allowed on the getter, an infinite loop would occur since the inline getter is calling itself.
To fix the code and prevent the infinite loop, you should reference the backing property _value in the getter and also make it public, as shown below:
Kotlin
classInfiniteLoopExample {var _value: Int = 0// // Change visibility to publicinlinevarvalue: Intget() = _value // Use the backing property hereset(v) { _value = v }}funmain() {val example = InfiniteLoopExample() example.value = 42println(example.value)}
Note: By changing the visibility to ‘public,’ you introduce a security risk as it exposes the internal details of your class. This approach is not recommended; even though it violates the ‘no backing field’ rule, I only made these changes for the sake of understanding. Instead, it’s better to follow the rules and guidelines for inline properties.
Real Life Example
Kotlin
inlinevar votingAge: Intget() {return18// Minimum voting age in India }set(value) {if (value < 18) {val waitingValue = 18 - valueprintln("Setting: Still waiting $waitingValue years to voting age") } else {println("Setting: No more waiting years to voting age") } }funmain() { votingAge = 4val votableAge = votingAgeprintln("The votable age in India is $votableAge")}
When you run the code, the following output will be produced:
Kotlin
Setting: Stillwaiting14yearstovotingageThe votable age in India is18
In India, the minimum voting age is 18 years old. This means that a person must be at least 18 years old in order to vote in an election. The inline property here stores the minimum voting age in India, and it can be used to check if a person is old enough to vote.
In the code, the value of the votingAge property is set to 4. However, the setter checks if the value is less than 18. Since it is, the setter prints a message saying that the person is still waiting to reach the voting age. The value of the votingAge property is not changed.
This code snippet can be used to implement a real-world application that checks if a person is old enough to vote. For example, it could be used to validate the age of a voter before they are allowed to cast their vote.
Benefits of Kotlin Inline Properties
There are several benefits to using Kotlin inline properties:
Performance Optimization:Inline properties eliminate the overhead of function calls, resulting in improved performance by reducing the runtime costs associated with accessing properties.
Control over Inlining: Inline properties give you explicit control over which properties should be inlined, allowing you to fine-tune performance optimizations for specific parts of your codebase.
Cleaner Syntax: Inline properties can lead to cleaner and more concise code by reducing the need for explicit getter methods.
Reduced Object Creation: In some cases, inline properties can help avoid unnecessary object creation, as the getter code is inserted directly into the calling code.
Smaller code size: Inline properties can reduce the size of your compiled code by eliminating the need to create separate functions for the property accessors.
Easier debugging:Inline properties can make it easier to debug your code by making it easier to see where the property accessors are being called.
Drawbacks of using Kotlin inline properties
There are a few drawbacks to using Kotlin inline properties:
Increased complexity: Inline properties can make your code more complex, especially if the property accessors are complex.
Reduced flexibility:Inline properties can reduce the flexibility of your code, because you cannot override or extend the property accessors.
Conclusion
Kotlin’s inline properties provide a powerful mechanism for optimizing code performance and enhancing code structure. By using inline properties, you gain the benefits of both properties and inline functions, leading to more readable and performant code. Understanding when and how to use inline properties can elevate your Kotlin programming skills and contribute to the efficiency of your projects.
In the world of Java programming, the concept of classes is central to the object-oriented paradigm. But did you know that classes can be nested within other classes? This unique feature is known as inner classes, and it opens up a whole new realm of possibilities in terms of code organization, encapsulation, and design patterns. In this blog post, we’ll delve into the fascinating world of inner classes, exploring their types, use cases, and benefits.
Introduction to Inner Classes
Sometimes, we can put a class inside another class. These are called “inner classes.” They were introduced in Java version 1.1 to fix problems with how events are handled in graphical interfaces. But because inner classes have useful features, programmers began using them in regular coding too.
We use inner classes when one type of object can’t exist without another type. For example, a university has departments. If there’s no university, there are no departments. So, we put the department class inside the university class.
Java
classUniversity { // Outer classclassDepartment { // Inner class }}
Similarly, a car needs an engine to exist. Since an engine can’t exist on its own without a car, we put the engine class inside the car class.
Java
classCar { // Outer classclassEngine { // Inner class }}
Also, think of a map that has pairs of keys and values. Each pair is called an entry. Since entries depend on maps, we define an entry interface inside the map interface.
These are declared as static inside another class.
They can access only static members of the outer class.
Example:
Java
classOuter {staticclassNested { }}
Remember:
Normal inner classes can access both static and instance members of the outer class.
Method local inner classes are declared inside methods and can only access final variables.
Anonymous inner classes are often used for implementing interfaces or extending classes without creating separate files.
Static nested classes are like regular classes but are defined within another class and can access only static members of the outer class.
When working with inner classes
Normal or Regular Inner Classes:
These are named classes declared within another class without the static keyword.
Compiling the below example generates two .class files: Outer.class and Outer$Inner.class.
Example:
Java
classOuter {classInner { }}
Running Inner Classes:
You can’t directly run an inner class from the command prompt unless it has a main method.
Attempting to run java Outer or java Outer$Inner without a main method leads to “NoSuchMethodError:main”.
Main Method Inside Outer Class:
By adding a main method in the outer class, you can run it.
Now, if we run below code java Outer will produce “Outer class main method”.
Example:
Java
classOuter {classInner { }publicstaticvoidmain(String[] args) {System.out.println("Outer class main method"); }}
Static Members in Inner Classes:
Inner classes can’t include static members, such as main methods.
Trying to place a main method inside an inner class results in a compile error: “Inner classes cannot have static declarations”.
In short, normal inner classes are named classes within another class, they can’t have static members, and their ability to be run directly depends on the presence of a main method.
Accessing Inner class code
Case 1: Accessing Inner Class Code from Static Area of Outer Class
Java
classOuter {classInner {publicvoidm1() {System.out.println("Inner class method"); } }publicstaticvoidmain(String[] args) {Outero = newOuter();Outer.Inneri = o.newInner();i.m1();// Alternatively:// 1. Outer.Inner i = new Outer().new Inner();// 2. new Outer().new Inner().m1(); }}
In this code:
The Outer class contains an inner class named Inner.
Inside the main method, we create an instance of Outer called o.
We then create an instance of the inner class using o.new Inner(), and call the m1() method on it.
The two alternative ways to create the inner class instance are shown as comments.
Running this code will print “Inner class method” to the console.
Case 2: Accessing Inner Class Code from Instance Area of Outer Class
Remember, the approach you choose depends on where you are accessing the inner class from and the context in which you want to use it.
Normal inner class / Regular inner class
1. In a normal or regular inner class, you can access both static and non-static members of the outer class directly. This makes it convenient to use and interact with the outer class’s members from within the inner class.
Java
classOuter {intx = 10;staticinty = 20;classInner {publicvoidm1() {System.out.println(x); // Accessing non-static member of outer classSystem.out.println(y); // Accessing static member of outer class } }publicstaticvoidmain(String[] args) {newOuter().newInner().m1(); }}
In this code:
The Outer class has an instance variable x and a static variable y.
The Inner class within Outer can directly access both x and y from the outer class.
Inside the m1() method of Inner, the non-static member x and the static member y are both printed.
When you run the main method, the output will be:
Java
1020
This demonstrates how a normal inner class can freely access both static and non-static members of its enclosing outer class.
2.In an inner class, the keyword this refers to the current instance of the inner class itself. If you want to refer to the instance of the outer class, you can use the syntax OuterClassName.this. This is particularly useful when there might be naming conflicts or when you explicitly want to access the outer class’s instance.
The Outer class contains an instance variable x with a value of 10.
Inside the Outer class, there’s an Inner class with its own instance variable x set to 100.
The m1() method inside the Inner class has a local variable x set to 1000.
Printing x will show the value of the local variable (1000).
Printing this.x inside the Inner class refers to the x within the Inner class (100).
Printing Outer.this.x refers to the x within the Outer class (10).
When you run the main method, the output will be:
Java
100010010
This code demonstrates the different levels of scope and how you can use this and OuterClassName.this to access variables from various contexts within an inner class.
Applicable access modifiers for both outer and inner classes in Java
For outer classes:
The access modifiers that can be applied are public, default (no modifier), final, abstract, and strictfp.
For inner classes:
The access modifiers that can be applied are: private, protected, and static.
Nesting of inner classes
Nesting of inner classes is possible, which means you can define one inner class inside another inner class. This creates a hierarchical structure of classes within classes. This is also known as nested inner classes.
In this example, we have an Outer class with an Inner class inside it, and within the Inner class, there’s a NestedInner class. You can create instances of each class and access their members accordingly.
When you run the code, it will display:
Java
NestedVar:30InnerVar:20OuterVar:10
This shows that nesting of inner classes allows you to organize your code in a structured manner and access members at different levels of nesting.
Method Local Inner Classes
Method local inner classes are inner classes that are defined within a method’s scope. They are only accessible within that specific method and provide a way to encapsulate functionality that is needed only within that method. This type of inner class is particularly useful when you want to confine a class’s scope to a specific method, keeping the code organized and localized.
Main Purpose of Method Local Inner Classes:
Method local inner classes are intended to define functionality that is specific to a particular method.
They encapsulate code that is required repeatedly within that method.
Method local inner classes are well-suited for addressing nested, localized requirements within a method’s scope.
Method local inner classes can only be accessed within the method where they are defined.
They have a limited scope and aren’t accessible outside of that method.
Method local inner classes are the least commonly used type of inner classes.
They are employed when specific circumstances demand a highly localized class definition.
The Test class has a method m1() that contains a method local inner class named Inner.
The Inner class has a method sum() that calculates and prints the sum of two numbers.
Within m1(), you create an instance of the Inner class and call its sum() method multiple times.
Running the code produces the following output:
Java
TheSum:30TheSum:300TheSum:3000
The above code effectively demonstrates how method local inner classes can be used to encapsulate functionality within a specific method’s scope.
We can declare a method-local inner class inside both instance and static methods.
If we declare an inner class inside an instance method, we can access both static and non-static members of the outer class directly from that method-local inner class.
On the other hand, If we declare an inner class inside a static method, we can access only static members of the outer class directly from that method-local inner class.
Example
Java
classTest {intx = 10;staticinty = 20;publicvoidm1() {classInner {publicvoidm2() {System.out.println(x); // Accessing instance member of outer classSystem.out.println(y); // Accessing static member of outer class } }Inneri = newInner();i.m2(); }publicstaticvoidmain(String[] args) {Testt = newTest();t.m1(); }}
Now, when we run this code, the output will be:
Java
1020
This demonstrates that method local inner classes can access both instance and static members of the outer class within the context of an instance method.
Now, If we declare the m1() method as static, you will indeed get a compilation error at line 1 where you’re trying to access the non-static variable x from a static context. Here’s how the code would look with the error:
Java
classTest {intx = 10;staticinty = 20;publicstaticvoidm1() {classInner {publicvoidm2() {System.out.println(x); // Compilation error: non-static variable x cannot be referenced from a static contextSystem.out.println(y); } }Inneri = newInner();i.m2(); }publicstaticvoidmain(String[] args) {Test.m1(); }}
In this version of the code, since m1() is declared as static, it can’t access instance variables like x directly. The compilation error mentioned in a comment will occur at the line where you’re trying to access x from the method local inner class’s m2() method. The y variable, being static, can still be accessed without an issue.
We will now look at a Very Important Concept in Inner Classes.
From a method-local inner class, we can’t access local variables of the method in which we declare the inner class. However, if the local variable is declared as final, then we can access it.
Java
classTest {publicvoidm1() {finalintx = 10; // Declaring a final local variable 'x' with a value of 10classInner {publicvoidm2() {System.out.println(x); // Accessing the final local variable 'x' within the inner class } }Inneri = newInner(); // Creating an instance of the inner classi.m2(); // Calling the method of the inner class to print the value of 'x' }publicstaticvoidmain(String[] args) {Testt = newTest(); // Creating an instance of the outer classt.m1(); // Calling the method of the outer class }}
Explanation:
In the m1() method of the Test class, a local variable x is declared and initialized with the value 10. The variable x is marked as final, indicating that its value cannot be changed after initialization.
Inside the m1() method, an inner class named Inner is defined. This inner class contains a method m2().
The m2() method of the Inner class prints the value of the final local variable x. Since x is declared as final, it can be accessed within the inner class.
Back in the m1() method, an instance of the Inner class is created using Inner i = new Inner();.
The m2() method of the inner class is called using the instance i, which prints the value of the final local variable x.
In the main method, an instance of the Test class is created (Test t = new Test();).
The m1() method of the outer class is called using the instance t, which triggers the creation of an instance of the inner class and the printing of the value of the final local variable x.
Output: When you run the code, the output will be
Java
10
This output confirms that the inner class is able to access the final local variable x.
a) At Line 1, which of the following variables can we access directly? i, j, k, m
Answer →We can access all variables except ‘k’ directly.
b) If we declare m1() as static, then at Line 1, which variables can we access directly? i, j, k, m
Answer –> We can access only ‘j’ and ‘m’.
c) If we declare m2() as static, then at Line 1, which variables can we access directly? i, j, k, m
Answer –>We will get a compilation error (CE) because we cannot declare static members inside inner classes.
Note → The only applicable modifiers for method-local inner classes are final, abstract, and strictfp. If we try to apply any other modifier, we will get a compilation error (CE).
Anonymous Inner Class
Sometimes, inner classes can be declared without a name. Such inner classes are called ‘anonymous inner classes.’ The main purpose of anonymous inner classes is for instant use, typically for one-time usage.
Anonymous Inner Classes:
Anonymous inner classes are inner classes declared without a name.
They are primarily used for instant (one-time) usage.
Anonymous inner classes can be categorized intothree types based on their declaration and behavior.
Types of Anonymous Inner Classes
Based on their declaration and behavior, there are three types of anonymous inner classes:
1. Anonymous Inner Class that Extends a Class
An anonymous inner class can extend an existing class.
It provides an implementation for the methods of the superclass or overrides them.
2. Anonymous Inner Class that Implements an Interface
An anonymous inner class can implement an interface.
It provides implementations for the methods declared in the interface.
3. Anonymous Inner Class Defined Inside Arguments
An anonymous inner class can be defined as an argument to a method.
You’re declaring an anonymous inner class that extends PopCorn.
You’re overriding the taste() method within this anonymous inner class.
You’re creating an object of this anonymous inner class using the PopCorn reference p.
Different approaches to working with threads
Normal Class Approach:
Java
// Example using a normal class that extends ThreadclassMyThreadextendsThread {publicvoidrun() {for (inti = 0; i < 10; i++) {System.out.println("Child Thread"); } }}classThreadDemo {publicstaticvoidmain(String[] args) {MyThreadt = newMyThread();t.start();for (inti = 0; i < 10; i++) {System.out.println("Main Thread"); } }}
Anonymous Inner Class Approach:
Java
// Example using an anonymous inner class extending ThreadclassThreadDemo {publicstaticvoidmain(String[] args) {Threadt = newThread() {publicvoidrun() {for (inti = 0; i < 10; i++) {System.out.println("Child Thread"); } } };t.start();for (inti = 0; i < 10; i++) {System.out.println("Main Thread"); } }}
Anonymous Inner class that implements an Interface
Normal Class Approach:
Java
classMyRunnableimplementsRunnable {publicvoidrun() {for (inti = 0; i < 10; i++) {System.out.println("Child Thread"); } }}classThreadDemo {publicstaticvoidmain(String[] args) {MyRunnabler = newMyRunnable();Threadt = newThread(r); // where r is target runnablet.start();for (inti = 0; i < 10; i++) {System.out.println("Main Thread"); } }}
Anonymous Inner Class Implementing an Interface:
Note ->Defining a thread by implementing a runnable interface
Java
// Example using an anonymous inner class implementing Runnable interfaceclassThreadDemo {publicstaticvoidmain(String[] args) {Runnabler = newRunnable() {publicvoidrun() {for (inti = 0; i < 10; i++) {System.out.println("Child Thread"); } } };Threadt = newThread(r);t.start();for (inti = 0; i < 10; i++) {System.out.println("Main Thread"); } }}
Anonymous Inner class that defines inside arguments
Java
// Example using an anonymous inner class inside argumentsclassThreadDemo {publicstaticvoidmain(String[] args) {newThread(newRunnable() {publicvoidrun() {for (inti = 0; i < 10; i++) {System.out.println("Child Thread"); } } }).start();for (inti = 0; i < 10; i++) {System.out.println("Main Thread"); } }}
All the above code examples effectively illustrate the different ways to work with threads using both normal classes and anonymous inner classes
Normal Java Class Vs Anonymous Inner Class
The differences between normal Java classes and anonymous inner classes when it comes to extending classes, implementing interfaces, and defining constructors.
Extending a Class:
A normal Java class can extend only one class at a time.
An anonymous inner class can also extend only one class at a time.
Implementing Interfaces:
A normal Java class can implement any number of interfaces simultaneously.
An anonymous inner class can implement only one interface at a time.
Combining Extension and Interface Implementation:
A normal Java class can extend a class and implement any number of interfaces simultaneously.
An anonymous inner class can either extend a class or implement an interface, but not both simultaneously.
Constructors:
Anormal Java class can have multiple constructors.
Anonymous inner classes cannot have explicitly defined constructors, primarily because they don’t have a specific name. The name of the class and the constructor must match, which is not feasible for anonymous classes.
Note: If the requirement is standard and required several times, then we should go for a normal top-level class. If the requirement is temporary and required only once (for instant use), then we should go for an anonymous inner class.
Where exactly Anonymous inner classes are used?
We can use anonymous inner classes frequently in GUI-based applications to implement event handling.
Anonymous inner classes are often used in GUI-based applications to implement event handling. Event handling in GUI applications involves responding to user interactions such as button clicks, mouse movements, and keyboard inputs. Anonymous inner classes provide a concise way to define event listeners and handlers directly inline within the code, making the code more readable and reducing the need for separate classes for each event.
Java
import javax.swing.*;import java.awt.event.ActionEvent;import java.awt.event.ActionListener;publicclassMyGUIFrameextendsJFrame {privateJButtonb1, b2, b3;publicMyGUIFrame() {// Initialize components b1 = newJButton("Button 1"); b2 = newJButton("Button 2"); b3 = newJButton("Button 3");// Add buttons to the frameadd(b1);add(b2);add(b3);// Attach anonymous action listeners to buttonsb1.addActionListener(newActionListener() {publicvoidactionPerformed(ActionEvente) {// Button 1 specific functionalityJOptionPane.showMessageDialog(MyGUIFrame.this, "Button 1 clicked!"); } });b2.addActionListener(newActionListener() {publicvoidactionPerformed(ActionEvente) {// Button 2 specific functionalityJOptionPane.showMessageDialog(MyGUIFrame.this, "Button 2 clicked!"); } });b3.addActionListener(newActionListener() {publicvoidactionPerformed(ActionEvente) {// Button 3 specific functionalityJOptionPane.showMessageDialog(MyGUIFrame.this, "Button 3 clicked!"); } });// Set layout and sizesetLayout(newFlowLayout());setSize(300, 150);setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);setVisible(true); }publicstaticvoidmain(String[] args) {SwingUtilities.invokeLater(() ->newMyGUIFrame()); }}
In this example, an ActionListener is implemented as an anonymous inner class for each button (b1, b2, b3) to handle their click events. The JOptionPane is used to show a message dialog when each button is clicked. The SwingUtilities.invokeLater() is used to ensure the GUI is created on the Event Dispatch Thread.
Remember to import the necessary classes (JFrame, JButton, ActionEvent, ActionListener, JOptionPane, SwingUtilities, etc.) from the appropriate packages.
Static Nested Classes
Sometimes, we can declare an inner class with the static modifier. Such types of inner classes are called static nested classes.
In the case of a normal or regular inner class, without an existing outer class object, there is no chance of an existing inner class object. That is, the inner class object is strongly associated with the outer class object.
However, in the case of static nested classes, without an existing outer class object, there may be a chance of an existing nested class object. Hence, a static nested class object is not strongly associated with the outer class object.
If you want to create a nested class object from outside of the outer class, you can do so as follows:
Java
Outer.Nestedn = new Outer.Nested();
Static Method Declaration
In normal or regular inner classes, we can’t declare static members. However, in static nested classes, we can declare static members, including the main method. This allows us to invoke a static nested class directly from the command prompt.
Java
classTest{staticclassNested {publicstaticvoidmain(String[] args) {System.out.println("Static nested class main method"); } }publicstaticvoidmain(String[] args) {System.out.println("Outer class main method"); }}
Explanation:
The main method of the outer class (Test) will be invoked when you execute java Test.
The main method of the static nested class (Nested) will be invoked when you execute java Test$Nested because the nested class is essentially a separate class named Test$Nested.
Output:
Running java Test will output: Outer class main method
Running java Test$Nested will output: Static nested class main method
Accessing static and non-static members from outer classes
In normal or regular inner classes, we can directly access both static and non-static members of the outer class. However, in static nested classes, we can only directly access the static members of the outer class and cannot access non-static members.
Java
classTest{intx = 10;staticinty = 20;staticclassNested {publicvoidm1() {// Compilation Error: non-static variable x cannot be referenced from a static context // System.out.println(x);System.out.println(y); // valid } }}
Explanation:
You cannot directly access the non-static variable x from the static method m1() in the static nested class because the static nested class and its methods are associated with the class itself, not an instance of the outer class.
However, you can access the static variable y since static members are associated with the class itself and can be accessed from both static and non-static contexts.
Differences between normal or regular inner class and static nested class
There are several significant differences between normal or regular inner classes and static nested classes. These differences revolve around aspects such as their association with the outer class, member accessibility, and more.
Normal or Regular Inner Class:
Without an existing outer class object, there is no chance of an existing inner class object. In other words, the inner class object is strongly associated with the outer class object.
In normal or regular inner classes, we can’t declare static members.
Normal or regular inner classes cannot declare a main method, thus we cannot directly invoke the inner class from the command prompt.
From normal or regular inner classes, we can directly access both static and non-static members of the outer class.
Static Nested Classes:
Without an existing outer class object, there may be a chance of an existing static nested class object. However, the static nested class object is not strongly associated with the outer class object.
In static nested classes, we can declare static members.
In static nested classes, we can declare a main method, allowing us to invoke the nested class directly from the command prompt.
From static nested classes, we can access only the static members of the outer class.
Various combinations of nested classes and interfaces
Case 1: Class Inside a Class
When there is no possibility of one type of object existing without another type of object, we can declare a class inside another class. For instance, consider a university that consists of several departments. Without the existence of a university, the concept of a department cannot exist. Therefore, it’s appropriate to declare the ‘Department’ class within the ‘University’ class.
Java
classUniversity{classDepartment { }}
Case 2: Interface Inside a Class
When there is a need for multiple implementations of an interface within a class, and all these implementations are closely related to that particular class, defining an interface inside the class becomes advantageous. This approach helps encapsulate the interface implementations within the context of the class.
Java
classVehicleTypes{interfaceVehicle {intgetNoOfWheels(); }classBusimplementsVehicle {publicintgetNoOfWheels() {return6; } }classAutoimplementsVehicle {publicintgetNoOfWheels() {return3; } }// Other classes and implementations can follow...}
Case 3: Interface Inside an Interface
We can declare an interface inside another interface. For instance, consider a‘Map’ which is a collection of key-value pairs. Each key-value pair is referred to as an ‘Entry.’ Since the existence of an ‘Entry’ object is reliant on the presence of a ‘Map’ object, it’s logical to define the ‘Entry’ interface inside the ‘Map’ interface. This approach helps encapsulate the relationship between the two interfaces.
Java
interfaceMap{interfaceEntry {// Define methods and members for the Entry interface// ...// ...// ... }}
Any interface declared inside another interface is always implicitly public and static, regardless of whether we explicitly declare them as such. This means that we can directly implement an inner interface without necessarily implementing the outer interface. Similarly, when implementing the outer interface, there’s no requirement to also implement the inner interface. In essence, outer and inner interfaces can be implemented independently.
When a particular functionality of a class is closely associated with an interface, it is highly recommended to declare that class inside the interface. This approach helps maintain a strong relationship between the class and the interface, emphasizing the specialized functionality encapsulated within the interface.
In the given example, the EmailDetails class is specifically required only for the EmailService interface and is not used elsewhere. Thus, it’s recommended to declare the EmailDetails class inside the EmailService interface. This approach ensures that the class is tightly associated with the interface it serves.
Furthermore, class declarations inside interfaces can also be used to provide default implementations for methods defined in the interface, contributing to the interface’s flexibility and usability.
In the above example, the DefaultVehicle class serves as the default implementation of the Vehicle interface, while the Bus class provides a customized implementation of the same interface.
It’s worth noting that a class declared inside an interface is always implicitly public and static, regardless of whether we explicitly declare them as such. As a result, it’s possible to create an instance of the inner class directly without needing an instance of the outer interface.
Conclusions
1.In Java, both classes and interfaces can be declared inside each other, allowing for a flexible and versatile approach to structuring and organizing code.
Declaring a class inside a class:
Java
classA{classB { } }
Declaring an interface inside a class:
Java
classA{interfaceB { }}
Declaring an interface inside an interface:
Java
interfaceA{interfaceB { }}
Declaring a class inside an interface:
Java
interfaceA{classB { }}
2.The interface declared inside an interface is always implicitly public and static, regardless of whether we explicitly declare them as such.
Java
interfaceA {interfaceB {// You can add methods and other members here }}
3. The class which is declared inside an interface is always public and static, whether we explicitly declare it as such or not.
Java
interfaceA {classB {// You can add fields, methods, and other members here }}
4.The interface declared inside a class is always implicitly static, but it doesn’t need to be declared as public.
Java
classA {interfaceB {// You can add methods and other members here }}
Conclusion
Inner classes are a powerful and versatile feature in Java, enabling you to create complex relationships and encapsulate functionality with elegance. Whether you’re organizing code, implementing event handling, or providing default implementations, inner classes offer a rich toolkit to tackle a variety of scenarios. By understanding the types of inner classes and their benefits, you can wield this feature to enhance code readability, maintainability, and design patterns.
Let’s explore Kotlin lambda name-resolution in this comprehensive guide. Learn how Kotlin resolves names in lambda expressions, enhancing your understanding of this powerful language feature. Master the intricacies of lambda function naming for improved code clarity and functionality.
Kotlin, with its concise and expressive syntax, brings functional programming concepts to the forefront. One of its powerful features is lambda expressions, which allow you to define and pass around blocks of code as first-class citizens. However, understanding the name-resolution rules when working with Kotlin lambdas can sometimes be a bit tricky. In this blog post, we’ll dive into these rules with clear examples to help you navigate them confidently.
What exactly are name-resolution rules?
Name-resolution rules refer to the guidelines that determine how the programming language identifies and selects variables, functions, or other symbols based on their names in different contexts. In the context of programming languages like Kotlin, these rules define how the compiler or interpreter decides which variable, function, or other entities should be referred to when a particular name is used.
For example, if you have a variable named x declared in a certain scope, and you use the name x in that scope, the name-resolution rules determine whether you are referring to the local variable x or some other variable with the same name in an outer or enclosing scope.
In the context of Kotlin lambda expressions, the name-resolution rules specify how variables from the surrounding scope are captured by lambdas and how lambda parameters interact with variables of the same name in outer scopes. Understanding these rules is crucial for writing correct and maintainable code when working with lambdas and closures.
Lambda Expressions in a Nutshell
Lambda expressions in Kotlin provide a way to define small, inline functions, often used as arguments to higher-order functions or assigned to variables. The general syntax of a lambda expression is as follows:
Now, let’s explore the intricacies of name resolution within lambda expressions. Let’s go through each of the lambda name-resolution rules in Kotlin with corresponding code examples and explanations
Capturing Variables (Just a short Recap for Rule_1)
Lambdas can capture variables from their surrounding scopes. These captured variables are accessible within the lambda’s body. However, the rules for capturing variables can sometimes lead to unexpected results.
In this case, the lambda captures the reference to outsideVariable, so it prints the updated value even after the variable changes.
Example 3: Capturing Final Variables
Kotlin
funmain() {val outsideVariable = 42val lambda: () -> Unit = {println(outsideVariable) } outsideVariable = 99// Compilation error: Val cannot be reassignedlambda()}
Since outsideVariable is a final (val) variable, it cannot be reassigned, leading to a compilation error.
Rule 1: Local Scope Access
Lambdas can access variables and functions from their surrounding scope (enclosing function or block) just like regular functions.
Kotlin
funmain() {val outerValue = 42val lambda = {println(outerValue) // Can access outerValue from the enclosing scope }lambda() // Prints: 42}
Explanation:Lambda expressions can access variables from their surrounding scope just like regular functions. The lambda in this example can access the outerValue variable defined in the main function.
Shadowing Lambda Parameters
Lambda parameters can shadow variables from outer scopes. This means that if a lambda parameter has the same name as a variable in the enclosing scope, the lambda will refer to its parameter, not the outer variable.
In this example, the lambda’s parameter value shadows the outer variable value, and the lambda refers to its parameter.
Rule 2: Shadowing
If a lambda parameter or a variable inside the lambda has the same name as a variable in the enclosing scope, the lambda’s local variable shadows the outer variable. The lambda will use its own variable instead of the outer one.
Kotlin
funmain() {valvalue = 42val lambda = { value: Int ->println(value) // Refers to the parameter inside the lambda }lambda(10) // Prints: 10}
Explanation: If a lambda parameter or a variable inside the lambda has the same name as a variable in the enclosing scope, the lambda’s local variable shadows the outer variable. In this example, the lambda parameter value shadows the outer value, so the lambda prints the parameter’s value.
Qualifying Lambda Parameters
To refer to variables from the outer scope when they are shadowed by lambda parameters, you can use the label @ followed by the variable name.
By using @value, the lambda refers to the outer variable value instead of its parameter.
Rule 3: Qualified Access
You can use a qualified name to access variables from an outer scope. For example, if you have a lambda inside a class method, you can access class-level properties using this.propertyName.
Kotlin
classExample {val property = "Hello from Example"funprintProperty() {val lambda = {println(this.property) // Uses 'this' to access class-level property }lambda() // Prints: Hello from Example }}funmain() {val example = Example() example.printProperty()}
Explanation: Inside a class method, you can access class-level properties using this.property. In this example, the lambda inside the printProperty method accesses the property of the Example class using this.
Rule 4: Avoiding Variable Capture
If you want to avoid capturing variables by reference and instead capture their values, you can use the run function.
By using run, you ensure that the value of outsideVariable is captured instead of its reference.
Rule 5: Access to Receiver
In lambdas with receivers, you can directly access properties and functions of the receiver object without needing to qualify them with the receiver’s name.
Explanation: In lambdas with receivers (like the lambda passed to apply here), you can directly access properties and functions of the receiver object without needing to qualify them with the receiver’s name. The lambda modifies the StringBuilder receiver directly.
Rule 6: Closure
Lambda expressions have closure, which means they capture the variables they reference from their containing scope. These captured variables are available even if the containing scope is no longer active.
Explanation: Lambda expressions have closure, meaning they capture the variables they reference from their containing scope. In this example, the closure captures the outerValue variable from its surrounding scope and retains it even after the closureExample function has finished executing.
Rule 7: Anonymous Functions
In contrast to lambda expressions, anonymous functions don’t have implicit name-resolution rules. They behave more like regular functions in terms of scoping and access.
Kotlin
funmain() {val outerValue = 42val anonymousFunction = fun() {println(outerValue) // Can access outerValue like a regular function }anonymousFunction() // Prints: 42}
Explanation: Anonymous functions behave more like regular functions in terms of scoping and access. They don’t introduce the same implicit receiver and closure behavior that lambda expressions do.
I hope these examples help you understand how each name-resolution rule works in Kotlin lambda expressions
Conclusion
Kotlin’s lambda expressions provide a flexible and powerful way to work with functional programming concepts. Understanding the name-resolution rules, especially when capturing variables and dealing with parameter shadowing, is essential to writing clean and predictable code. By following the examples provided in this blog post, you’ll be better equipped to use lambdas effectively in your Kotlin projects. Happy coding!
In the world of mobile and embedded systems, efficient and high-performance graphics rendering is crucial to provide visually appealing and responsive user interfaces. Android, as one of the most popular mobile operating systems, employs theEGL (Embedded-system Graphics Library)to manage the interaction between the application’s graphics rendering code and the underlying hardware. In this blog, we will dive deep into the world of Android EGL, exploring its role, components, and significance in delivering a seamless graphical experience.
Introduction to Android EGL
EGL, or Embedded-system Graphics Library, is an open-standard interface for rendering graphics on embedded systems, designed to abstract the complexities of various display and rendering hardware. It acts as a bridge between the application’s rendering code and the underlying graphics hardware, enabling efficient communication and resource management.
In the context of Android, EGL is utilized for creating and managing rendering contexts, which are essential for efficient graphics operations. EGL forms a crucial part of Android’s graphics stack, working alongside other components like OpenGL ES (OpenGL for Embedded Systems) for rendering 2D and 3D graphics.
What is EGL?
EGL (Embedded-system Graphics Library) is indeed an interface that serves as a bridge between Khronos rendering APIs like OpenGL, OpenGL ES, and OpenVG, and the underlying native platform’s windowing system. Its primary purpose is to facilitate graphics context management, surface and buffer creation, binding, rendering synchronization, and to enable high-performance mixed-mode 2D and 3D rendering using other Khronos APIs.
EGL provides a standardized way for applications to interact with the graphics hardware, regardless of the specific platform or device they are running on. By abstracting the complexities of the native windowing system and hardware, EGL offers developers a consistent interface to work with graphics rendering, making it easier to create visually appealing and efficient graphics-intensive applications.
Let’s break down the key points of EGL’s role
Interface for Rendering APIs:EGL acts as a bridge between various Khronos rendering APIs and the underlying platform’s windowing system. This allows applications to seamlessly use these rendering APIs for graphics operations while abstracting the platform-specific details.
Graphics Context Management:EGL manages the graphics context, which includes the state of OpenGL, OpenGL ES, or OpenVG rendering. The context holds information about shaders, textures, buffers, and other rendering resources. Efficient context management is crucial for optimizing rendering performance.
Surface and Buffer Handling:EGL is responsible for creating and managing rendering surfaces and buffers. These surfaces can be windows, off-screen images (pixmaps), or pixel buffers. EGL provides functions to create these surfaces, bind them to rendering contexts, and handle buffer swapping for display.
Rendering Synchronization:EGL ensures proper synchronization between the rendering operations and the native windowing system. This synchronization is crucial to prevent artifacts, tearing, and other visual inconsistencies during rendering.
Mixed-Mode 2D and 3D Rendering:EGL enables the integration of different Khronos APIs, allowing applications to seamlessly combine 2D and 3D graphics rendering. This is particularly valuable for creating rich and versatile graphical experiences.
High-Performance Graphics: By abstracting hardware-specific details and optimizing resource sharing, EGL contributes to achieving high-performance graphics rendering. This is especially important for resource-constrained environments like mobile devices and embedded systems.
Why Use EGL for Graphics Rendering?
When it comes to graphics rendering using APIs like OpenGL ES or OpenVG, EGL (Embedded-system Graphics Library) steps in as a crucial facilitator. It might seem like an extra layer, but it offers several important benefits that make it an essential part of the graphics pipeline. Let’s delve into why EGL is so important:
Managing Rendering Context:Before you start drawing any graphics, you need a “context” — it’s like a container that holds all the necessary settings and states for OpenGL ES or OpenVG. EGL creates and manages this context, ensuring that your graphics rendering has the right environment to work in.
Creating Surfaces:Think of surfaces as the canvas where your graphics will be painted. EGL provides mechanisms to create these surfaces. Whether you’re rendering to a window on the screen or an off-screen image (pixmap), EGL takes care of setting up the right surface for you.
Buffer Buffet: Surfaces come with buffers — memory areas where your graphics data resides. EGL lets you specify the type of buffers you need, ensuring that your graphics commands have a place to “draw” on.
Integration with OpenGL ES and OpenVG:APIs like OpenGL ES and OpenVG are the artists that create stunning visuals. But these artists need a studio (rendering context) and a canvas (surface) to work their magic. EGL creates a seamless connection between them, making sure they understand each other’s needs and collaborate effectively.
Abstraction from Hardware:Different devices and platforms have different ways of handling graphics. EGL acts as a translator between your graphics code and the underlying hardware, ensuring your code remains consistent even if you switch devices or platforms.
Efficient Resource Management: Managing resources like memory and processing power is crucial for performance. EGL helps in the efficient sharing of resources between surfaces and contexts, making sure you get the best out of your hardware.
Mixed-mode Rendering: Sometimes, you want to combine 2D and 3D graphics. EGL enables this harmonious blending of different graphic styles, resulting in richer and more versatile visuals.
Synchronization and Display: EGL takes care of the timing — it ensures that your beautifully rendered graphics appear on the screen at the right moment, without flickering or tearing. It’s like conducting an orchestra of pixels!
Cross-Platform Compatibility:Since EGL offers a standardized way of working with rendering contexts and surfaces, you can develop graphics-intensive applications that work across different devices and platforms without rewriting your code from scratch.
In a nutshell, EGL is like the director on a movie set — it coordinates all the elements, makes sure they work together seamlessly and ensures the final product is a masterpiece. So, the next time you see stunning graphics on your favorite mobile app or game, remember that EGL played a significant role in making it look and perform so well!
EGL Provides
EGL is an interface between graphics APIs and the underlying native window system. It provides mechanisms for:
Communicating with the native windowing system: EGL provides a way for graphics APIs to interact with the native windowing system of the device. This includes creating and managing windows and rendering graphics to those windows.
Querying the available types and configurations of drawing surfaces: EGL provides a way for graphics APIs to query the available types and configurations of drawing surfaces. This information can be used to choose the best drawing surface for a particular application.
Creating drawing surfaces: EGL provides a way for graphics APIs to create drawing surfaces. Drawing surfaces are the objects that graphics APIs use to render graphics to the screen.
Synchronizing rendering between OpenGL ES 3.0 and other graphics-rendering APIs: EGL provides a way for graphics APIs to synchronize their rendering with each other. This is important for applications that use multiple graphics APIs, such as OpenGL ES and OpenVG.
Managing rendering resources such as texture maps: EGL provides a way for graphics APIs to manage rendering resources such as texture maps. This includes creating, loading, and unloading texture maps.
Key Components of EGL
Display: The display is a fundamental concept in EGL, representing the rendering target. It could be a physical screen, a frame buffer, or any other rendering surface. EGL provides functions to enumerate and select displays based on their capabilities.
Surface: A surface represents an area on the display where graphics can be drawn. EGL supports different types of surfaces, including windows, pixmaps (off-screen images), and pbuffers (pixel buffers). Applications create surfaces to render graphics onto them.
Context: The context defines the state associated with OpenGL ES rendering, including shader programs, textures, and other rendering resources. EGL contexts enable efficient sharing of resources between multiple surfaces and threads, improving performance and memory utilization.
Configurations:EGL configurations specify the attributes of the rendering surface and the capabilities required for rendering. Applications can query available configurations and choose the most suitable one based on their requirements.
Working with EGL in Android
Understanding how EGL is used within the Android ecosystem can provide insight into its significance.
Initialization:The EGL initialization process involves querying and setting up the display, choosing an appropriate configuration, and creating a rendering context. This initialization is typically done when the application starts.
Surface Creation:Applications create surfaces using EGL functions, specifying the type of surface (window, pixmap, pbuffer) and the associated attributes. These surfaces serve as the canvas for rendering graphics.
Rendering: Once the surface is created and the context is set, applications can use OpenGL ES to render graphics. EGL manages the interaction between the rendering context and the surface, ensuring that the graphics commands are properly directed.
Buffer Swapping:EGL manages the presentation of rendered content on the display. Applications use the EGL function eglSwapBuffers() to swap the front and back buffers of the rendering surface, making the newly rendered content visible to the user.
Resource Management: EGL contexts allow efficient sharing of resources between surfaces and threads. This is crucial for optimizing memory usage and rendering performance, especially in scenarios where multiple surfaces need to be rendered simultaneously.
Significance of EGL in Android
Android EGL plays a pivotal role in delivering a smooth and visually appealing user experience. Its significance can be understood through the following points:
Hardware Abstraction: EGL abstracts the underlying graphics hardware, allowing applications to target a variety of devices without needing to deal with hardware-specific intricacies.
Optimized Rendering: By managing rendering contexts and resource sharing, EGL helps in optimizing graphics rendering, leading to improved performance and responsiveness.
Multiple Surfaces: EGL enables the creation and management of multiple rendering surfaces, essential for scenarios like split-screen multitasking and concurrent rendering.
Cross-Platform Compatibility: EGL is an open standard, making it possible to port graphics-intensive applications across different platforms that support EGL, not limited to Android.
Integration with OpenGL ES: EGL seamlessly integrates with OpenGL ES, providing a comprehensive graphics solution for Android applications.
Conclusion
In the realm of mobile and embedded systems, Android EGL stands as a cornerstone for efficient graphics rendering. By abstracting the complexities of hardware and offering a standardized interface, EGL empowers developers to create visually stunning and high-performance applications. Understanding the components and workings of EGL provides developers with the tools to leverage its power effectively, delivering engaging user experiences on a wide range of Android devices. As technology continues to advance, Android EGL will undoubtedly continue to play a vital role in shaping the future of graphics rendering in the embedded system landscape.
In the fast-paced world of automotive technology, every second counts, especially when it comes to ensuring the safety of both drivers and pedestrians. One crucial component in modern vehicles is the rearview camera, which provides drivers with a clear view of what’s behind them. However, the challenge arises when the camera system needs to be up and running within mere seconds of ignition, while the Android operating system, which controls many of the vehicle’s functions, takes significantly longer to boot. In this blog, we will explore a groundbreaking solution to this problem — the Exterior View System (EVS), a self-contained application designed to minimize the delay between ignition and camera activation.
Problem
In vehicles, there is a camera located at the rear (back) of the vehicle to provide the driver with a view of what’s behind them. This camera is useful for parking, reversing, and overall safety. However, there is a requirement that this rearview camera should be able to show images on the display screen within 2 seconds of the vehicle’s ignition (engine start) being turned on.
Challenge
The challenge is that many vehicles use the Android operating system to power their infotainment systems, including the display screen where the rearview camera’s images are shown. Android, like any computer system, takes some time to start up. In this case, it takes tens of seconds (meaning around 10 or more seconds) for Android to fully boot up and become operational after the ignition is turned on.
Solution is Exterior View System (EVS)
Exterior View System (EVS): To address the problem of the slow boot time of Android and ensure that the rearview camera can show images within the required 2 seconds, a solution called the Exterior View System (EVS) is proposed.
So, What is Exterior View System (EVS)
The Exterior View System (EVS) emerges as a pioneering solution to the problem of delayed camera activation. Unlike traditional camera systems that rely heavily on the Android OS, EVS is an independent application developed in C++. This approach drastically reduces the system’s dependency on Android, allowing EVS to become operational within a mere two seconds of ignition.
The Exterior View System (EVS) in Android Automotive is a hardware abstraction layer (HAL) that provides support for rearview and surround view cameras in vehicles. EVS enables OEMs to develop and deploy advanced driver assistance systems (ADAS) and other safety features that rely on multiple camera views.
The EVS HAL consists of a number of components, including:
A camera manager that provides access to the vehicle’s cameras
A display manager that controls the output of the camera streams
A frame buffer manager that manages the memory used to store camera frames
A sensor fusion module that combines data from multiple cameras to create a single, unified view of the vehicle’s surroundings.
EVS is a key component of Android Automotive’s ADAS and safety features. It enables vehicles to provide drivers with a comprehensive view of their surroundings, which can help to prevent accidents.
Here are some of the benefits of using EVS in Android Automotive:
Improved safety:EVS can help to prevent accidents by providing drivers with a comprehensive view of their surroundings. This is especially helpful in low-visibility conditions, such as at night or in bad weather.
Advanced driver assistance features: EVS can be used to power advanced driver assistance features, such as lane departure warning, blind spot monitoring, and parking assist. These features can help to make driving safer and more convenient.
Enhanced user experience:EVS can be used to enhance the user experience of Android Automotive by providing drivers with a more immersive view of their surroundings. This can be helpful for navigation, entertainment, and other tasks.
If you are looking for a safe and advanced driving experience, then Android Automotive with EVS is a great option.
Here are some examples of how EVS can be used in Android Automotive:
Rearview camera:A rearview camera can be used to provide drivers with a view of the area behind their vehicle. This can be helpful for backing up and parking.
Sideview cameras:Sideview cameras can be used to provide drivers with a view of the area to the sides of their vehicle. This can be helpful for changing lanes and avoiding obstacles.
Surround-view cameras:Surround-view cameras can be used to provide drivers with a 360-degree view of the area around their vehicle. This can be helpful for parking in tight spaces and maneuvering in difficult conditions.
Lane departure warning: Lane departure warning uses EVS to detect when the vehicle is drifting out of its lane. If the vehicle starts to drift, the system will alert the driver and may even apply the brakes to help keep the vehicle in its lane.
Blind spot monitoring:Blind spot monitoring uses EVS to detect vehicles in the driver’s blind spots. If a vehicle is detected in the blind spot, the system will alert the driver with a visual or audible warning.
Parking assist: Parking assist uses EVS to help drivers park their vehicles. The system will provide guidance on how to steer and brake, and it may even automatically control the steering wheel and brakes.
These are just a few examples of how EVS can be used in Android Automotive. As the technology continues to develop, we can expect to see even more innovative and advanced uses for EVS in the future.
BTW, Why EVS is introduced?
The introduction of the Exterior View System (EVS) serves several key purposes, each of which contributes to its significance:
SIMPLE: Support camera and view display with a simplified design
EVS is designed to provide a straightforward and uncomplicated way to manage camera input and display views. Its primary goal is to make it easy for developers to work with cameras and show what they capture on the screen. By offering a simplified design, EVS reduces complexity, making it more efficient to integrate camera functionality into applications.
EARLY: Intend to show display very early in the Android boot process
One of the primary motivations behind EVS is to ensure that camera views can be displayed as quickly as possible after the ignition of the vehicle. Traditional Android boot times can be relatively long, potentially delaying the display of camera feeds. EVS addresses this issue by functioning independently of the Android operating system and initiating the camera display within just a few seconds of starting the vehicle. This capability enhances user experience by providing prompt access to crucial camera information.
EXTENSIBLE: Enables advanced features to be implemented in user apps
EVS is designed with extensibility in mind, allowing developers to implement advanced features and functionalities within their applications. By providing a framework that can be built upon, EVS empowers app creators to integrate innovative and sophisticated camera-related features, enhancing the overall capabilities of their applications. This extensibility promotes creativity and enables the development of unique and tailored user experiences.
Overall, the introduction of EVS is driven by the desire to simplify camera and view display functionality, ensure early access to camera views during the Android boot process, and provide a platform for the implementation of advanced and customizable features within user applications. This approach aims to enhance the efficiency, responsiveness, and versatility of camera-related functionalities in the context of Android-based systems.
EVS stack in Android
The EVS stack in Android consists of three main components that work together to facilitate the functioning of the Exterior View System:
EVS Stack in Android
EVS Application
The EVS application is composed of native code and is initiated by the init.rc (initialization script) during the system startup process. This application runs in the background even when it’s not actively being used by a user. Its primary purpose is to manage the processing and display of exterior camera views.
EVS Manager
The EVS Manager acts as an intermediary layer that connects the EVS application with the Hardware Abstraction Layer (HAL) and the user-facing applications. It essentially serves as a wrapper, facilitating communication and data exchange between these different components. Importantly, the EVS Manager can handle multiple concurrent clients, meaning that it can manage requests from multiple user applications that want to access and display camera views simultaneously.
EVS HAL (Hardware Abstraction Layer)
The EVS HAL is a crucial component that interacts with the underlying hardware and interfaces with the SurfaceFlinger module, which is responsible for rendering graphics on the screen. The EVS HAL is designed to be hardware-independent, meaning it can work with various types of hardware configurations. It plays a vital role in capturing camera data, processing it, and delivering it to the EVS Manager for further distribution to user applications.
Overall, the EVS stack in Android is structured to ensure efficient communication between the EVS application, the EVS Manager, and the EVS HAL. This stack enables the seamless management of exterior camera views, from capturing the data to processing it and finally displaying it to users through various applications.
Architecture
The Exterior View System’s architecture is designed to maximize efficiency and speed while maintaining a seamless user experience. The following system components are present in the EVS architecture:
EVS System components overview
EVS Application
There’s an EVS application example written in C++ that you can find at /packages/services/Car/evs/app. This example shows you how to use EVS. The job of this application is to ask the EVS Manager for video frames and then send these frames to the EVS Manager so they can be shown on the screen. It’s designed to start up as soon as the EVS and Car Service are ready, usually within two seconds after the car turns on. Car makers can change or use a different EVS application if they want to.
EVS Manager
The EVS Manager, located at /packages/services/Car/evs/manager, is like a toolbox for EVS applications. It helps these applications create different things, like showing a basic rearview camera view or even a complex 6DOF(Six degrees of freedom (6DOF) refers to the specific number of axes that a rigid body is able to freely move in three-dimensional space.) multi-camera 3D view. It talks to the applications through HIDL, a special communication way in Android. It can work with many applications at the same time.
Other programs, like the Car Service, can also talk to the EVS Manager. They can ask the EVS Manager if the EVS system is up and running or not. This helps them know when the EVS system is working.
EVS HIDL interface
The EVS HIDL interface is how the EVS system’s camera and display parts talk to each other. You can find this interface in the android.hardware.automotive.evs package. There’s an example version of it in /hardware/interfaces/automotive/evs/1.0/default that you can use to test things out. This example makes fake images and checks if they work properly.
The car maker (OEM) needs to make the actual code for this interface. The code is based on the .hal files in /hardware/interfaces/automotive/evs. This code sets up the real cameras, gets their data, and puts it in special memory areas that Gralloc (Gralloc is a type of shared memory that is also shared with the GPU)understands. The display part of the code has to make a memory area where the app can put its images (usually using something called EGL), and then it shows these images on the car screen. This display part is important because it makes sure the app’s images are shown instead of anything else on the screen. Car makers can put their own version of the EVS code in different places, like /vendor/… /device/… or hardware/… (for example, /hardware/[vendor]/[platform]/evs).
Kernel drivers
For a device to work with the EVS system, it needs special software called kernel drivers. If a device already has drivers for its camera and display, those drivers can often be used for EVS too. This can be helpful, especially for display drivers, because showing images might need to work together with other things happening in the device.
In Android 8.0, there’s an example driver based on something called v4l2 (you can find it in packages/services/Car/evs/sampleDriver). This driver uses the kernel for v4l2 support (a way to handle video) and uses something called SurfaceFlinger to show images.
It’s important to note that the sample driver uses SurfaceFlinger, which isn’t suitable for a real device because EVS needs to start quickly, even before SurfaceFlinger is fully ready. However, the sample driver is designed to work with different hardware and lets developers test and work on EVS applications at the same time as they develop EVS drivers.
EVS hardware interface description
In this section, we explain the Hardware Abstraction Layer (HAL) for the EVS (Exterior View System) in Android. Manufacturers need to create implementations of this HAL to match their hardware.
IEvsEnumerator
This object helps find available EVS hardware (cameras and the display) in the system.
getCameraList():Gets a list of all available cameras.
openCamera(string camera_id):Opens a specific camera for interaction.
closeCamera(IEvsCamera camera): Closes a camera.
openDisplay():Opens the EVS display.
closeDisplay(IEvsDisplay display):Closes the display.
getDisplayState():Gets the current display state.
IEvsCamera
This object represents a single camera and is the main interface for capturing images.
getCameraInfo():Gets information about the camera.
setMaxFramesInFlight(int32 bufferCount): Sets the maximum number of frames the camera can hold.
startVideoStream(IEvsCameraStream receiver):Starts receiving camera frames.
doneWithFrame(BufferDesc buffer):Signals that a frame is done being used.
It’s important to note that these interfaces help EVS applications communicate with the hardware and manage camera and display functionality. Manufacturers can customize these implementations to match their specific hardware features and capabilities.
IEvsCameraStream
The client uses this interface to receive video frames asynchronously.
deliverFrame(BufferDesc buffer):Called by the HAL whenever a video frame is ready. The client must return buffer handles using IEvsCamera::doneWithFrame(). When the video stream stops, this callback might continue as the pipeline drains. When the last frame is delivered, a NULL bufferHandle is sent, indicating the end of the stream. The NULL bufferHandle doesn’t need to be sent back using doneWithFrame(), but all other handles must be returned.
IEvsDisplay
This object represents the EVS display, controls its state, and handles image presentation.
getDisplayInfo():Gets basic information about the EVS display.
setDisplayState(DisplayState state): Sets the display state.
getDisplayState(): Gets the current display state.
getTargetBuffer():Gets a buffer handle associated with the display.
returnTargetBufferForDisplay(handle bufferHandle): Informs the display that a buffer is ready for display.
DisplayDesc
Describes the basic properties of an EVS display.
display_id:Unique identifier for the display.
vendor_flags:Additional information for a custom EVS Application.
DisplayState
Describes the state of the EVS display.
NOT_OPEN: Display has not been opened.
NOT_VISIBLE:Display is inhibited.
VISIBLE_ON_NEXT_FRAME:Will become visible with the next frame.
VISIBLE: Display is currently active.
DEAD:Display is not available, and the interface should be closed.
The IEvsCameraStream interface allows the client to receive video frames from the camera, while the IEvsDisplay interface manages the state and presentation of images on the EVS display. These interfaces help coordinate the communication between the EVS hardware and the application, ensuring smooth and synchronized operation.
EVS Manager
The EVS Manager is a component that acts as an intermediary between applications and the EVS Hardware API, which handles external camera views. The Manager provides shared access to cameras, allowing multiple applications to use camera streams concurrently. A primary EVS application is the main client of the Manager, with exclusive display access. Other clients can have read-only access to camera images.
EVS Manager mirrors underlying EVS Hardware API
The EVS Manager offers the same API as the EVS Hardware drivers, except that the EVS Manager API allows concurrent camera stream access. The EVS Manager is, itself, the one allowed client of the EVS Hardware HAL layer, and acts as a proxy for the EVS Hardware HAL.
IEvsEnumerator
openCamera(string camera_id):Obtains an interface to interact with a specific camera. Multiple processes can open the same camera for video streaming.
IEvsCamera
startVideoStream(IEvsCameraStream receiver):Starts video streams independently for different clients. The camera starts when the first client begins.
doneWithFrame(uint32 frameId, handle bufferHandle): Returns a frame when a client is done with it. Other clients continue to receive all frames.
stopVideoStream(): Stops a video stream for a client, without affecting other clients.
setExtendedInfo(int32 opaqueIdentifier, int32 opaqueValue): Allows one client to affect another by sending driver-specific values.
IEvsDisplay
The EVS Manager passes the IEvsDisplay interface directly to the underlying HAL implementation.
In essence, the EVS Manager acts as a bridge, enabling multiple clients to utilize the EVS system simultaneously, while maintaining independent access to cameras. It provides flexibility and concurrent access to camera streams, enhancing the overall functionality of the EVS system.
Typical control flow
The EVS application in Android is a C++ program that interacts with the EVS Manager and Vehicle HAL to offer basic rearview camera functionality. It’s meant to start early in the system boot process and can show appropriate video based on available cameras and the car’s state (gear, turn signal). Manufacturers can customize or replace this application with their own logic and visuals.
EVS application sample logic, get camera list.
Since image data is provided in a standard graphics buffer, the application needs to move the image from the source buffer to the output buffer. This involves a data copy, but it also gives the app the flexibility to manipulate the image before displaying it.
For instance, the app could move pixel data while adding scaling or rotation. Alternatively, it could use the source image as an OpenGL texture and render a complex scene onto the output buffer, including virtual elements like icons, guidelines, and animations. More advanced applications might even combine multiple camera inputs into a single output frame for a top-down view of the vehicle surroundings.
Overall, the EVS application provides the essential connection between hardware and user presentation, allowing manufacturers to create custom and sophisticated visual experiences based on their specific vehicle designs and features.
Boot Sequence Diagram
The boot sequence diagram outlines the steps involved in the initialization and operation of the Exterior View System (EVS) within the context of an Android-based system:
Communication with EVS Manager and Vehicle HAL
The process begins by establishing communication between the EVS Application and both the EVS Manager and the Vehicle HAL (Hardware Abstraction Layer). This communication enables the EVS Application to exchange information and commands with these two key components.
Infinite Loop for Monitoring Camera and Gear/Turn Signal State
Once communication is established, the EVS Application enters an infinite loop. This loop serves as the core operational mechanism of the system. Within this loop, the EVS Application constantly monitors two critical inputs: the camera state and the state of the vehicle’s gear or turn signals. These inputs help determine what needs to be displayed to the user.
Reaction to Camera and Vehicle State
Based on the monitored inputs, the EVS Application reacts accordingly. If the camera state changes (e.g., a new camera feed is available), the EVS Application processes the camera data. Similarly, if there’s a change in the gear or turn signal state, the system responds by updating the displayed content to provide relevant information to the driver.
Use of Source Image as OpenGL Texture and Rendering a Complex Scene
The EVS Application utilizes the source image from the camera feed as an OpenGL texture. OpenGL is a graphics rendering technology that enables the creation of complex visual scenes. The EVS Application takes advantage of this capability to render a sophisticated and informative scene. This scene, which includes data from the camera feed and potentially other elements, is then composed and prepared for display.
Rendering to the Output Buffer
The rendered scene is finally placed into the output buffer, which is essentially a designated area of memory used for displaying content on the screen. This process ensures that the composed scene, which combines the camera feed and other relevant information, is ready for presentation to the user.
In essence, the boot sequence diagram illustrates how the EVS Application interacts with the EVS Manager, the Vehicle HAL, and the hardware to continuously monitor camera and vehicle states, react to changes, create a visually informative scene, and render that scene for display on the screen. This orchestration ensures that the driver receives real-time and relevant exterior view information during the operation of the vehicle.
Boot Time Evaluation
The evaluation of boot time for the application involves ensuring that it is initiated promptly by the system’s initialization process. Specifically, the goal is for the application to start running as soon as the EVS Manager and the Vehicle HAL become available. This initiation is targeted to occur within a time frame of 2.0 seconds from the moment the power is turned on.
In simpler terms, the aim is to have the application up and running very quickly after the vehicle is powered on. This swift start time helps ensure that the Exterior View System (EVS) becomes operational without unnecessary delays, allowing the system to provide timely and accurate information to the user based on the exterior camera feeds and other relevant data.
Measured from Android first stage init
The evaluation of boot time is measured from the initial stage of the Android system’s initialization process. This means that the time it takes for the system to fully start up and become operational is calculated starting from the very beginning of the boot process. This measurement includes all the necessary tasks and processes that occur during the system’s startup sequence, such as loading essential components, initializing hardware, and launching applications.
In essence, boot time evaluation from Android first stage init provides a comprehensive view of the time it takes for the entire system to transition from a powered-off state to a fully functional state, including the initiation and execution of various components like the Exterior View System (EVS) application, EVS Manager, Vehicle HAL, and other crucial elements. The goal is to optimize and minimize this boot time to ensure efficient and timely access to the system’s functionalities and services.
Quick Boot Optimization
Here’s an improved version with three steps to enhance the boot time of the system and the operation of the Exterior View System (EVS):
EVS App: Concurrent Camera Stream and GL Preparation
EVS App: Start Camera Stream with GL preparing concurrently
The EVS Application can be optimized to start the camera stream and simultaneously prepare the OpenGL (GL) rendering. By executing these tasks concurrently, the system can make more efficient use of available resources, reducing overall initialization time. This means that as the camera stream begins, the OpenGL components responsible for rendering the visuals are already being prepared, allowing for a smoother and quicker transition to displaying the camera views.
EVS HAL: Display Frames via Composer Service Early
EVS HAL: Display frames via composer service before SufaceFlinger is ready
The EVS Hardware Abstraction Layer (HAL) can be enhanced to leverage the composer service to display frames even before SurfaceFlinger is fully ready. By doing so, the system can start showing visual content sooner, improving the responsiveness of the user interface. This approach allows for an early display of camera frames and other graphics, enhancing the user experience by reducing any perceptible delay in visual feedback.
Android Init: Start EVS Services/HALs on Early-Init
Android Init: Start EVS related services/HALs earlier (on boot -> on early-init)
To further expedite the boot process and enable faster access to EVS-related functionalities, consider moving the initialization of EVS services and HALs to the “early-init” phase of the Android boot sequence. This adjustment ensures that essential EVS components are initiated earlier in the startup process, reducing the overall time it takes for the system to become fully operational. Starting EVS-related services and HALs at an earlier stage streamlines the boot process, making the EVS capabilities available to users more quickly after powering on the device.
By implementing these three steps, the boot time of the system can be significantly improved, and the operation of the Exterior View System can become more seamless and responsive, enhancing the overall user experience.
Optimization Breakdown
The optimization efforts yield significant improvements in the launch time of the Exterior View System (EVS) and the overall system boot time. Here’s a breakdown of the results:
Optimized EVS Launch Time
By implementing the proposed optimizations, the EVS launch time has been reduced to 1.1 seconds from the Android first stage initialization. This signifies a substantial improvement in getting the EVS system up and running promptly after the Android boot process starts.
Total System Boot Time
The total time required for the entire system to boot up, including bootloader and kernel time, has been reduced to approximately 3.0 seconds. This represents an impressive reduction in the time it takes for the system to become fully operational and ready for use.
Additionally, there’s an alternative scenario to consider:
Reduced Texture Operations for Faster EVS Launch
If the GL (OpenGL) preparation and texture operations are removed from the EVS App, the launch time of EVS can be further decreased to 0.7 seconds. This change has a notable impact on getting the EVS system up and running even more swiftly after the Android boot process initiates.
Total Boot Time with Reduced Texture Operations
With the removal of texture operations, the total system boot time is reduced to approximately 2.6 seconds. This achievement demonstrates an even more streamlined boot process for the entire system.
Overall, the optimization efforts, along with the option to remove texture operations from the EVS App, have led to significant improvements in both the launch time of the EVS system and the overall boot time of the entire system on your hardware development board. These enhancements contribute to a more responsive and efficient user experience, allowing users to access and utilize the EVS functionality more quickly and effectively.
Display Sharing — EVS Priority and Mechanism
The integration of exterior cameras in vehicles has transformed the way drivers navigate their surroundings. From parallel parking to navigating tight spaces, these cameras offer valuable assistance. However, the challenge arises when determining how to seamlessly switch between the main display, which often serves multiple functions, and the exterior view provided by EVS. The solution lies in prioritizing EVS for display sharing.
EVS Priority over Main Display
The EVS application is designed to have priority over the main display. This means that when certain conditions are met, EVS can take control of the main display to show its content. The main display is the screen usually used for various functions, like entertainment, navigation, and other infotainment features.
Grabbing the Display
Whenever there’s a need to display images from an exterior camera (such as the rearview camera), the EVS application can “grab” or take control of the main display. This allows the camera images to be shown prominently to the driver, providing important visual information about the vehicle’s surroundings.
Example Scenario — Reverse Gear
One specific scenario where this display-sharing mechanism is used is when the vehicle’s reverse gear is selected. When the driver shifts the transmission into reverse, the EVS application can immediately take control of the main display to show the live feed from the rearview camera. This is crucial for assisting the driver in safely maneuvering the vehicle while reversing.
No Simultaneous Content Display
Importantly, there is no mechanism in place to allow both the EVS application and the Android operating system to display content simultaneously on the main display. In other words, only one of them can be active and show content at any given time.
In short, the concept of display sharing in this context involves the Exterior View System (EVS) having priority over the main display in the vehicle. EVS can take control of the main display whenever there’s a need to show images from an exterior camera, such as the rearview camera. This mechanism ensures that the driver receives timely and relevant visual information for safe driving. Additionally, it’s important to note that only one of the applications (EVS or Android) can display content on the main screen at a time; they do not operate simultaneously.
Conclusion
The Exterior View System (EVS) stands as a remarkable advancement in automotive technology, addressing the critical issue of swift camera activation during vehicle ignition. By employing a self-contained application with minimal dependencies on the Android operating system, EVS ensures that drivers have access to real-time camera images within a mere two seconds of starting the ignition. This breakthrough architecture, prioritized display sharing, and synchronized activation make EVS a game-changer in enhancing road safety and driver convenience.
As the automotive industry continues to evolve, innovations like the Exterior View System pave the way for a safer and more efficient driving experience. With EVS leading the charge, we can look forward to a future where technology seamlessly integrates with our everyday journeys, ensuring a smoother and more secure ride for all.
In the era of advanced driver assistance systems (ADAS) and autonomous vehicles, the integration of various sensors and cameras is crucial for ensuring safety and enhancing the overall driving experience. One integral component of this integration is the Vehicle Camera Hardware Abstraction Layer (HAL), a vital software framework that bridges the gap between hardware and software, enabling efficient communication and control over vehicle cameras. In this blog post, we will dive deep into the concept of Vehicle Camera HAL, its importance, architecture, and its role in shaping the future of automotive technology.
Vehicle Camera HAL
Android has a special part called the automotive HIDL Hardware Abstraction Layer (HAL), which helps with capturing and showing images right when Android starts up in cars. This part keeps working as long as the car system is on. It has something called the exterior view system (EVS) stack, which is like a set of tools to handle what the car’s cameras see. This is usually used for things like showing the rearview camera or a view of all around the car on the screen in cars with Android-based screens. The EVS system also helps to add fancy features to apps.
Android also has a special way for the EVS part to talk to the camera and screen (you can find it in /hardware/interfaces/automotive/evs/1.0). While you could make a rearview camera app using the normal Android camera and screen stuff, it might start too late when Android starts up. But using this special way (the dedicated HAL) makes it smoother and easier for the car maker to add the EVS system.
Architecture
The Exterior View System’s architecture is designed to maximize efficiency and speed while maintaining a seamless user experience. The following system components are present in the EVS architecture:
EVS System components overview
EVS Application
There’s an EVS application example written in C++ that you can find at /packages/services/Car/evs/app. This example shows you how to use EVS. The job of this application is to ask the EVS Manager for video frames and then send these frames to the EVS Manager so they can be shown on the screen. It’s designed to start up as soon as the EVS and Car Service are ready, usually within two seconds after the car turns on. Car makers can change or use a different EVS application if they want to.
EVS Manager
The EVS Manager, located at /packages/services/Car/evs/manager, is like a toolbox for EVS applications. It helps these applications create different things, like showing a basic rearview camera view or even a complex 6DOF(Six degrees of freedom (6DOF) refers to the specific number of axes that a rigid body is able to freely move in three-dimensional space.) multi-camera 3D view. It talks to the applications through HIDL, a special communication way in Android. It can work with many applications at the same time.
Other programs, like the Car Service, can also talk to the EVS Manager. They can ask the EVS Manager if the EVS system is up and running or not. This helps them know when the EVS system is working.
EVS HIDL interface
The EVS HIDL interface is how the EVS system’s camera and display parts talk to each other. You can find this interface in the android.hardware.automotive.evs package. There’s an example version of it in /hardware/interfaces/automotive/evs/1.0/default that you can use to test things out. This example makes fake images and checks if they work properly.
The car maker (OEM) needs to make the actual code for this interface. The code is based on the .hal files in /hardware/interfaces/automotive/evs. This code sets up the real cameras, gets their data, and puts it in special memory areas that Gralloc (Gralloc is a type of shared memory that is also shared with the GPU) understands. The display part of the code has to make a memory area where the app can put its images (usually using something called EGL), and then it shows these images on the car screen. This display part is important because it makes sure the app’s images are shown instead of anything else on the screen. Car makers can put their own version of the EVS code in different places, like /vendor/… /device/… or hardware/… (for example, /hardware/[vendor]/[platform]/evs).
Kernel drivers
For a device to work with the EVS system, it needs special software called kernel drivers. If a device already has drivers for its camera and display, those drivers can often be used for EVS too. This can be helpful, especially for display drivers, because showing images might need to work together with other things happening in the device.
In Android 8.0, there’s an example driver based on something called v4l2 (you can find it in packages/services/Car/evs/sampleDriver). This driver uses the kernel for v4l2support (a way to handle video) and uses something called SurfaceFlinger to show images.
It’s important to note that the sample driver uses SurfaceFlinger, which isn’t suitable for a real device because EVS needs to start quickly, even before SurfaceFlinger is fully ready. However, the sample driver is designed to work with different hardware and lets developers test and work on EVS applications at the same time as they develop EVS drivers.
EVS hardware interface description
In this section, we explain the Hardware Abstraction Layer (HAL) for the EVS (Exterior View System) in Android. Manufacturers need to create implementations of this HAL to match their hardware.
IEvsEnumerator
This object helps find available EVS hardware (cameras and the display) in the system.
getCameraList():Gets a list of all available cameras.
openCamera(string camera_id):Opens a specific camera for interaction.
closeCamera(IEvsCamera camera): Closes a camera.
openDisplay():Opens the EVS display.
closeDisplay(IEvsDisplay display):Closes the display.
getDisplayState():Gets the current display state.
IEvsCamera
This object represents a single camera and is the main interface for capturing images.
getCameraInfo():Gets information about the camera.
setMaxFramesInFlight(int32 bufferCount): Sets the maximum number of frames the camera can hold.
startVideoStream(IEvsCameraStream receiver):Starts receiving camera frames.
doneWithFrame(BufferDesc buffer):Signals that a frame is done being used.
It’s important to note that these interfaces help EVS applications communicate with the hardware and manage camera and display functionality. Manufacturers can customize these implementations to match their specific hardware features and capabilities.
IEvsCameraStream
The client uses this interface to receive video frames asynchronously.
deliverFrame(BufferDesc buffer):Called by the HAL whenever a video frame is ready. The client must return buffer handles using IEvsCamera::doneWithFrame(). When the video stream stops, this callback might continue as the pipeline drains. When the last frame is delivered, a NULL bufferHandle is sent, indicating the end of the stream. The NULL bufferHandle doesn\’t need to be sent back using doneWithFrame(), but all other handles must be returned.
IEvsDisplay
This object represents the EVS display, controls its state, and handles image presentation.
getDisplayInfo():Gets basic information about the EVS display.
setDisplayState(DisplayState state): Sets the display state.
getDisplayState(): Gets the current display state.
getTargetBuffer():Gets a buffer handle associated with the display.
returnTargetBufferForDisplay(handle bufferHandle): Informs the display that a buffer is ready for display.
DisplayDesc
Describes the basic properties of an EVS display.
display_id:Unique identifier for the display.
vendor_flags:Additional information for a custom EVS Application.
DisplayState
Describes the state of the EVS display.
NOT_OPEN: Display has not been opened.
NOT_VISIBLE:Display is inhibited.
VISIBLE_ON_NEXT_FRAME:Will become visible with the next frame.
VISIBLE: Display is currently active.
DEAD:Display is not available, and the interface should be closed.
The IEvsCameraStream interface allows the client to receive video frames from the camera, while the IEvsDisplay interface manages the state and presentation of images on the EVS display. These interfaces help coordinate the communication between the EVS hardware and the application, ensuring smooth and synchronized operation.
EVS Manager
The EVS Manager is a component that acts as an intermediary between applications and the EVS Hardware API, which handles external camera views. The Manager provides shared access to cameras, allowing multiple applications to use camera streams concurrently. A primary EVS application is the main client of the Manager, with exclusive display access. Other clients can have read-only access to camera images.
EVS Manager mirrors underlying EVS Hardware API
The EVS Manager offers the same API as the EVS Hardware drivers, except that the EVS Manager API allows concurrent camera stream access. The EVS Manager is, itself, the one allowed client of the EVS Hardware HAL layer, and acts as a proxy for the EVS Hardware HAL.
IEvsEnumerator
openCamera(string camera_id):Obtains an interface to interact with a specific camera. Multiple processes can open the same camera for video streaming.
IEvsCamera
startVideoStream(IEvsCameraStream receiver):Starts video streams independently for different clients. The camera starts when the first client begins.
doneWithFrame(uint32 frameId, handle bufferHandle): Returns a frame when a client is done with it. Other clients continue to receive all frames.
stopVideoStream(): Stops a video stream for a client, without affecting other clients.
setExtendedInfo(int32 opaqueIdentifier, int32 opaqueValue): Allows one client to affect another by sending driver-specific values.
IEvsDisplay
The EVS Manager passes the IEvsDisplay interface directly to the underlying HAL implementation.
In essence, the EVS Manager acts as a bridge, enabling multiple clients to utilize the EVS system simultaneously, while maintaining independent access to cameras. It provides flexibility and concurrent access to camera streams, enhancing the overall functionality of the EVS system.
EVS application
The EVS application in Android is a C++ program that interacts with the EVS Manager and Vehicle HAL to offer basic rearview camera functionality. It’s meant to start early in the system boot process and can show appropriate video based on available cameras and the car’s state (gear, turn signal). Manufacturers can customize or replace this application with their own logic and visuals.
EVS application sample logic, get camera list.
Since image data is provided in a standard graphics buffer, the application needs to move the image from the source buffer to the output buffer. This involves a data copy, but it also gives the app the flexibility to manipulate the image before displaying it.
For instance, the app could move pixel data while adding scaling or rotation. Alternatively, it could use the source image as an OpenGL texture and render a complex scene onto the output buffer, including virtual elements like icons, guidelines, and animations. More advanced applications might even combine multiple camera inputs into a single output frame for a top-down view of the vehicle surroundings.
Overall, the EVS application provides the essential connection between hardware and user presentation, allowing manufacturers to create custom and sophisticated visual experiences based on their specific vehicle designs and features.
Boot Sequence Diagram
The boot sequence diagram outlines the steps involved in the initialization and operation of the Exterior View System (EVS) within the context of an Android-based system:
Communication with EVS Manager and Vehicle HAL
The process begins by establishing communication between the EVS Application and both the EVS Manager and the Vehicle HAL (Hardware Abstraction Layer). This communication enables the EVS Application to exchange information and commands with these two key components.
Infinite Loop for Monitoring Camera and Gear/Turn Signal State
Once communication is established, the EVS Application enters an infinite loop. This loop serves as the core operational mechanism of the system. Within this loop, the EVS Application constantly monitors two critical inputs: the camera state and the state of the vehicle’s gear or turn signals. These inputs help determine what needs to be displayed to the user.
Reaction to Camera and Vehicle State
Based on the monitored inputs, the EVS Application reacts accordingly. If the camera state changes (e.g., a new camera feed is available), the EVS Application processes the camera data. Similarly, if there’s a change in the gear or turn signal state, the system responds by updating the displayed content to provide relevant information to the driver.
Use of Source Image as OpenGL Texture and Rendering a Complex Scene
The EVS Application utilizes the source image from the camera feed as an OpenGL texture. OpenGL is a graphics rendering technology that enables the creation of complex visual scenes. The EVS Application takes advantage of this capability to render a sophisticated and informative scene. This scene, which includes data from the camera feed and potentially other elements, is then composed and prepared for display.
Rendering to the Output Buffer
The rendered scene is finally placed into the output buffer, which is essentially a designated area of memory used for displaying content on the screen. This process ensures that the composed scene, which combines the camera feed and other relevant information, is ready for presentation to the user.
In essence, the boot sequence diagram illustrates how the EVS Application interacts with the EVS Manager, the Vehicle HAL, and the hardware to continuously monitor camera and vehicle states, react to changes, create a visually informative scene, and render that scene for display on the screen. This orchestration ensures that the driver receives real-time and relevant exterior view information during the operation of the vehicle.
Use the EGL/SurfaceFlinger in the EVS Display HAL
This section provides instructions on how to use the EGL/SurfaceFlinger in the EVS Display HAL implementation for Android 10. It includes details on building libgui for vendor processes, using binder in an EVS HAL implementation, SELinux policies, and building the EVS HAL reference implementation as a vendor process.
Building libgui for Vendor Processes
The libgui library is required to use EGL/SurfaceFlinger in EVS Display HAL implementations. To build libgui for vendor processes, create a new target in the build script that is identical to libgui but with some modifications by addition of two these fields:
For Android 8 (and higher), /dev/binder became exclusive to framework processes. Vendor processes should use /dev/hwbinder and convert AIDL interfaces to HIDL. You can use /dev/vndbinder to continue using AIDL interfaces between vendor processes.
Update your EVS HAL implementation to use /dev/binder for SurfaceFlinger:
#include <binder/ProcessState.h>
int main() { // ...
// Use /dev/binder for SurfaceFlinger ProcessState::initWithDriver(\"/dev/binder\");
// ... }
SELinux Policies
Depending on your device’s implementation, SELinux policies may prevent vendor processes from using /dev/binder. You can modify SELinux policies to allow access to /dev/binder for your EVS HAL implementation:
# Allow to use /dev/binder typeattribute hal_evs_driver binder_in_vendor_violators;
# Allow the driver to use the binder device allow hal_evs_driver binder_device:chr_file rw_file_perms;
Building EVS HAL Reference Implementation as a Vendor Process
Modify your Android.mk file(packages/services/Car/evs/Android.mk) for the EVS HAL reference implementation to include libgui_vendor and set LOCAL_PROPRIETARY_MODULE to true:
# NOTE: It can be helpful, while debugging, to disable optimizations #LOCAL_CFLAGS += -O0 -g diff --git a/evs/sampleDriver/service.cpp b/evs/sampleDriver/service.cpp index d8fb31669..5fd029358 100644 --- a/evs/sampleDriver/service.cpp +++ b/evs/sampleDriver/service.cpp @@ -21,6 +21,7 @@ #include <utils/Errors.h> #include <utils/StrongPointer.h> #include <utils/Log.h> +#include <binder/ProcessState.h>
#include \"ServiceNames.h\" #include \"EvsEnumerator.h\" @@ -43,6 +44,9 @@ using namespace android; int main() { ALOGI(\"EVS Hardware Enumerator service is starting\"); + // Use /dev/binder for SurfaceFlinger + ProcessState::initWithDriver(\"/dev/binder\"); + // Start a thread to listen video device addition events. std::atomic<bool> running { true }; std::thread ueventHandler(EvsEnumerator::EvsUeventThread, std::ref(running)); diff --git a/evs/sepolicy/evs_driver.te b/evs/sepolicy/evs_driver.te index f1f31e9fc..632fc7337 100644 --- a/evs/sepolicy/evs_driver.te +++ b/evs/sepolicy/evs_driver.te @@ -3,6 +3,9 @@ type hal_evs_driver, domain, coredomain; hal_server_domain(hal_evs_driver, hal_evs) hal_client_domain(hal_evs_driver, hal_evs)
+# allow to use /dev/binder +typeattribute hal_evs_driver binder_in_vendor_violators; + # allow init to launch processes in this context type hal_evs_driver_exec, exec_type, file_type, system_file_type; init_daemon_domain(hal_evs_driver) @@ -22,3 +25,7 @@ allow hal_evs_driver ion_device:chr_file r_file_perms;
# Allow the driver to access kobject uevents allow hal_evs_driver self:netlink_kobject_uevent_socket create_socket_perms_no_ioctl; + +# Allow the driver to use the binder device +allow hal_evs_driver binder_device:chr_file rw_file_perms;
These instructions provide a step-by-step guide to incorporate EGL/SurfaceFlinger in your EVS Display HAL implementation for Android 10. Keep in mind that these steps might need further adaptation based on your specific device and implementation.
Conclusion
The Vehicle Camera Hardware Abstraction Layer (HAL) serves as a crucial link between the complex hardware of vehicle cameras and the software applications that leverage their capabilities. By abstracting hardware intricacies, standardizing interfaces, and optimizing performance, the HAL empowers automotive developers to focus on creating innovative applications and features that enhance driving safety and convenience. As the automotive industry continues to advance, the Vehicle Camera HAL will remain a cornerstone of the technology driving the vehicles of the future.
Android’s Vehicle Hardware Abstraction Layer (HAL) is a crucial component that facilitates communication between Android applications and the various sensors and signals within a vehicle. The Vehicle HAL stores information in the form of Vehicle Properties, which are often associated with signals on the vehicle bus. This blog will delve into the fundamental aspects of the Vehicle HAL, including Vehicle Properties, System Property Identifiers, extending VehicleProperty, and the essential functions defined in IVehicle.
Vehicle Hardware Abstraction Layer (Vehicle HAL / VHAL)
The Vehicle Hardware Abstraction Layer (VHAL) is like a bridge between Android software and the hardware inside a vehicle. It helps Android applications communicate with the different sensors and functions in a standardized way.
Think of the VHAL as a set of rules that the vehicle follows, telling it how to communicate with Android. These rules are called “properties.” Each property represents a specific function or piece of information inside the vehicle.
For example, one property could be the vehicle’s speed, another property could be the temperature setting for the heating system, and so on.
Properties have certain characteristics, like whether they contain whole numbers (integers) or decimal numbers (floats) and how they can be changed or accessed.
There are different ways to interact with properties:
Read: You can ask the vehicle for the current value of a property. For instance, you can read the speed property to know how fast the vehicle is going.
Write:You can set the value of a property programmatically. For example, you can write a new temperature setting to control the vehicle’s heating system.
Subscribe:You can subscribe to changes in a property, which means you will get notified whenever that property’s value changes. For instance, if you subscribe to the speed property, you will receive updates whenever the vehicle’s speed changes.
The VHAL interface provides a list of properties that vehicle manufacturers (OEMs) can implement in their vehicles. It also contains information about each property, such as what type of data it holds (int or float) and what change modes are allowed (e.g., reading, writing, or subscribing).
By following the VHAL rules and using properties, Android applications can communicate with the vehicle’s hardware without needing to know the specific details of each vehicle model. This abstraction makes it easier for developers to create apps that work across different types of vehicles, improving compatibility and user experience.
Vehicle properties
Vehicle Properties are pieces of information that represent various aspects of a vehicle’s hardware and functionality. Each property is uniquely identified by an integer (int32) key, which acts as its unique identifier.
Read-Only Properties
Definition:Read-only properties are those from which you can only retrieve information; you cannot change their values.
Usage:These properties provide information about the vehicle’s state or status, such as its current speed or fuel level.
Examples:The vehicle’s speed (FLOAT type), engine status (BOOLEAN type), or current timestamp (EPOCH_TIME type) are read-only properties.
Write-Only Properties
Definition: Write-only properties are used to send information to the Vehicle HAL; you cannot read their values.
Usage:These properties allow applications to pass data to the vehicle’s hardware or control certain functionalities.
Examples:Sending a command to turn on the headlights or adjusting the HVAC(Heating, Ventilation and Air Conditioning)system’s fan speed are actions facilitated by write-only properties.
Read-Write Properties
Definition:Read-write properties support both reading and writing operations, allowing you to both retrieve and change their values.
Usage:These properties enable two-way communication between applications and the vehicle’s hardware.
Examples: Setting the target temperature for the HVAC system (FLOAT type), adjusting the volume of the audio system (INT32 type), or configuring custom preferences (STRING type) are read-write properties.
Value Types
Vehicle Properties can have different value types, indicating the data format they use to store information. Some common value types include:
BYTES:Represents a sequence of raw binary data.
BOOLEAN:Represents a true/false value.
EPOCH_TIME: Represents a timestamp in the Unix Epoch time format (number of seconds since January 1, 1970).
FLOAT:Represents a single decimal number.
FLOAT[]: Represents an array of decimal numbers.
INT32: Represents a single whole number (integer).
INT32[]:Represents an array of whole numbers (integers).
INT64:Represents a single large whole number (long integer).
INT64[]: Represents an array of large whole numbers (long integers).
STRING:Represents a sequence of characters, such as text or words.
MIXED:Represents a combination of different data types within a single property.
Zoned Properties
Some properties are zoned, meaning they can have multiple values based on the number of zones supported. For example, a zoned property related to tire pressure may have different values for each tire. Zoned properties help account for variations across different parts of the vehicle.
Vehicle Properties in the Vehicle HAL can be read-only, write-only, or read-write, and each property has a specific value type associated with it. Zoned properties allow for multiple values based on the number of zones supported, catering to various parts or areas of the vehicle. Together, these properties facilitate seamless communication between Android applications and the vehicle’s hardware, enabling better control and monitoring of vehicle functionalities.
Area Types
Area Types in the Vehicle HAL define different areas within the vehicle, such as windows, mirrors, seats, doors, and wheels. Each area type serves as a category to organize and address specific parts of the vehicle.
The available Area Types are as follows:
GLOBAL:This area type represents a singleton area, which means it is a single entity with no multiple sub-areas.
WINDOW: This area type is based on windows and uses the VehicleAreaWindow enumeration to identify different window areas, such as front and rear windows.
MIRROR: This area type is based on mirrors and uses the VehicleAreaMirror enumeration to identify different mirror areas, such as left and right mirrors.
SEAT:This area type is based on seats and uses the VehicleAreaSeat enumeration to identify different seat areas, such as the front and rear seats.
DOOR:This area type is based on doors and uses the VehicleAreaDoor enumeration to identify different door areas, such as front and rear doors.
WHEEL:This area type is based on wheels and uses the VehicleAreaWheel enumeration to identify different wheel areas, such as the front-left and rear-right wheels.
Each zoned property must use a pre-defined area type, and each area type has a set of bit flags defined in its respective enum (e.g., VehicleAreaSeat has flags like ROW_1_LEFT, ROW_1_CENTER, ROW_1_RIGHT, etc.).
For example, the SEAT area defines VehicleAreaSeat enums:
ROW_1_LEFT = 0x0001
ROW_1_CENTER = 0x0002
ROW_1_RIGHT = 0x0004
ROW_2_LEFT = 0x0010
ROW_2_CENTER = 0x0020
ROW_2_RIGHT = 0x0040
ROW_3_LEFT = 0x0100
…
Area IDs
Zoned properties are addressed through Area IDs, which represent specific combinations of flags from their respective enum. Each zoned property may support one or more Area IDs to define the relevant areas within the vehicle.
Area IDs
For example, if a property uses the VehicleAreaSeat area type, it might use the following Area IDs:
ROW_1_LEFT | ROW_1_RIGHT:This Area ID applies to both front seats, combining the flags for the left and right seats in the first row.
ROW_2_LEFT:This Area ID applies only to the rear left seat in the second row.
ROW_2_RIGHT:This Area ID applies only to the rear right seat in the second row.
By using Area IDs, zoned properties can target specific areas within the vehicle, allowing for more granular control and monitoring of different parts of the vehicle separately.
Property Status
Property Status in the Vehicle HAL indicates the current condition of a property’s value. Each property value is accompanied by a VehiclePropertyStatus value, which informs about the property’s availability and validity.
Every property value comes with a VehiclePropertyStatus value. This indicates the current status of the property:
Property Status
Available Values of VehiclePropertyStatus:
AVAILABLE:This status indicates that the property is supported by the vehicle and the current value is valid and accessible. It means the property is ready to be read or written to.
UNAVAILABLE: When a property has the UNAVAILABLE status, it means the property value is currently unavailable or not accessible. This status is typically used for transient conditions, where a supported property might be temporarily disabled or unavailable. It is not meant to indicate that the property is unsupported in general.
ERROR:The ERROR status indicates that something is wrong with the property. This status might be used when there is a problem with retrieving or setting the property’s value, such as a communication issue or an internal error.
Important Note: It is essential to remember that if a property is not supported by the vehicle, it should not be included in the Vehicle HAL at all. In other words, unsupported properties should not be part of the VHAL interface. It is not acceptable to set the property status to UNAVAILABLE permanently just to denote an unsupported property.
Configuring a property
Configuring a property in the Vehicle HAL involves using the VehiclePropConfig structure to provide important configuration information for each property. This information includes various variables that help define how the property can be accessed, monitored, and controlled.
Use VehiclePropConfig to provide configuration information for each property. Information includes:
Configuring Property
Below are the details of the variables used in the configuration:
access
Description: The ‘access’ variable specifies the type of access allowed for the property.
Values: It can be set to one of the following:
1. Read-only access (Value: ‘r’):
Description: The property can be read but not modified.
Access Type: Read-only.
2.Write-only access (Value: ‘w’):
Description: The property can be written but not read.
Access Type: Write-only.
3. Read-write access (Value: ‘rw’):
Description: The property supports both reading and writing.
Access Type: Read-write.
changeMode
Description: The ‘changeMode’ variable represents how the property is monitored for changes.
Values: It can be set to either ‘ON_CHANGE’ or ‘CONTINUOUS.’
‘ON_CHANGE’:The property triggers an event only when its value changes.
‘CONTINUOUS’:The property is constantly changing, and the subscriber is notified at the sampling rate set.
areaConfigs
Description: The ‘areaConfigs’ variable contains configuration information for different areas associated with the property.
Information: It includes areaId, min, and max values.
areaId:Represents the Area ID associated with the property (e.g., ROW_1_LEFT, ROW_2_RIGHT).
min: Specifies the minimum valid value for the property.
max:Specifies the maximum valid value for the property.
configArray
Description: The ‘configArray’ variable is used to hold additional configuration parameters for the property.
Information: It can store an array of specific data related to the property.
configString
Description: The ‘configString’ variable is used to provide additional information related to the property as a string.
Information: It can hold any extra details or specifications for the property.
minSampleRate, maxSampleRate
Description: These variables specify the minimum and maximum sampling rates (measurement frequency) for the property when monitoring changes.
Information: They define how often the property values are checked for updates.
prop
Description: The ‘prop’ variable is the Property ID, an integer that uniquely identifies the property in the Vehicle HAL.
Information: Each property in the Vehicle HAL is assigned a specific Property ID, which acts as its unique identifier. This ID is used to access, configure, and interact with the property within the Vehicle HAL interface. It ensures that each property can be referenced uniquely, even if there are multiple properties with similar or related functionalities.
System Property Identifiers
System Property Identifiers in the Vehicle HAL are unique labels used to categorize and identify specific properties. They are marked with the tag “VehiclePropertyGroup:SYSTEM” to distinguish them from other types of properties.
In Android 12, there are more than 150 such identifiers. Each identifier represents a different property related to the vehicle’s system and functionalities. For example, one of these identifiers is “HVAC_TEMPERATURE_SET,” which stands for the target temperature set for the vehicle’s HVAC system.
Let’s break down the details of the “HVAC_TEMPERATURE_SET” identifier:
Property Name:HVAC_TEMPERATURE_SET
Description:Represents the target temperature set for the HVAC (Heating, Ventilation, and Air Conditioning) system in the vehicle.
Change Mode:The property is monitored in the “ON_CHANGE” mode, which means an event is triggered whenever the target temperature changes.
Access: The property can be both read and written, allowing applications to retrieve the current target temperature and update it programmatically.
Unit:The temperature values are measured in Celsius (°C).
System Property Identifiers in the Vehicle HAL are unique labels that categorize different properties related to the vehicle’s system. They provide standardized access to various functionalities, such as setting the target temperature for the HVAC system. By using these identifiers, Android applications can seamlessly interact with the vehicle’s hardware, enhancing user experience and control over various vehicle features.
Handling zone properties
Handling zone properties involves dealing with collections of multiple properties, where each part can be accessed using a specific Area ID value. Here’s how the different calls work:
Get Calls:
When you make a “get” call for a zoned property, you must include the Area ID in the request.
As a result, only the current value for the requested Area ID is returned.
If the property is global (applies to all zones), the Area ID is set to 0.
Set Calls:
For a “set” call on a zoned property, you need to specify the Area ID.
This means that only the value for the requested Area ID will be changed.
Subscribe Calls:
A “subscribe” call generates events for all Area IDs associated with the property.
This means that whenever there is a change in any Area ID’s value, the subscribed function will be notified.
When dealing with zoned properties, using the Area ID allows you to access specific parts of the collection. “Get” calls return the current value for a specified Area ID, “Set” calls change the value for a requested Area ID, and “Subscribe” calls generate events for all Area IDs associated with the property.
Now, let’s look at specific scenarios for Get and Set calls:
Get Calls
During initialization, the value of the property may not be available yet due to pending vehicle network messages. In this case, the “get” call should return a special code, -EAGAIN, indicating that the value is not available yet.
Some properties, like HVAC (Heating, Ventilation, and Air Conditioning), have separate power properties to turn them on/off. When you “get” a property like HVAC Temperature and it’s powered off, it should return a status of UNAVAILABLE instead of an error.
Get HVAC temperature (CS = CarService, VHAL = Vehicle HAL)
Set Calls
A “set” call usually triggers a change request across the vehicle network. It’s ideally an asynchronous operation, returning as soon as possible, but it can also be synchronous if needed.
In some cases, a “set” call might require initial data that isn’t available during initialization. In such situations, the “set” call should return StatusCode#TRY_AGAIN to indicate that you should try again later.
For properties with separate power states (on and off), if the property is powered off and the “set” can’t be done, it should return StatusCode#NOT_AVAILABLE or StatusCode#NOT_AVAILABLE_DISABLED.
Until the “set” operation is complete and effective, the “get” call might not necessarily return the same value as what was set. For example, if you “set” the HVAC Temperature, the “get” call might not immediately reflect the new value until the change takes effect.
Set HVAC temperature (CS = CarService, VHAL = Vehicle HAL)
Handling custom properties
To support partner-specific needs, the VHAL allows custom properties that are restricted to system apps. Use the following guidelines when working with custom properties:
Property ID Generation:
Use the following format to generate the Property ID: VehiclePropertyGroup:VENDOR.
The VENDOR group should be used exclusively for custom properties.
Vehicle Area Type:
Select an appropriate area type(VehicleArea type) that best represents the scope of the custom property within the vehicle.
Vehicle Property Type:
Choose the proper data type for the custom property.
For most cases, the BYTES type is sufficient, allowing the passing of raw data.
Be cautious when adding a big payload, as frequently sending large data through custom properties can slow down the entire vehicle network access.
Property ID Format:
Choose a four-nibble ID for the custom property.
The format should consist of four hexadecimal characters.
Avoid Replicating Existing Vehicle Properties:
To prevent ecosystem fragmentation, do not use custom properties to replicate vehicle properties that already exist in the VehiclePropertyIds SDK.
In the VehiclePropConfig.configString field, provide a short description of the custom property. This helps sanity check tools flag accidental replication of existing vehicle properties. For example, you can use a description like “hazard light state.”
Accessing Custom Properties:
Access custom properties through CarPropertyManager for Java components or through the Vehicle Network Service API for native components.
Avoid modifying other car APIs to prevent future compatibility issues.
Permissions for Vendor Properties:
After implementing vendor properties, select only the permissions list in the VehicleVendorPermission enum for vendor properties.
Avoid mapping vendor permissions to system properties to prevent breaking the Compatibility Test Suite (CTS) and Vendor Test Suite (VTS).
By following these guidelines, you can create and manage custom properties in the VHAL effectively while ensuring compatibility and preventing fragmentation within the ecosystem.
Handling HVAC properties
Handling HVAC properties in the VHAL (Vehicle Hardware Abstraction Layer) involves controlling various aspects of the HVAC system in a vehicle. Most HVAC properties are zoned properties, meaning they can be controlled separately for different zones or areas in the vehicle. However, some properties are global, affecting the entire vehicle’s HVAC system.
HVAC properties
Above two sample-defined HVAC properties are:
VEHICLE_PROPERTY_HVAC_TEMPERATURE_SET:This property is used to set the temperature per zone in the vehicle.
VEHICLE_PROPERTY_HVAC_RECIRC_ON:This property is used to control recirculation per zone.
To see a complete list of available HVAC properties, you can search for properties starting with VEHICLE_PROPERTY_HVAC_* in the types.hal file.
When the HVAC property uses VehicleAreaSeat, there are additional rules for mapping a zoned HVAC property to Area IDs. Each available seat in the car must be part of an Area ID in the Area ID array.
Let’s take two examples to better understand how to map HVAC_TEMPERATURE_SET to Area IDs:
Example One:
Car Configuration: The car has two front seats (ROW_1_LEFT, ROW_1_RIGHT) and three back seats (ROW_2_LEFT, ROW_2_CENTER, ROW_2_RIGHT).
HVAC Units: The car has two temperature control units: one for the driver side and one for the passenger side.
A valid mapping set of Area IDs for HVAC_TEMPERATURE_SET is:
Driver side temperature control: ROW_1_LEFT | ROW_2_LEFT
Passenger side temperature control: ROW_1_RIGHT | ROW_2_CENTER | ROW_2_RIGHT
An alternative mapping for the same hardware configuration is:
Driver side temperature control: ROW_1_LEFT | ROW_2_LEFT | ROW_2_CENTER
Passenger side temperature control: ROW_1_RIGHT | ROW_2_RIGHT
Example Two:
Car Configuration: The car has three seat rows with two seats in the front row (ROW_1_LEFT, ROW_1_RIGHT), three seats in the second row (ROW_2_LEFT, ROW_2_CENTER, ROW_2_RIGHT), and three seats in the third row (ROW_3_LEFT, ROW_3_CENTER, ROW_3_RIGHT).
HVAC Units: The car has three temperature control units: one for the driver side, one for the passenger side, and one for the rear.
A reasonable way to map HVAC_TEMPERATURE_SET to Area IDs is as a three-element array:
Keep in mind that the exact mapping of HVAC properties to Area IDs may vary based on the vehicle’s hardware configuration and the HVAC system’s design. The examples provided above demonstrate how different seat configurations and HVAC units can influence the mapping of HVAC properties to specific zones in the vehicle.
Handling sensor properties
VHAL sensor properties are a way for apps to access real sensor data or policy information from the vehicle. Some sensor information, such as driving status and day/night mode, is accessible by any app without restriction. This is because this data is mandatory to build a safe vehicle application. Other sensor information, such as vehicle speed, is more sensitive and requires specific permissions that users can manage.
The supported sensor properties are defined in the types.hal file. This file lists all of the available sensor properties, along with their type, access permissions, and other metadata.
To access a VHAL sensor property, an app must first obtain a reference to the IVehicle interface. This interface provides methods for reading, writing, and subscribing to sensor properties.
Once the app has a reference to the IVehicle interface, it can use the get method to read the value of a sensor property. The get method takes the property ID as an argument and returns the value of the property.
The app can also use the set method to write the value of a sensor property. The set method takes the property ID and the new value as arguments.
To subscribe to a sensor property, the app can use the subscribe method. The subscribe method takes the property ID and a callback as arguments. The callback will be invoked whenever the value of the property changes.
Here is an example of how to access a VHAL sensor property:
// Get a reference to the IVehicle interface. IVehicle vehicle = VehicleManager.getVehicle();
// Get the value of the driving status property. int drivingStatus = vehicle.get(VehiclePropertyIds.DRIVING_STATUS);
// If the vehicle is driving, turn on the headlights. if (drivingStatus == 1) { vehicle.set(VehiclePropertyIds.HEADLIGHTS, 1); }
HAL interfaces
The Vehicle Hardware Abstraction Layer (VHAL) is a HAL interface that allows apps to access vehicle properties. The VHAL provides a number of interfaces that can be used to read, write, and subscribe to vehicle properties.
The getAllPropConfigs() interface returns a list of all the properties that are supported by the VHAL. The getPropConfigs() interface returns the configuration of a specific property. The set() interface allows you to write a value to a property. The subscribe() interface allows you to subscribe to a property so that you are notified when its value changes.
The VHAL also provides two callback interfaces: onPropertyEvent() and onPropertySetError(). The onPropertyEvent() interface is called whenever the value of a property that you are subscribed to changes. The onPropertySetError() interface is called if an error occurs when you try to set the value of a property.
Here is just a recap of the above example of how to use the VHAL to read the value of the driving status property:
// Get a reference to the IVehicle interface. IVehicle vehicle = VehicleManager.getVehicle();
// Get the value of the driving status property. int drivingStatus = vehicle.get(VehiclePropertyIds.DRIVING_STATUS);
Here is a brief explanation of the HAL interfaces:
VHAL Interfaces:
IVehicle.hal file
Please note that the below .hal files are not Java, C++ or scss files (I selected auto mode so it will take Java, C++, or scss)
BTW, What is .hal file?
A .hal file is a Hardware Abstraction Layer (HAL) file that defines the interface between a hardware device and the Android operating system. HAL files are written in the Hardware Interface Description Language (HIDL), which is a language for describing hardware interfaces in a platform-independent way.
package [email protected];
import IVehicleCallback;
interface IVehicle {
/**
* Returns a list of all property configurations supported by this vehicle
* HAL.
*/
getAllPropConfigs() generates (vec<VehiclePropConfig> propConfigs);
/**
* Returns a list of property configurations for given properties.
*
* If requested VehicleProperty wasn't found it must return
* StatusCode::INVALID_ARG, otherwise a list of vehicle property
* configurations with StatusCode::OK
*/
getPropConfigs(vec<int32_t> props)
generates (StatusCode status, vec<VehiclePropConfig> propConfigs);
/**
* Get a vehicle property value.
*
* For VehiclePropertyChangeMode::STATIC properties, this method must always
* return the same value always.
* For VehiclePropertyChangeMode::ON_CHANGE properties, it must return the
* latest available value.
*
* Some properties like AUDIO_VOLUME requires to pass additional data in
* GET request in VehiclePropValue object.
*
* If there is no data available yet, which can happen during initial stage,
* this call must return immediately with an error code of
* StatusCode::TRY_AGAIN.
*/
get(VehiclePropValue requestedPropValue)
generates (StatusCode status, VehiclePropValue propValue);
/**
* Set a vehicle property value.
*
* Timestamp of data must be ignored for set operation.
*
* Setting some properties require having initial state available. If initial
* data is not available yet this call must return StatusCode::TRY_AGAIN.
* For a property with separate power control this call must return
* StatusCode::NOT_AVAILABLE error if property is not powered on.
*/
set(VehiclePropValue propValue) generates (StatusCode status);
/**
* Subscribes to property events.
*
* Clients must be able to subscribe to multiple properties at a time
* depending on data provided in options argument.
*
* @param listener This client must be called on appropriate event.
* @param options List of options to subscribe. SubscribeOption contains
* information such as property Id, area Id, sample rate, etc.
*/
subscribe(IVehicleCallback callback, vec<SubscribeOptions> options)
generates (StatusCode status);
/**
* Unsubscribes from property events.
*
* If this client wasn't subscribed to the given property, this method
* must return StatusCode::INVALID_ARG.
*/
unsubscribe(IVehicleCallback callback, int32_t propId)
generates (StatusCode status);
/**
* Print out debugging state for the vehicle hal.
*
* The text must be in ASCII encoding only.
*
* Performance requirements:
*
* The HAL must return from this call in less than 10ms. This call must avoid
* deadlocks, as it may be called at any point of operation. Any synchronization
* primitives used (such as mutex locks or semaphores) must be acquired
* with a timeout.
*
*/
debugDump() generates (string s);
};
getAllPropConfigs():
This interface returns a list of all the properties that are supported by the VHAL. This list includes the property ID, property type, and other metadata.
Generates (vec<VehiclePropConfig> propConfigs).
Lists the configuration of all properties supported by the VHAL.
CarService uses supported properties only.
getPropConfigs(vec<int32_t> props):
This interface returns the configuration of a specific property. The configuration includes the property ID, property type, access permissions, and other metadata.
This interface allows you to subscribe to a property so that you are notified when its value changes. The callback that you provide will be called whenever the value of the property changes.
Generates (StatusCode status).
Starts monitoring a property value change.
For zoned properties, there is an additional unsubscribe(IVehicleCallback callback, int32_t propId) method to stop monitoring a specific property for a given callback.
VHAL Callback Interfaces:
IVehicleCallback.hal
package [email protected];
interface IVehicleCallback {
/**
* Event callback happens whenever a variable that the API user has
* subscribed to needs to be reported. This may be based purely on
* threshold and frequency (a regular subscription, see subscribe call's
* arguments) or when the IVehicle#set method was called and the actual
* change needs to be reported.
*
* These callbacks are chunked.
*
* @param values that has been updated.
*/
oneway onPropertyEvent(vec<VehiclePropValue> propValues);
/**
* This method gets called if the client was subscribed to a property using
* SubscribeFlags::SET_CALL flag and IVehicle#set(...) method was called.
*
* These events must be delivered to subscriber immediately without any
* batching.
*
* @param value Value that was set by a client.
*/
oneway onPropertySet(VehiclePropValue propValue);
/**
* Set property value is usually asynchronous operation. Thus even if
* client received StatusCode::OK from the IVehicle::set(...) this
* doesn't guarantee that the value was successfully propagated to the
* vehicle network. If such rare event occurs this method must be called.
*
* @param errorCode - any value from StatusCode enum.
* @param property - a property where error has happened.
* @param areaId - bitmask that specifies in which areas the problem has
* occurred, must be 0 for global properties
*/
oneway onPropertySetError(StatusCode errorCode,
int32_t propId,
int32_t areaId);
};
After seeing this file you might be wondering about, what is a oneway method.
A oneway method in a HAL file is a method that does not require a response from the hardware device. Oneway methods are typically used for asynchronous operations, such as sending a command to the hardware device or receiving a notification from the hardware device.
Here is an example of a oneway method in a HAL file:
oneway void setBrightness(int brightness);
This method sets the brightness of the hardware device to the specified value. The method does not require a response from the hardware device, so the caller does not need to wait for the method to complete before continuing.
Oneway methods are often used in conjunction with passthrough HALs. Passthrough HALs are HALs that run in the same process as the calling application. This means that oneway methods in passthrough HALs can be invoked directly by the calling application, without the need for a binder call.
This callback is called whenever the value of a property that you are subscribed to changes. The callback will be passed a list of the properties that have changed and their new values.
A one-way callback function.
Notifies vehicle property value changes to registered callbacks.
This function should be used only for properties that have been subscribed to for monitoring.
This callback is called if an error occurs when you try to set the value of a property. The callback will be passed the error code and the property ID that was being set.
A one-way callback function.
Notifies errors that occurred during property write operations.
The error can be related to the VHAL level or specific to a property and an area (in the case of zoned properties).
These interfaces and callbacks form the core communication mechanism between the VHAL and other components, such as CarService and applications, allowing for the configuration, querying, writing, and monitoring of vehicle properties. The usage of these interfaces may vary depending on the specific implementation of the VHAL in different systems or platforms.
Properties Monitoring and Notification
In the context of the Vehicle Hardware Abstraction Layer (VHAL) and its properties, the IVehicle::subscribe method and IVehicleCallback::onChange callback are used for monitoring changes in vehicle properties. Additionally, there is a ChangeMode enum that defines how the properties behave in terms of their update frequency.
IVehicle::subscribe
The IVehicle::subscribe method is used to register a callback (implementing IVehicleCallback) to receive updates when the subscribed properties change.
This method allows applications to start monitoring specific vehicle properties for value changes.
IVehicleCallback::onChange
The IVehicleCallback::onChange callback function is invoked when there are updates to the subscribed properties.
When a property changes and the VHAL detects the change, it notifies all registered callbacks using this callback function.
ChangeMode Enum
The ChangeMode enum defines how a particular property behaves in terms of its update frequency. It has the following possible values:
STATIC:The property never changes.
ON_CHANGE: The property only signals an event when its value changes.
CONTINUOUS:The property constantly changes and is notified at a sampling rate set by the subscriber.
These definitions allow applications to subscribe to properties with different update behaviors based on their specific needs. For example, if an application is interested in monitoring the vehicle speed, it may subscribe to the speed property with the CONTINUOUS change mode to receive a continuous stream of speed updates at a certain sampling rate. On the other hand, if an application is interested in the vehicle’s daytime/nighttime mode, it may subscribe with the ON_CHANGE change mode to receive updates only when the mode changes from day to night or vice versa.
The use of these definitions and methods allows for efficient monitoring and notification of changes in vehicle properties, ensuring that applications can stay up-to-date with the latest data from the vehicle’s sensors and systems.
Conclusion
The Vehicle HAL is a critical component of the Android operating system that facilitates seamless communication between Android applications and a vehicle’s hardware and sensors. By utilizing Vehicle Properties and the various functions defined in the IVehicle interface, developers can access and control essential aspects of a vehicle’s state and functioning. Furthermore, the ability to extend Vehicle Properties using custom identifiers offers developers the flexibility to tailor their applications to specific vehicle hardware and functionalities, thereby enhancing the overall user experience. As Android continues to evolve, the Vehicle HAL is expected to play an even more significant role in shaping the future of automotive technology.
In the ever-evolving landscape of technology, where innovation and adaptability are paramount, Android has emerged as a dominant force. Beyond smartphones, Android’s influence has extended into various industries, including the automotive sector. A pivotal development in this journey has been the introduction of Project Treble and its subsequent integration into Android Automotive OS, ushering in a new era of modular advancements. This article delves into the essence of Project Treble and how its modular approach has transformed Android’s foray into the automotive realm.
Project Treble and Android Automotive OS
Project Treble is an initiative by Google introduced in Android 8.0 Oreo to address the challenges of Android fragmentation(Here Fragmentation refers to the situation where many Android devices run different versions of the operating system) and make it easier for device manufacturers to update their devices to newer Android versions. It separates the Android OS framework from the hardware-specific components, allowing manufacturers to update the Android OS without modifying the lower-level hardware drivers and firmware.
Project Treble
In the context of Android Automotive OS, Project Treble has a similar goal but is adapted to the specific needs of automotive infotainment systems. Android Automotive OS is built on top of the regular Android OS but is optimized for use in vehicles. It provides a customized user interface and integrates with car-specific hardware and features.
Project Treble in Android Automotive OS helps automotive manufacturers (OEMs) update their in-car infotainment systems more efficiently. Separating the Android OS framework from the hardware-specific components, allows OEMs to focus on developing and updating their unique infotainment features without being held back by delays caused by complex hardware integration.
Android Open Source Project (AOSP) Architecture
In the Android Open Source Project (AOSP) architecture, everything above the Android System Services is known as the “Android Framework,” and it is provided by Google. This includes various components like the user interface, app development framework, and system-level services.
AOSP Architecture
On the other hand, the Hardware Abstraction Layer (HALs) and the Kernel are provided by System on a Chip (SoC) and hardware vendors. The HALs act as a bridge between the Android Framework and the specific hardware components, allowing the Android system to work efficiently with different hardware configurations.
In a groundbreaking move, Google extended the Android Open Source Project (AOSP) to create a complete in-vehicle infotainment operating system(we will look in detail later). Here’s a simple explanation of the extensions:
Car System Applications:Google added specific applications designed for in-car use, such as music players, navigation apps, and communication tools. These applications are optimized for easy and safe use while driving.
Car APIs:Google introduced specialized Application Programming Interfaces (APIs) that allow developers to access car-specific functionalities. These APIs provide standardized ways for apps to interact with car features like sensors and controls.
Car Services:Car Services are system-level components that handle car-specific functionalities, such as managing car sensors, audio systems, and climate controls. These services provide a consistent and secure way for apps to interact with car hardware.
Vehicle Hardware Abstraction Layer:To interact with the unique hardware components of different vehicles, Google developed the Vehicle Hardware Abstraction Layer (HAL). It acts as a bridge between the Android system and the specific hardware, enabling a seamless and consistent experience across various cars.
By combining these extensions with the existing Android system, Google created a fully functional and adaptable in-vehicle infotainment operating system. This system can be used in different vehicles without the need for significant modifications, offering a unified and user-friendly experience for drivers and passengers.
Treble Components
Project Treble introduced several new components to the Android architecture to enhance modularity and streamline the update process for Android devices.
Showing what’s newly added by Treble
Let’s briefly explain each of these components:
New HAL types: These are Hardware Abstraction Layers (HALs) that help the Android system communicate with various hardware components in a standardized way. They allow easier integration of different hardware into the Android system.
Hardware Interface Definition Language (HIDL):HIDL is a language used to define interfaces between HALs and the Android framework. It makes communication between hardware and software more efficient.
New Partitions:Treble introduced new partitions in the Android system, like the /vendor partition. These partitions help separate different parts of the system, making updates easier and faster.
ConfigStore HAL:This component manages configuration settings for hardware components. It provides a standardized way to access and update configuration data.
Device Tree Overlays:Device Tree Overlays enable changes to hardware configuration without having to modify the kernel. It allows for easier customization of hardware.
Vendor NDK: The Vendor Native Development Kit (NDK) provides tools and libraries for device manufacturers to develop software specific to their hardware. It simplifies the integration of custom functionalities.
Vendor Interface Object:The Vendor Interface Object (VINTF) defines a stable interface between the Android OS and the vendor’s HAL implementations. It ensures compatibility and smooth updates.
Vendor Test Suite (VTS):VTS is a testing suite that ensures HAL implementations work correctly with the Android framework. It helps in verifying the compatibility and reliability of devices.
Project Treble’s components make Android more modular, efficient, and customizable. They streamline communication with hardware, separate system components, and allow device manufacturers to update and optimize their devices more easily, resulting in a better user experience and faster Android updates.
Modularity in Android Automotive with Treble
Thanks to the architectural changes brought about by Project Treble and the expanded use of partitions, the future of Android Automotive has become significantly more flexible and adaptable. This enhancement extends beyond just the Human-Machine Interface (HMI) layer and allows for potential replacements of the Android framework, Board Support Package (BSP), and even the hardware if necessary.
In simpler terms, the core components of the Android Automotive system have been made more independent and modular. This means that manufacturers now have the freedom to upgrade or customize specific parts of the system without starting from scratch. The result is a highly future-proof system that can readily embrace emerging technologies and cater to evolving user preferences.
Let’s delve into the transition and see how this modularity was achieved after the implementation of Project Treble:
HALs before Treble
Before Project Treble, HAL interfaces were defined as C header files located in the hardware/libhardware folder of the Android system. Each new version of Android required the HAL to support a new interface, which meant significant effort and changes for hardware vendors.
HALs before Treble
In simpler terms, HALs used to be tightly coupled with the Android framework, and whenever a new Android version was released, hardware vendors had to update their HALs to match the new interfaces. This process was time-consuming and complex, leading to delays in device updates and making it difficult to keep up with the latest Android features.
Project Treble addressed this issue by introducing the Hardware Interface Definition Language (HIDL). With HIDL, HAL interfaces are now defined in a more standardized and independent way, making it easier for hardware vendors to implement and update their HALs to support new Android versions. This change has significantly improved the efficiency of Android updates and allowed for a more flexible and future-ready Android ecosystem.
Pass-through HALs
In the context of Android Automotive, Pass-through HALs are special Hardware Abstraction Layers (HALs) that use the Hardware Interface Definition Language (HIDL) interface. The unique aspect of Pass-through HALs is that you can directly call them from your application’s process, without going through the usual Binder communication.
Pass-through HALs
To put it simply, when an app wants to interact with a regular HAL, it communicates using the Binder mechanism, which involves passing messages between different processes. However, with Pass-through HALs, you can directly communicate with the HAL from your app’s process. This direct calling approach can offer certain advantages in terms of efficiency and performance for specific tasks in the automotive context. It allows apps to access hardware functionalities with reduced overhead and faster response times.
Binderized HALs
In the Android Automotive context, Binderized HALs run in their dedicated processes and are accessible only through Binder Inter-Process Communication (IPC) calls. This setup ensures that the communication between the Android system and the HALs is secure and efficient.
Binderized HALs
Regarding Legacy HALs, Google has already created a wrapper to make them work in a Binderized environment. This wrapper acts as an intermediary layer, allowing the existing Legacy HALs to communicate with the Android framework through the Binder IPC mechanism. As a result, these Legacy HALs can seamlessly function alongside Binderized HALs, ensuring compatibility and a smooth transition to the new architecture.
In essence, the wrapper provides a bridge between the legacy hardware components and the modern Android system, enabling Legacy HALs to work cohesively in the Binderized environment. This approach ensures that the Android Automotive system can benefit from the improved performance and security of Binderized HALs while still supporting and integrating with older hardware that relies on Legacy HALs.
Ideal HALs
In an ideal scenario, Binderized HALs are the preferred approach for Hardware Abstraction Layers (HALs) in Android. Binderized HALs run in their dedicated processes and are accessed through the secure Binder Inter-Process Communication (IPC) mechanism. This design ensures efficient communication, better security, and separation of hardware functionalities from the Android system.
Ideal HALs
However, for some reasons, we didn’t bother implementing Binderized HALs as intended. Instead, we are using a different approach, possibly using legacy HALs that were not originally designed for Binder IPC. While this alternative approach may work, it might not provide the full benefits of Binderized HALs, such as improved performance and security.
It’s important to recognize that sticking to the ideal Binderized HALs offers several advantages and aligns with the best practices recommended by Google. If possible, it’s better to consider transitioning to Binderized HALs for a more robust and efficient Android Automotive system.
Detailed Architecture
Now, as you know, in Android 8.0, the Android operating system underwent a re-architecture to establish clear boundaries between the device-independent Android platform and device- or vendor-specific code. Before this update, Android had already defined interfaces called HAL interfaces, which were written in C headers located in hardware/libhardware.
With the re-architecture, these HAL interfaces were replaced by a new concept called HIDL (HAL Interface Definition Language). HIDL offers stable and versioned interfaces, which can be either written in Java or as client- and server-side HIDL interfaces in C++.
Detailed Architecture C++
The primary purpose of HIDL interfaces is to be used from native code, especially focused on enabling the auto-generation of efficient C++ code. This is because native code is generally faster and more efficient for low-level hardware interactions. However, to maintain compatibility and support various Android subsystems, some HIDL interfaces are also exposed directly to Java code.
Detailed Architecture / Java
For instance, certain Android subsystems like Telephony utilize Java HIDL interfaces to interact with underlying hardware components. This allows them to benefit from the stable and versioned interface definitions provided by HIDL, ensuring seamless communication between the device-independent Android platform and device-specific code.
Conclusion
Project Treble’s modular approach and Android Automotive OS’s tailored architecture have revolutionized Android’s adaptability for both devices and vehicles. By separating hardware-specific components, manufacturers can efficiently update their systems. The integration of specialized APIs and services in Android Automotive OS streamlines infotainment, while Project Treble’s HAL enhancements and modularity ensure seamless hardware communication. These advancements collectively promise a future-proof, user-friendly experience for both drivers and passengers.