Use an IVI Data Source
The TomTom Digital Cockpit SDK is not available for general use.
Please contact us for more information.
Introduction to IVI Data Sources
Properties of an IVI service interface are
mirrored to all clients of the service. A property of type IviDataSource
can
be used in an IVI service interface 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
and only load the data that is required to show the visible elements.
Note: IviDataSource
is an experimental API.
Overview of the example application
The example application contains an AccountsService
which exposes all available accounts to its
clients. It uses an IviDataSource
for this. The example demonstrates how to implement an
IviDataSource
on the service side and how to use it in the account frontend.
The Plan
To implement and use an IviDataSource
we will:
Before you start, we highly recommend making yourself familiar with the
Android Paging library
architecture. We also recommend following the
Android Paging Codelab in advance of
following this tutorial. The concepts explained in the above references will make it much easier to
understand the concepts introduced in this tutorial, even if you do not intend to use a
RecyclerView
.
Also make yourself familiar with how to create an IVI service interface as explained here before following this tutorial.
The IVI service interface
The IviDataSource
interface is parameterized by two types. By an element type ('E') and a query
type (Q
). To use an IviDataSource
in the IVI service interface we need to define these two types
first.
Both the element type and the query type must be a type that is supported in an IVI service
interface. So for custom types, the type needs to implement the
Parcelable
interface.
The element type is the type of data exposed by the IviDataSource
. In this tutorial we are going
to expose accounts. As such we have to define an Account
class, like:
src/main/kotlin/com/example/ivi/example/plugin/common/Account.kt
1import android.os.Parcelable2import com.tomtom.ivi.platform.framework.api.common.uid.Uid3import java.time.Instant4import kotlinx.parcelize.Parcelize56/**7 * Contains all data of a user account.8 */9@Parcelize10data class Account(11 /**12 * Unique ID for the account.13 */14 val accountUid: Uid<Account> = Uid.new(),1516 /**17 * A string representing the name of the account.18 */19 val username: String,2021 /**22 * `true` if the user is logged in.23 */24 val loggedIn: Boolean = false,2526 /**27 * Date time when this user logged in for the last time.28 */29 val lastLogIn: Instant? = null30) : Parcelable
Next, we need to define the query type. The query type allows clients to specify which data they want to obtain from the data source. This can also, for instance, define the order in which the data needs to be provided. In this example we allow the client to select all available accounts or only select the accounts that are currently logged in. We also allow clients to sort the accounts on the username or on the last login date time. An example definition of the query type follows:
src/main/kotlin/com/example/ivi/example/plugin/serviceapi/AccountsDataSourceQuery.kt
1import android.os.Parcelable2import kotlinx.parcelize.Parcelize34@Parcelize5data class AccountsDataSourceQuery(6 val selection: Selection,7 val orderBy: Order8) : Parcelable {910 enum class Selection {11 ALL,12 LOGGED_IN_AT_LEAST_ONCE13 }1415 enum class Order {16 USERNAME,17 LAST_LOG_IN_TIME_DESCENDING18 }19}
Now that the element type and query types are defined, we can add the data source to an IVI service interface:
src/main/kotlin/com/example/ivi/example/plugin/serviceapi/AccountsService.kt
1import com.example.ivi.example.plugin.common.Account2import com.tomtom.ivi.platform.framework.api.common.annotations.IviExperimental3import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.IviDataSource4import com.tomtom.ivi.platform.framework.api.ipc.iviserviceannotations.IviService56@IviService(7 serviceId = "com.example.ivi.example.plugin.service"8)9interface AccountsService {10 /**11 * Indicates which account is currently active.12 * `null` if no account is logged in.13 */14 val activeAccount: Account?1516 /**17 * Data set of accounts. The accounts can be queried and sorted.18 */19 @IviExperimental20 val accounts: IviDataSource<Account, AccountsDataSourceQuery>2122 // ...23}
The data source implementation
To implement the data source on the client side we need a class that extends
MutableIviDataSource
. To construct this class we need to indicate whether
our implementation will support jumping. If jumping is not supported, only sequential pages are
loaded. If jumping is supported, it is possible that pages are skipped. A typical use case of this
is when a user performs a jump scroll through a list shown by means of a RecyclerView
. If a data
source implements reading elements, for instance, by keeping a database cursor open, the
implementation will need to detect the jump and move the cursor forwards or backwards before reading
new records from the cursor. Jumps can be detected based on the requested data index when
IviPagingSource.load
is called.
The MutableIviDataSource
requires us to implement one method: createIviPagingSource
. This
method is called every time a client requests a new set of pages for a given query. It has to
return a class that extends MutableIviPagingSource
.
To implement MutableIviPagingSource
, we have to implement the loadSizeLimit
property and the
loadWithLoadSizeLimited
method. The loadWithLoadSizeLimited
method is given an
IviPagingSource.LoadParam
instance. This instance defines which page to load and the number of
elements in the page (loadSize
) as requested by the client. If the client requests a page size
larger then the loadSizeLimit
property value, the given loadSize
is limited to the value of the
loadSizeLimit
property.
There are three types of loads that can be requested by the IviPagingSource.LoadParam
type:
- Refresh. First page is loaded or after a jump when jumping is supported.
- Append. Data after the previous page is loaded. For instance: The user is scrolling down.
- Prepend. Data before the previous page is loaded. For instance: The user is scrolling up.
The above, and in fact the whole IviPagingSource
API is based on Android's
PagingSource
class. So, if you are familiar with PagingSource
concepts, it will help you to implement
your MutableIviPagingSource
.
The loadWithLoadSizeLimited
method is a suspend method. The implementation should suspend
when loading data utilises IO. This is to prevent blocking of the calling thread.
In our example, MutableAccountsDataSource
class implements the data source. Note that this
example code is not very representative as the implementation is not loading any data from
a remote data source.
If your MutableIviPagingSource
implementation keeps resources open, ensure that an invalidate
callback is registered by calling registerInvalidatedCallback
. And close the resources in the
callback.
src/main/kotlin/com/example/ivi/example/plugin/service/MutableAccountsDataSource.kt
1import com.example.ivi.example.plugin.common.Account2import com.example.ivi.example.plugin.serviceapi.AccountsDataSourceQuery3import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.IviPagingSource4import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.MutableIviDataSource5import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.MutableIviPagingSource67internal class MutableAccountsDataSource : MutableIviDataSource<Account, AccountsDataSourceQuery>(8 jumpingSupported = true9) {10 override fun createPagingSource(11 query: AccountsDataSourceQuery12 ): MutableIviPagingSource<Account> =13 MutableAccountsPagingSource(query)1415 private class MutableAccountsPagingSource(16 val query: AccountsDataSourceQuery17 ) : MutableIviPagingSource<Account>() {18 override val loadSizeLimit = DATA_SOURCE_MAX_PAGE_SIZE1920 init {21 registerInvalidatedCallback {22 // Close resources if applicable.23 }24 }2526 override suspend fun loadWithLoadSizeLimited(27 loadParams: IviPagingSource.LoadParams28 ): IviPagingSource.LoadResult<Account> {29 // ...30 }31 }3233 companion object {34 const val DATA_SOURCE_MAX_PAGE_SIZE: Int = 10035 }36}
Next, initialize a MutableAccountsDataSource
instance in the StockAccountsService
.
src/main/kotlin/com/example/ivi/example/plugin/service/StockAccountsService.kt
1import com.example.ivi.example.plugin.common.Account2import com.example.ivi.example.plugin.serviceapi.AccountsServiceBase34internal class StockAccountsService(iviServiceHostContext: IviServiceHostContext) :5 AccountsServiceBase(iviServiceHostContext) {67 private val mutableAccountsDataSource = MutableAccountsDataSource()89 override fun onCreate() {10 super.onCreate()1112 accounts = mutableAccountsDataSource1314 // ...1516 serviceReady = true17 }18}
Don't forget to invalidate all active IviPagingSource
s when the data set is modified:
src/main/kotlin/com/example/ivi/example/plugin/service/MutableAccountsDataSource.kt
mutableAccountsDataSource.invalidateAllPagingSources()
Use the data source
Now, with the data source defined in the AccountsService
interface and implemented in the
StockAccountsService
, we can start using the data source. In this tutorial we will use the data
source in the AccountLoginViewModel
in two different ways:
The latter variant allows the data source to be represented in a RecyclerView
. When your aim is
to use the data source outside of a RecyclerView
, use the former variant.
Using LiveData
The following example maps the accounts
data source to a
LiveData
instance which
value is set to the account info of the the last logged in user:
src/main/kotlin/com/example/ivi/example/plugin/frontend/login/AccountLoginViewModel.kt
1import androidx.lifecycle.LiveData2import com.example.ivi.example.plugin.common.Account3import com.example.ivi.example.plugin.serviceapi.AccountsService4import com.example.ivi.example.plugin.serviceapi.AccountsDataSourceQuery5import com.example.ivi.example.plugin.serviceapi.createApi6import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.IviDataSource7import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.first8import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.mapQuery9import com.tomtom.ivi.platform.frontend.api.common.frontend.viewmodels.FrontendViewModel10import kotlinx.coroutines.flow.Flow1112internal class AccountLoginViewModel(panel: AccountLoginPanel) :13 FrontendViewModel<AccountLoginPanel>(panel) {1415 private val accountsServiceApi =16 AccountsService.createApi(this, frontendContext.iviServiceProvider)1718 /**19 * Converts an [IviDataSource] [LiveData] to an [Account] [LiveData], the value of which is set20 * to the first item of the query result set.21 */22 val lastLogin: LiveData<Account> =23 accountsServiceApi.accounts.mapQuery(lastLoginQuery).first()2425 companion object {26 private val lastLoginQuery = AccountsDataSourceQuery(27 selection = AccountsDataSourceQuery.Selection.LOGGED_IN_AT_LEAST_ONCE,28 orderBy = AccountsDataSourceQuery.Order.LAST_LOG_IN_TIME_DESCENDING29 )30 }31}
In the above example, the IviDataSource
LiveData
is transformed to an IviPagingSource
LiveData
for the given lastLoginQuery
by the mapQuery
function. The mapQuery
will create
a new IviPagingSource
each time the previous paging source is invalidated. The IviPagingSource
LiveData
instance is transformed to the first Account
of the paging source by the first
function.
It is also possible to use other transformations. A mapQuery
extension exists which takes a
transformation lambda as an argument. The lambda is provided with a
PageProvider instance to load pages for the created IviPagingSource
.
See this page for binding
the Account
LiveData
to a view.
Using a Flow
of PagingData
To expose a data source in an RecyclerView
, you typically need to construct a
Pager
instance to
create pairs of
PagingData
and
PagingSource
instances. To construct the Pager
instance you need to provide it with a
PagingConfig
instance. The Pager
provides a Flow
of PagingData
.
The platform_framework_api_ipc_iviserviceandroidpaging
module provides
extension functions to convert an IviDataSource
or an IviDataSource
LiveData
to a Flow
of
PagingData
. This creates the Pager
instance under the hood. To use these extensions you need to
provide the PagingConfig
instance too.
The following example maps all Accounts
from the accounts
data source to a Flow
of
PagingData
.
src/main/kotlin/com/example/ivi/example/plugin/frontend/login/AccountLoginViewModel.kt
1import androidx.lifecycle.LiveData2import androidx.paging.PagingConfig3import androidx.paging.PagingData4import com.example.ivi.example.plugin.common.Account5import com.example.ivi.example.plugin.serviceapi.AccountsService6import com.example.ivi.example.plugin.serviceapi.AccountsDataSourceQuery7import com.example.ivi.example.plugin.serviceapi.createApi8import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.IviDataSource9import com.tomtom.ivi.platform.framework.api.ipc.iviserviceandroidpaging.mapPagingData10import com.tomtom.ivi.platform.frontend.api.common.frontend.viewmodels.FrontendViewModel11import kotlinx.coroutines.flow.Flow1213internal class AccountLoginViewModel(panel: AccountLoginPanel) :14 FrontendViewModel<AccountLoginPanel>(panel) {1516 private val accountsServiceApi =17 AccountsService.createApi(this, frontendContext.iviServiceProvider)1819 /**20 * Converts an [IviDataSource] [LiveData] to a [Flow] of [PagingData]. This21 * flow can be bound to an `RecyclerView`. See Android Paging library for details.22 */23 val allAccountsPagingDataFlow: Flow<PagingData<Account>> = accountsServiceApi.accounts24 .mapPagingData(pagingConfig, allAccountsQuery, this)2526 companion object {27 private val allAccountsQuery = AccountsDataSourceQuery(28 selection = AccountsDataSourceQuery.Selection.ALL,29 orderBy = AccountsDataSourceQuery.Order.USERNAME30 )3132 private val pagingConfig = PagingConfig(33 pageSize = 1034 )35 }36}
In the above example mapPagingData
is given the PagingConfig
instance, the allAccountsQuery
instance and a lifecycle owner (this
).
You can bind the Flow
of PagingData
to a RecyclerView
as explained in the
Android Paging library
and Android Paging Codelab documentation.