Create an IVI Service
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:
- Define an IVI service interface.
- Create an implementation of the IVI service interface.
- Deploy the service.
- Use the service API in the client-side code.
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.ivi23// 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 = true7}89dependencies {10 api(libraries.iviPlatformFrameworkApiCommonUid)11}1213// 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.IviService3// The annotation for IVI service functions.4import com.tomtom.ivi.platform.framework.api.ipc.iviserviceannotations.IviServiceFun56@IviService(7 serviceId = "com.example.ivi.example.plugin.service"8)9interface AccountsService {10 val activeAccount: Account?1112 @IviServiceFun13 suspend fun logIn(username: String, password: SensitiveString): Boolean1415 @IviServiceFun16 suspend fun logOut()1718 // The service interface must at least contain the empty companion object, which will be19 // extended by the IVI service framework.20 // Kotlin does not allow extending the companion object if it has not been declared.21 companion object22}
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.Uid23@Parcelize4data 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
IviPagingSource
s. The IviPagingSource
s 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 @IviExperimental4 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.service23import com.example.ivi.example.plugin.common.Account4import com.tomtom.ivi.platform.framework.api.ipc.iviservice.IviServiceHostContext56internal class StockAccountsService(iviServiceHostContext: IviServiceHostContext) :7 AccountsServiceBase(iviServiceHostContext) {89 override fun onCreate() {10 super.onCreate()11 }1213 override fun onRequiredPropertiesInitialized() {14 serviceReady = true15 }1617 override suspend fun logIn(username: String, password: String): Boolean {18 // Implement functionality19 }2021 override suspend fun logOut() {22 // Implement functionality23 }2425 companion object {26 private fun isValidUsername(value: String) = value.trim().length > 127 private fun isValidPassword(value: String) = value.trim().length > 228 }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.service23import com.tomtom.ivi.platform.framework.api.ipc.iviservice.AnyIviServiceBase4import com.tomtom.ivi.platform.framework.api.ipc.iviservice.IviServiceHostContext5// The simple implementation of a service host builder.6import com.tomtom.ivi.platform.framework.api.ipc.iviservice.SimpleIviServiceHostBuilder78// `ServiceHostBuilder` suffix is mandatory.9class AccountsServiceHostBuilder : SimpleIviServiceHostBuilder() {1011 override fun createIviServices(12 iviServiceHostContext: IviServiceHostContext13 ): Collection<AnyIviServiceBase> =14 // Return the service interface implementation to run in the host.15 listOf(StockAccountsService(iviServiceHostContext))1617 // The builder implementation must at least contain an empty companion object, which will be18 // extended by the IVI service framework.19 // Kotlin does not allow extending the companion object if it has not been declared.20 companion object21}
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.ExampleModuleReference2import com.tomtom.ivi.platform.gradle.api.common.iviapplication.config.IviServiceHostConfig3import com.tomtom.ivi.platform.gradle.api.common.iviapplication.config.IviServiceInterfaceConfig45/**6 * Defines a configuration for the accounts service.7 *8 * The configuration specifies the service host implementation and the list of interfaces9 * 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.ivi23ivi {4 application {5 enabled = true6 services {7 // Register the accounts service to the application.8 addHost(accountsServiceHost)9 }10 }11}1213// 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.login23import androidx.lifecycle.MutableLiveData4import androidx.lifecycle.map5import com.example.ivi.example.plugin.serviceapi.AccountsService6import com.example.ivi.example.plugin.serviceapi.createApi7import com.tomtom.ivi.platform.frontend.api.common.frontend.viewmodels.FrontendViewModel8import com.tomtom.tools.android.api.livedata.allTrue9import com.tomtom.tools.android.api.livedata.valueUpToDate1011internal class AccountLoginViewModel(panel: AccountLoginPanel) :12 FrontendViewModel<AccountLoginPanel>(panel) {1314 // Create an instance of service's client API.15 private val accountsServiceApi =16 AccountsService.createApi(this, frontendContext.iviServiceProvider)1718 // Properties used by the layout.19 val username = MutableLiveData("")20 val password = MutableLiveData("")2122 // [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 )3031 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 used34 // with LiveData's transformations.35 isLoginEnabled.valueUpToDate?.takeIf { it }?.let {36 val username = username.value ?: return37 val password = password.value ?: return3839 // Log in an user with asynchronous call.40 accountsServiceApi.logInAsync(username, password)4142 // 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.info23import androidx.lifecycle.map4import com.example.ivi.example.plugin.serviceapi.AccountsService5import com.example.ivi.example.plugin.serviceapi.createApi6import com.tomtom.ivi.platform.frontend.api.common.frontend.viewmodels.FrontendViewModel7import java.util.Locale89internal class AccountInfoViewModel(panel: AccountInfoPanel) :10 FrontendViewModel<AccountInfoPanel>(panel) {1112 private val accountsServiceApi =13 AccountsService.createApi(this, frontendContext.iviServiceProvider)1415 val displayName = accountsServiceApi.activeAccount.map {16 it?.username?.replaceFirstChar(Char::uppercaseChar)17 }1819 fun onLogoutClick() = accountsServiceApi.logOutAsync()20}
Sharing data between service and clients
All properties of an IVI service are mirrored.