Create an IVI Service

Last edit: 2024.04.30

Introduction of the IVI application architecture

An IVI application consists of IVI services for the business logic and frontends for the UI, which combines layouts, panels, and view models. Panels and associated view models are rather short-living components that are created to display something on screen, and are destroyed as soon as content disappears. Services live much longer, almost matching the application lifetime. Apart from Application Platform services, there can be application services that work with dedicated frontends. Such services may suit as a model for a frontend.

Overview of the example application

The example application replaces TomTom Digital Cockpit's user profile frontend with the account frontend. The account frontend adds new panels to show account information and a login page. The account frontend can be invoked by a menu item (see frontend plugin). The account status is managed by the accounts service. The source code for the frontend and service can be found in the following directories in the example app source:

The plan

The accounts service will provide methods to login and logout a user, and keep the current logged user.

To create an IVI service, you need to perform the following steps:

IVI service interface module

Module configuration

An IVI service interface should be defined in a dedicated module, so it can be used both by the service implementation, and the service's clients.

To define the service interface, first create a new module at examples/plugin/serviceapi and add a build script.

Create a build.gradle.kts file:

1import com.tomtom.ivi.platform.gradle.api.framework.config.ivi
2
3// Gradle extension to configure a project.
4ivi {
5 // Set the flag to configure that the project contains the definition of an IVI service.
6 serviceApi = true
7}
8
9dependencies {
10 api(libraries.iviPlatformFrameworkApiCommonUid)
11}
12
13// The rest of the module configuration...

The Gradle project configuration above creates an IVI service module. To create an IVI settings service project, set settingsServiceApi instead of serviceApi.

The IVI service interface

When the project is configured, create a Kotlin interface class, annotated with the @IviService annotation, which takes one mandatory argument serviceId. This specifies the unique identifier that is used by client connections.

Create src/main/kotlin/com/example/ivi/example/plugin/serviceapi/AccountsService.kt

1// The annotation for an IVI service interface.
2import com.tomtom.ivi.platform.framework.api.ipc.iviserviceannotations.IviService
3// The annotation for IVI service functions.
4import com.tomtom.ivi.platform.framework.api.ipc.iviserviceannotations.IviServiceFun
5
6@IviService(
7 serviceId = "com.example.ivi.example.plugin.service"
8)
9interface AccountsService {
10 val activeAccount: Account?
11
12 @IviServiceFun
13 suspend fun logIn(username: String, password: SensitiveString): Boolean
14
15 @IviServiceFun
16 suspend fun logOut()
17
18 // The service interface must at least contain the empty companion object, which will be
19 // extended by the IVI service framework.
20 // Kotlin does not allow extending the companion object if it has not been declared.
21 companion object
22}

The Account class will be added in the parcelable types section.

The interface may have nullable or non-nullable values, methods, and event listener interfaces. All methods must have the suspend modifier and the @IviServiceFun annotation. The @IviServiceFun annotation is required to distinguish methods of the service interface from auxiliary methods added by Kotlin/Java compiler. The interface must at least contain the empty companion object, which will be extended by the IVI service framework with a few methods, such as createApi and createApiOrNull. Kotlin does not allow extending the companion object if it has not been declared.

The package may have multiple service interfaces defined, although each must have a distinct identifier. The package may contain other classes, functions, and any other Kotlin code like a regular Kotlin package.

Parcelable types

An IVI service uses the Android Binder framework to transfer data between clients and the service. Therefore, an IVI service interface can only use types that can be written to Android's Parcel type. Custom types must implement the Parcelable interface. The kotlin-parcelize plugin provides a Parcelable implementation generator. When you annotate a class with @Parcelize, a Parcelable implementation is automatically generated.

Create src/main/kotlin/com/example/ivi/example/plugin/serviceapi/Account.kt

1import com.tomtom.ivi.platform.framework.api.common.uid.Uid
2
3@Parcelize
4data class Account(
5 val accountUid: Uid<Account> = Uid.new(),
6)

Map types

While the service interface may use properties of Map<K,V> it is not recommended because it is inefficient. Any change of such property will require the transfer of a full map object to clients. Instead, use MirrorableMap which is optimized for updates over Binder transactions. It becomes a regular Map on the client side.

1@IviService(serviceId = "service.example")
2interface ExampleService {
3 val mapProp: MirrorableMap<Int, String>
4}

Data sources

The values of IVI service properties are pushed to all clients. When a pull model is desired, for instance for large data sets, it is possible to instead use a property of type IviDataSource. An IviDataSource can be used to expose a data set to clients without requiring the full data set to be loaded into memory. It also allows querying and sorting of the data on the service side and allows clients to process the data while it is also loading it from the service.

To load data from an IviDataSource you need to create one or more IviPagingSources. The IviPagingSources can be used to load data pages from the data source. Each IviPagingSource is bound to a query.

The IviPagingSource class is designed to seamlessly integrate with the Android Paging library. This makes it possible to represent elements in a RecyclerView. The platform_framework_api_ipc_iviserviceandroidpaging module contains extension functions for the integration.

src/main/kotlin/com/example/ivi/example/plugin/serviceapi/AccountsService.kt

1@IviService(serviceId = "com.example.ivi.example.plugin.service")
2interface AccountsService {
3 @IviExperimental
4 val accounts: IviDataSource<Account, AccountsDataSourceQuery>
5}

If you want to use an IVI data source, follow the use an IVI data source tutorial.

Note: IviDataSource is an experimental API.

The IVI service implementation

Behind the scenes

The IVI service framework generates multiple classes and methods from an annotated interface using the KSP compiler plugin. They all belong to the package of the service interface. These classes include an abstract base class for the service implementation and the service's client API.

Project configuration

An IVI service implementation should be defined in a different package than the interface.

To implement the service interface, create a new module at examples/plugin/service/ and add a build script.

Create a build.gradle.kts file:

1dependencies {
2 // The IVI service interface project provides the base class for an implementation.
3 implementation(project(":examples_plugin_serviceapi"))
4}

The service implementation

The IVI service framework generates an abstract base class for the implementation of the service API. The generated name is of the format <ServiceInterface>Base, in our example AccountsServiceBase. The service implementation must extend the class and implement the methods defined in the service interface.

Create src/main/kotlin/com/example/ivi/example/plugin/service/StockAccountsService.kt

1package com.example.ivi.example.plugin.service
2
3import com.example.ivi.example.plugin.common.Account
4import com.tomtom.ivi.platform.framework.api.ipc.iviservice.IviServiceHostContext
5
6internal class StockAccountsService(iviServiceHostContext: IviServiceHostContext) :
7 AccountsServiceBase(iviServiceHostContext) {
8
9 override fun onCreate() {
10 super.onCreate()
11 }
12
13 override fun onRequiredPropertiesInitialized() {
14 serviceReady = true
15 }
16
17 override suspend fun logIn(username: String, password: String): Boolean {
18 // Implement functionality
19 }
20
21 override suspend fun logOut() {
22 // Implement functionality
23 }
24
25 companion object {
26 private fun isValidUsername(value: String) = value.trim().length > 1
27 private fun isValidPassword(value: String) = value.trim().length > 2
28 }
29}

The service implementation must initialize all properties of the service interface at a startup. The service has observers for the properties to propagate changes to the clients. The lifecycle of observers depends on the service's lifecycle. Therefore, the service's lifecycle has to be started to propagate value changes. The implementation of onCreate callback in the base class starts the service's lifecycle. The service implementation should prefer overriding the onCreate callback for the initialization and avoid the init block. The overridden onCreate() method must call the super method. The init block can be still used to initialize private properties of the implementation that are not defined in the service interface and do not depend on the service's lifecycle.

Note: After the service is created, it is not yet available for clients until it declares its availability by changing the serviceReady property to true.

The serviceReady property can be set in the onCreate when all properties have been initialized. If the property has deferred initialization, because it depends on a LiveData value of another service for example, the availability can be changed via onRequiredPropertiesInitialized. This function is called when all (required) non-nullable properties are initialized with values. This is the preferred way even if there is no deferred initialization, or no required properties.

The implementation of the service methods is similar to the implementation of a regular Kotlin interface. It can declare new properties although they will not be visible in the service API interface.

Create a service host

An IVI service needs a host to run. A host may implement multiple service interfaces. The IVI service framework provides the abstract builder class IviServiceHostBuilder to build a service host. There is also the generic implementation of a service host that takes a list of service interface implementations. The generic implementation can be created by implementing the SimpleIviServiceHostBuilder.

The builder class must at least contain an empty companion object, which will be extended by the IVI service framework. Kotlin does not allow extending the companion object if it has not been declared.

The builder class must follow a specific naming convention. It must have a "ServiceHostBuilder" suffix and must start with an upper case character.

Create src/main/kotlin/com/example/ivi/example/plugin/service/AccountsServiceHostBuilder.kt

1package com.example.ivi.example.plugin.service
2
3import com.tomtom.ivi.platform.framework.api.ipc.iviservice.AnyIviServiceBase
4import com.tomtom.ivi.platform.framework.api.ipc.iviservice.IviServiceHostContext
5// The simple implementation of a service host builder.
6import com.tomtom.ivi.platform.framework.api.ipc.iviservice.SimpleIviServiceHostBuilder
7
8// `ServiceHostBuilder` suffix is mandatory.
9class AccountsServiceHostBuilder : SimpleIviServiceHostBuilder() {
10
11 override fun createIviServices(
12 iviServiceHostContext: IviServiceHostContext
13 ): Collection<AnyIviServiceBase> =
14 // Return the service interface implementation to run in the host.
15 listOf(StockAccountsService(iviServiceHostContext))
16
17 // The builder implementation must at least contain an empty companion object, which will be
18 // extended by the IVI service framework.
19 // Kotlin does not allow extending the companion object if it has not been declared.
20 companion object
21}

Service deployment

An IVI service host is deployed by default in its own process. This ensures memory, main thread and crash isolation. Binder is used for inter-process communication. If the client of the service is in the same process as the service host, the inter-process communication is skipped. It is possible to deploy service hosts together or, as a last resort option, as part of the main process.

To avoid multi-threading issues, all service functions are executed on the main thread of the process. Service functions can suspend and offload work to other threads to unblock the main thread of the process. This is especially a must for a service that needs to be deployed in the main process. This to avoid blocking the UI as this runs on the main thread as well.

The next paragraph explains how to deploy a service host with the default deployment configuration. See Configure the Runtime Deployment of the IVI System for information about how to configure the deployment.

Deploy the service host

Define an IVI service host implementation, in your gradle file. This can also be defined in a top-level gradle file (for example iviservicehosts.gradle.kts) so it can be used in a multi-project build, including the tests.

Modify the examples/plugin/app/build.gradle.kts file:

1import com.tomtom.ivi.buildsrc.dependencies.ExampleModuleReference
2import com.tomtom.ivi.platform.gradle.api.common.iviapplication.config.IviServiceHostConfig
3import com.tomtom.ivi.platform.gradle.api.common.iviapplication.config.IviServiceInterfaceConfig
4
5/**
6 * Defines a configuration for the accounts service.
7 *
8 * The configuration specifies the service host implementation and the list of interfaces
9 * implemented by this service host.
10 */
11val accountsServiceHost =
12 IviServiceHostConfig(
13 // Needs to match with the name of the builder class.
14 serviceHostBuilderName = "AccountsServiceHostBuilder",
15 // The module with the implementation of the service host builder class.
16 implementationModule = ExampleModuleReference("examples_plugin_service"),
17 interfaces = listOf(
18 IviServiceInterfaceConfig(
19 serviceName = "AccountsService",
20 // The module with the service interface.
21 serviceApiModule = ExampleModuleReference("examples_plugin_serviceapi")
22 )
23 ),
24 dependencies = IviServiceDependencies()
25 )

The service host build configuration uses the ExampleModuleReference class to resolve a module name into the fully-qualified package. It is defined once and used for all configurations. See Integrate TomTom Digital Cockpit into a Gradle Project for details.

If there are service interfaces that this service depends on, include these in dependencies. This enables build-time verification of whether these dependencies are present.

The next step is to register the service host build configuration in the main application's build script.

Modify the examples/plugin/app/build.gradle.kts file:

1import com.tomtom.ivi.platform.gradle.api.framework.config.ivi
2
3ivi {
4 application {
5 enabled = true
6 services {
7 // Register the accounts service to the application.
8 addHost(accountsServiceHost)
9 }
10 }
11}
12
13// The rest of the build script, dependencies, etc.

Use the IVI service in the client-side code

Clients access a service via an instance of the <ServiceInterface>Api class, in our example, AccountsServiceApi. The API instance is created with <ServiceInterface>.createApi(...) or <ServiceInterface>.createApiOrNull(...). The former requires a service to be registered and running, and the latter is an optional connection.

To use the service API, create an instance with createApi in the view-model of the account login page of the account frontend.

In examples/plugin/frontend/ create:

src/main/kotlin/com/example/ivi/example/plugin/frontend/login/AccountLoginViewModel.kt

1package com.example.ivi.example.plugin.frontend.login
2
3import androidx.lifecycle.MutableLiveData
4import androidx.lifecycle.map
5import com.example.ivi.example.plugin.serviceapi.AccountsService
6import com.example.ivi.example.plugin.serviceapi.createApi
7import com.tomtom.ivi.platform.frontend.api.common.frontend.viewmodels.FrontendViewModel
8import com.tomtom.tools.android.api.livedata.allTrue
9import com.tomtom.tools.android.api.livedata.valueUpToDate
10
11internal class AccountLoginViewModel(panel: AccountLoginPanel) :
12 FrontendViewModel<AccountLoginPanel>(panel) {
13
14 // Create an instance of service's client API.
15 private val accountsServiceApi =
16 AccountsService.createApi(this, frontendContext.iviServiceProvider)
17
18 // Properties used by the layout.
19 val username = MutableLiveData("")
20 val password = MutableLiveData("")
21
22 // [allTrue] is a LiveData transformation that emits `true` when all sources become `true`,
23 // and `false` otherwise.
24 val isLoginEnabled = allTrue(
25 // Wait till the service becomes available.
26 accountsServiceApi.serviceAvailable,
27 username.map { it.isNotBlank() },
28 password.map { it.isNotBlank() }
29 )
30
31 fun onLoginClick() {
32 // `LiveData.valueUpToDate` returns the current value if it is available.
33 // Unlike `LiveData.value` it uses an observer to obtain the value, so it can be used
34 // with LiveData's transformations.
35 isLoginEnabled.valueUpToDate?.takeIf { it }?.let {
36 val username = username.value ?: return
37 val password = password.value ?: return
38
39 // Log in an user with asynchronous call.
40 accountsServiceApi.logInAsync(username, password)
41
42 // There is a suspendable method as well.
43 // runBlocking {
44 // accountsServiceApi.coLogIn(username, password)
45 // }
46 }
47 }
48}

The service's client API is similar to the service interface. Service properties are mirrored in the API instance as LiveData of the original type. For example,val activeAccount: Account? in the service interface becomes val activeAccount: LiveData<Account?>. Service methods are mapped to two execution models: as coroutine coLogIn(...) and as async call logInAsync(...).

The accountsServiceApi.serviceAvailable property mirrors the serviceReady property of the service implementation.

Create src/main/kotlin/com/example/ivi/example/plugin/frontend/info/AccountInfoViewModel.kt

1package com.example.ivi.example.plugin.frontend.info
2
3import androidx.lifecycle.map
4import com.example.ivi.example.plugin.serviceapi.AccountsService
5import com.example.ivi.example.plugin.serviceapi.createApi
6import com.tomtom.ivi.platform.frontend.api.common.frontend.viewmodels.FrontendViewModel
7import java.util.Locale
8
9internal class AccountInfoViewModel(panel: AccountInfoPanel) :
10 FrontendViewModel<AccountInfoPanel>(panel) {
11
12 private val accountsServiceApi =
13 AccountsService.createApi(this, frontendContext.iviServiceProvider)
14
15 val displayName = accountsServiceApi.activeAccount.map {
16 it?.username?.replaceFirstChar(Char::uppercaseChar)
17 }
18
19 fun onLogoutClick() = accountsServiceApi.logOutAsync()
20}

Sharing data between service and clients

All properties of an IVI service are mirrored.