Use an IVI Data Source

Last edit: 2023.07.27

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 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 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.Parcelable
2import com.tomtom.ivi.platform.framework.api.common.uid.Uid
3import java.time.Instant
4import kotlinx.parcelize.Parcelize
5
6/**
7 * Contains all data of a user account.
8 */
9@Parcelize
10data class Account(
11 /**
12 * Unique ID for the account.
13 */
14 val accountUid: Uid<Account> = Uid.new(),
15
16 /**
17 * A string representing the name of the account.
18 */
19 val username: String,
20
21 /**
22 * `true` if the user is logged in.
23 */
24 val loggedIn: Boolean = false,
25
26 /**
27 * Date time when this user logged in for the last time.
28 */
29 val lastLogIn: Instant? = null
30) : 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.Parcelable
2import kotlinx.parcelize.Parcelize
3
4@Parcelize
5data class AccountsDataSourceQuery(
6 val selection: Selection,
7 val orderBy: Order
8) : Parcelable {
9
10 enum class Selection {
11 ALL,
12 LOGGED_IN_AT_LEAST_ONCE
13 }
14
15 enum class Order {
16 USERNAME,
17 LAST_LOG_IN_TIME_DESCENDING
18 }
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.Account
2import com.tomtom.ivi.platform.framework.api.common.annotations.IviExperimental
3import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.IviDataSource
4import com.tomtom.ivi.platform.framework.api.ipc.iviserviceannotations.IviService
5
6@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?
15
16 /**
17 * Data set of accounts. The accounts can be queried and sorted.
18 */
19 @IviExperimental
20 val accounts: IviDataSource<Account, AccountsDataSourceQuery>
21
22 // ...
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.Account
2import com.example.ivi.example.plugin.serviceapi.AccountsDataSourceQuery
3import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.IviPagingSource
4import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.MutableIviDataSource
5import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.MutableIviPagingSource
6
7internal class MutableAccountsDataSource : MutableIviDataSource<Account, AccountsDataSourceQuery>(
8 jumpingSupported = true
9) {
10 override fun createPagingSource(
11 query: AccountsDataSourceQuery
12 ): MutableIviPagingSource<Account> =
13 MutableAccountsPagingSource(query)
14
15 private class MutableAccountsPagingSource(
16 val query: AccountsDataSourceQuery
17 ) : MutableIviPagingSource<Account>() {
18 override val loadSizeLimit = DATA_SOURCE_MAX_PAGE_SIZE
19
20 init {
21 registerInvalidatedCallback {
22 // Close resources if applicable.
23 }
24 }
25
26 override suspend fun loadWithLoadSizeLimited(
27 loadParams: IviPagingSource.LoadParams
28 ): IviPagingSource.LoadResult<Account> {
29 // ...
30 }
31 }
32
33 companion object {
34 const val DATA_SOURCE_MAX_PAGE_SIZE: Int = 100
35 }
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.Account
2import com.example.ivi.example.plugin.serviceapi.AccountsServiceBase
3
4internal class StockAccountsService(iviServiceHostContext: IviServiceHostContext) :
5 AccountsServiceBase(iviServiceHostContext) {
6
7 private val mutableAccountsDataSource = MutableAccountsDataSource()
8
9 override fun onCreate() {
10 super.onCreate()
11
12 accounts = mutableAccountsDataSource
13
14 // ...
15
16 serviceReady = true
17 }
18}

Don't forget to invalidate all active IviPagingSources 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.LiveData
2import com.example.ivi.example.plugin.common.Account
3import com.example.ivi.example.plugin.serviceapi.AccountsService
4import com.example.ivi.example.plugin.serviceapi.AccountsDataSourceQuery
5import com.example.ivi.example.plugin.serviceapi.createApi
6import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.IviDataSource
7import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.first
8import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.mapQuery
9import com.tomtom.ivi.platform.frontend.api.common.frontend.viewmodels.FrontendViewModel
10import kotlinx.coroutines.flow.Flow
11
12internal class AccountLoginViewModel(panel: AccountLoginPanel) :
13 FrontendViewModel<AccountLoginPanel>(panel) {
14
15 private val accountsServiceApi =
16 AccountsService.createApi(this, frontendContext.iviServiceProvider)
17
18 /**
19 * Converts an [IviDataSource] [LiveData] to an [Account] [LiveData], the value of which is set
20 * to the first item of the query result set.
21 */
22 val lastLogin: LiveData<Account> =
23 accountsServiceApi.accounts.mapQuery(lastLoginQuery).first()
24
25 companion object {
26 private val lastLoginQuery = AccountsDataSourceQuery(
27 selection = AccountsDataSourceQuery.Selection.LOGGED_IN_AT_LEAST_ONCE,
28 orderBy = AccountsDataSourceQuery.Order.LAST_LOG_IN_TIME_DESCENDING
29 )
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.LiveData
2import androidx.paging.PagingConfig
3import androidx.paging.PagingData
4import com.example.ivi.example.plugin.common.Account
5import com.example.ivi.example.plugin.serviceapi.AccountsService
6import com.example.ivi.example.plugin.serviceapi.AccountsDataSourceQuery
7import com.example.ivi.example.plugin.serviceapi.createApi
8import com.tomtom.ivi.platform.framework.api.ipc.iviservice.datasource.IviDataSource
9import com.tomtom.ivi.platform.framework.api.ipc.iviserviceandroidpaging.mapPagingData
10import com.tomtom.ivi.platform.frontend.api.common.frontend.viewmodels.FrontendViewModel
11import kotlinx.coroutines.flow.Flow
12
13internal class AccountLoginViewModel(panel: AccountLoginPanel) :
14 FrontendViewModel<AccountLoginPanel>(panel) {
15
16 private val accountsServiceApi =
17 AccountsService.createApi(this, frontendContext.iviServiceProvider)
18
19 /**
20 * Converts an [IviDataSource] [LiveData] to a [Flow] of [PagingData]. This
21 * flow can be bound to an `RecyclerView`. See Android Paging library for details.
22 */
23 val allAccountsPagingDataFlow: Flow<PagingData<Account>> = accountsServiceApi.accounts
24 .mapPagingData(pagingConfig, allAccountsQuery, this)
25
26 companion object {
27 private val allAccountsQuery = AccountsDataSourceQuery(
28 selection = AccountsDataSourceQuery.Selection.ALL,
29 orderBy = AccountsDataSourceQuery.Order.USERNAME
30 )
31
32 private val pagingConfig = PagingConfig(
33 pageSize = 10
34 )
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.