Customize the Wizard Generated Application
Customize a wizard-generated application and learn how to use Fiori object cells, search UI, and the collection view.
Overview
You will learn
- How to customize the values displayed in an object cell
- How to modify the navigation between screens
- How to change menu options
- How to enable filtering of object cells on a list screen
- How to add a collection view showing the top products
Prerequisites
Prerequisites
- You have Set Up a BTP Account for Tutorials. Follow the instructions to get an account, and then to set up entitlements and service instances for the following BTP services.
- SAP Mobile Services
- You completed Try Out the SAP BTP SDK Wizard for Android.
Steps
Run the previously created project.
Select Products.

Entities screen 
Original Products Screen The category name is displayed (rather than the product name) because the app was generated from the OData service’s metadata, which does not specify which of the many fields from the product entity to display. When creating the sample user interface, the SDK wizard uses the first property found as the value to display. To view the complete metadata document, open the
res/raw/com_sap_edm_sampleservice_v4.xmlfile.XML<EntityType Name="Product"> <Key> <PropertyRef Name="ProductID"/> </Key> <Property Name="Category" Type="Edm.String" Nullable="true" MaxLength="40"/> ... ... <Property Name="Name" Type="Edm.String" Nullable="false" MaxLength="80"/> ... ... </EntityType>Each product is displayed in an object cell, which is one of the Fiori UI controls for Android.

object cell As seen above, an object cell is used to display information about an entity.
In this section, you will configure the object cell to display a product’s name, category, description, and price.
In Android Studio, on Windows, press
Ctrl+shift+N, or on a Mac, presscommand+shift+O. TypeProductEntitiesScreento openProductEntitiesScreen.kt.On Windows, press
Ctrl+F, or on a Mac, presscommand+F, and typeviewModel.getEntityTitle(entity)to navigate to the linesetHeadline(viewModel.getEntityTitle(entity)). Change this line tosetHeadline(entity.getOptionalValue(Product.name).toString()). This will display the product name as the headline value of the object cell.If the class
Productappears in red, it indicates that Android Studio could not locate the class. Select the class, and on Windows pressAlt+Enter, or on a Mac, pressoption+returnto use Android Studio’s quick fix to add the missing imports.Alternatively, you can open the settings screen: Windows: Settings; Mac: Android Studio > Settings…. Then go to Editor > General > Auto Import, enable Add unambiguous imports on the fly in the
Kotlinsection.In the same code block, replace
setSubheadlineandsetFootnote, and addsetStatusInfoLabelwith the following code, which will display the category, description, and price.KotlinsetSubheadline(entity.getDataValue(Product.category).toString()) setFootnote(entity.getDataValue(Product.shortDescription).toString()) setStatusInfoLabel( FioriStatusInfoLabelDataInOC( items = listOf( FioriLabelItemData( label = "$ ${entity.getDataValue(Product.price).toString()}" ) ) ) )On Windows, press
Ctrl+F, or on a Mac, presscommand+F, and typeFioriObjectCellto navigate to theFioriObjectCellinvocation.Add the following code right after
FioriObjectCellinvocation, and add it right beforeif (entities.loadState.refresh == LoadState.Loading)at the same time, to add a divider between the product items.KotlinFioriDivider()On Windows, press
Ctrl+N, or on a Mac, presscommand+O, and typeRepositoryto openRepository.kt.On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typereadto navigate to theread(pageSize: Int = 40, page: Int = 0, query: DataQuery? = null)method.Replace the
orderByProperty?.alsoblock with the following code to specify that the sort order should be by category and then by the name of the products.KotlinorderByProperty?.also { dataQuery.orderBy(it, SortOrder.ASCENDING) if (entitySet.entityType == ESPMContainerMetadata.EntityTypes.product) { dataQuery.thenBy(Product.name, SortOrder.ASCENDING) } }Quit the app and then re-run it. You’ll see that the Products screen has been updated to display the product’s name, category, description, and price, with the entries sorted by category and then by name.

Nicely formatted product list
In Android Studio, on Windows, press
Ctrl+N, or on a Mac, presscommand+O. TypeProductsListFragmentto openProductsListFragment.kt.On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typepopulateObjectCellto navigate to thepopulateObjectCellmethod. Change the parameter in the first line of the method fromgetOptionalValue(Product.category)togetOptionalValue(Product.name). This will ensure that the product name is displayed as the headline value of the object cell:Kotlinval dataValue = productEntity.getOptionalValue(Product.name)Replace the
viewHolder.objectCell.applyblock with the following code, which will display the category, description, and price.KotlinviewHolder.objectCell.apply { headline = masterPropertyValue setUseCutOut(false) (productEntity.getDataValue(Product.category))?.let { subheadline = it.toString() } (productEntity.getDataValue(Product.shortDescription))?.let { footnote = it.toString() } (productEntity.getDataValue(Product.price))?.let { statusWidth = 200 setStatus("$ $it", 1) } }On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typeonViewStateRestoredto navigate to theonViewStateRestoredmethod.Replace
fragmentBinding.itemList?.letblock with the following code, which adds a divider between product items.KotlinfragmentBinding.itemList.let { val linearLayoutManager = LinearLayoutManager(currentActivity) val dividerItemDecoration = DividerItemDecoration(it.context, linearLayoutManager.orientation) it.addItemDecoration(dividerItemDecoration) it.layoutManager = linearLayoutManager this.adapter = ProductListAdapter(currentActivity, it) it.adapter = this.adapter }If the classes
LinearLayoutManagerandDividerItemDecorationappear in red, it indicates that Android Studio could not locate them. Select each class, and on Windows, pressAlt+Enter, or on a Mac, pressoption+returnto use Android Studio’s quick fix to add the missing imports.Alternatively, you can enable the following setting: Windows: Settings; Mac: Android Studio > Settings…. Then go to Editor > General > Auto Import, enable Add unambiguous imports on the fly in the
Kotlinsection.On Windows, press
Ctrl+N, or on a Mac, presscommand+O, and typeRepositoryto openRepository.kt.On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typereadto move to theread()method.Replace the
if (!entitySet.isSingleton && orderByProperty != null)block with the following code to specify that the sort order should be by category and then by the name of the products.Kotlinif (!entitySet.isSingleton && orderByProperty != null) { dataQuery = dataQuery.orderBy(orderByProperty, SortOrder.ASCENDING) if (entitySet.entityType == ESPMContainerMetadata.EntityTypes.product) { dataQuery.thenBy(Product.name, SortOrder.ASCENDING) } }Quit the app and then re-run it. You’ll see that the Products screen has been updated to display the product’s name, category, description, and price, with the entries sorted by category and then by name.

Nicely formatted product list
Examine the ProductCategories screen.

In this section, you will update the screen’s title, configure the object cell to display the category name and main category name, add the number of products in each category, and include a separator decoration between cells.
Press
Shifttwice and typestrings.xmlto openres/values/strings.xml.Add the following entry:
XML<string name="product_categories_title">Product Categories</string>On Windows, press
Ctrl+Shift+N, or on a Mac, presscommand+shift+O, and typeProductCategoryEntitiesScreen, to openProductCategoryEntitiesScreen.kt.On Windows, press
Ctrl+F, or on a Mac, presscommand+F, and typetitleto move to thetitleline.On Windows, press
Ctrl+/, or on a Mac, presscommand+/, to comment out the line.Add the following line right after the commented-out line to set the screen’s title:
Kotlintitle = stringResource(id = R.string.product_categories_title),On Windows, press
Ctrl+F, or on a Mac, presscommand+F, and typeFioriObjectCellto navigate to theFioriObjectCellinvocation.Add the following code right after
FioriObjectCellinvocation and add it right beforeif (entities.loadState.refresh == LoadState.Loading)at the same time, which adds a divider between categories:KotlinFioriDivider()On Windows, press
Ctrl+F, or on a Mac, presscommand+F, and typeFioriObjectCellData, to move to theFioriObjectCellDatacode block.Replace the value of
objectCellDatawith the following to display the main category instead, hide the footnote, and show the number of products per category.Kotlinval objectCellData = FioriObjectCellData.Builder().apply { setHeadline(viewModel.getEntityTitle(entity)) setSubheadline(entity.getDataValue(ProductCategory.mainCategoryName).toString()) setAvatar(avatar) setStatusInfoLabel( FioriStatusInfoLabelDataInOC( items = listOf( FioriLabelItemData( label = "${entity.getDataValue(ProductCategory.numberOfProducts).toString()} Products" ) ) ) ) }.build()Run the app again. You’ll see that the title, subheadline, and status are now displayed, while the icon and footnote are no longer visible.

Modified ProductCategories Screen
Examine the ProductCategories screen.

In this section, you will update the screen’s title, configure the object cell to display the category name and main category name, add the number of products in each category, and include a separator decoration between cells.
Press
Shifttwice and typestrings.xmlto openres/values/strings.xml.Add the following entry:
XML<string name="product_categories_title">Product Categories</string>On Windows, press
Ctrl+N, or on a Mac, presscommand+O, and typeProductCategoriesListFragment, to openProductCategoriesListFragment.kt.On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typeonViewStateRestoredto move to theonViewStateRestoredmethod, find thecurrentActivity.title = activityTitleline.On Windows, press
Ctrl+/, or on a Mac, presscommand+/, to comment out the line.Add the following line right after the commented-out line to set the screen’s title:
KotlincurrentActivity.title = resources.getString(R.string.product_categories_title)Still in this method, replace the
fragmentBinding.itemList?.letblock with the following code, which adds a divider between categories:KotlinfragmentBinding.itemList.let { val linearLayoutManager = LinearLayoutManager(currentActivity) val dividerItemDecoration = DividerItemDecoration(it.context, linearLayoutManager.orientation) it.addItemDecoration(dividerItemDecoration) it.layoutManager = linearLayoutManager this.adapter = ProductCategoryListAdapter(currentActivity, it) it.adapter = this.adapter }On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typepopulateObjectCell, to move to thepopulateObjectCellmethod.Replace the
viewHolder.objectCell.applyblock with the following to display the main category instead, hide the footnote, and show the number of products per category.KotlinviewHolder.objectCell.apply { headline = masterPropertyValue detailImage = null setDetailImage(viewHolder, productCategoryEntity) (productCategoryEntity.getDataValue(ProductCategory.mainCategoryName))?.let { subheadline = it.toString() } lines = 2 //Not using footnote (productCategoryEntity.getDataValue(ProductCategory.numberOfProducts))?.let { statusWidth = 220 setStatus("$it Products", 1) } }Run the app again. You’ll see that the title, subheadline, and status are now displayed, while the icon and footnote are no longer visible.

Modified ProductCategories Screen
In this section, you will modify the app to initially show the Product Categories screen when opened. Selecting a category will navigate to a Products screen for the selected category. The floating action button on the Categories screen will be removed.
On Windows, press
Ctrl+Shift+N, or on a Mac, presscommand+Shift+O, and typeODataNavHost, to openODataNavHost.kt.Change the
startDestinationofNavHostto:KotlinEntityNavigationCommands(ESPMContainerMetadata.EntityTypes.productCategory).entityListNav.routeAdd the following composable content before
composable(route = EntitySetsDest.route)code block:Kotlincomposable(route = EntityNavigationCommands(ESPMContainerMetadata.EntityTypes.productCategory).entityListNav.route) { val viewModel: ODataViewModel<EntityValue> = viewModel( factory = ODataEntityViewModelFactory( LocalContext.current.applicationContext as Application, ESPMContainerMetadata.EntityTypes.productCategory, ESPMContainerMetadata.EntitySets.productCategories, getOrderByProperty(ESPMContainerMetadata.EntityTypes.productCategory), ) ) ODataScreen( navController, isExpandedScreen, viewModel, ProductCategoryEntitiesExpandScreen, ProductCategoryEntitiesScreen, ProductCategoryEntityEditScreen, ProductCategoryEntityDetailScreen ) }This will cause the Product Categories screen to be the first screen seen when opening the app.
On Windows, press
Ctrl+N, or on a Mac, presscommand+O, and typeODataViewModel, to openODataViewModel.kt.On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typeonFloatingAdd, to navigate toonFloatingAddmethod.Replace the
onFloatingAddfunction with the following code:Kotlinopen fun onFloatingAdd(): (() -> Unit)? { val action = { onCreateAction() refreshEntities() } return parent?.let { parent -> return navigationPropertyName?.let { val navProp = parent.entityType.getProperty(navigationPropertyName) val navValue = parent.getOptionalValue(navProp) if (navProp.isEntityList || navProp.isComplexList || navValue == null) action else null } ?: action } ?: action }On Windows, press
Ctrl+Shift+N, or on a Mac, presscommand+Shift+O, and typeProductEntitiesScreen, to openProductEntitiesScreen.kt.Add a variable in the method body, after the variable “viewModel”, to retrive the selected category name from
ODataViewModel:Kotlinval category = (viewModel.parent as? ProductCategory)?.categoryNameOn Windows, press
Ctrl+F, or on a Mac, presscommand+F, and typereturn@itemsto locate the lineval entity = entities[index] ?: return@items. Immediately after this line, add the following code to filter the products list to display only the products for the selected category:Kotlincategory?.also { if((entity as Product).category != it) { return@items } }On Windows, press
Ctrl+Shift+N, or on a Mac, presscommand+Shift+O, and typeODataNavHost, to openODataNavHost.kt.Replace the
if (!uiState.isEntityFocused)block with the following:Kotlinif (!uiState.isEntityFocused) { entityListScreen( { if ((viewModel as EntityViewModel).entityType != ESPMContainerMetadata.EntityTypes.productCategory) { navController.navigate(EntitySetsDest.route) } }, { if ((viewModel as EntityViewModel).entityType == ESPMContainerMetadata.EntityTypes.productCategory) { navController.navigate(EntitySetsDest.route) } else { navController.navigateUp() } }, viewModel, false ) }You can navigate to the EntityList screen by pressing the Back button on the Product Categories screen. The EntityList screen retains the Settings menu for convenience.
Replace the
EntityOperationType.DETAILcode block with the following, which will enable the navigation from the Category list screen to the Product list screen.KotlinEntityOperationType.DETAIL -> if ((viewModel as EntityViewModel).entityType == ESPMContainerMetadata.EntityTypes.productCategory) { viewModel.lostEntityFocus() val productCategory = uiState.masterEntity as ProductCategory navController.currentBackStackEntry?.savedStateHandle?.set( key = "productCategory", value = productCategory ) navController.navigate(EntityNavigationCommands(ESPMContainerMetadata.EntityTypes.product).entityListNav.route) } else { entityDetailScreen( onNavigateProperty, viewModel::lostEntityFocus, viewModel, false ) }Replace the
composable(route = EntityNavigationCommands(entityType).entityListNav.route)block with the following code so that the product screen can retrieve the selected category name:Kotlincomposable(route = EntityNavigationCommands(entityType).entityListNav.route) { ODataScreen( navController, isExpandedScreen, viewModel( factory = ODataEntityViewModelFactory( LocalContext.current.applicationContext as Application, entityType, entitySet, getOrderByProperty(entityType), if (entityType == ESPMContainerMetadata.EntityTypes.product) navController.previousBackStackEntry?.savedStateHandle?.get<ProductCategory>( "productCategory" ) else null ) ), entityExpandScreen, entityListScreen, entityEditScreen, entityDetailScreen ) }On Windows, press
Ctrl+Shift+N, or on a Mac, presscommand+Shift+O, and typeProductCategoryEntitiesScreen, to openProductCategoryEntitiesScreen.kt.Set
floatingActionClickinOperationScreenSettingsofOperationScreentonullinstead ofviewModel.onFloatingAdd().On Windows, press
Ctrl+Shift+N, or on a Mac, presscommand+Shift+O, and typeEntityScreenCommonUI, to openEntityScreenCommonUI.kt.Find the
ActionItemofR.string.menu_homeand change its overflowMode to the following:KotlinoverflowMode = if((viewModel as EntityViewModel).entityType == ESPMContainerMetadata.EntityTypes.productCategory) OverflowMode.NOT_SHOWN else OverflowMode.IF_NECESSARY,Run the app again. You’ll see that the Product Categories screen is now the first screen displayed, the Home menu is no longer visible, and selecting a category shows the products list screen, which now displays only the products for that selected category.

Product category list screen
On Windows, press
Ctrl+N, or on a Mac, presscommand+O, and typeMainBusinessActivity, to openMainBusinessActivity.kt.On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typestartEntitySetListActivity, to move to thestartEntitySetListActivitymethod.Add the following line below the other Intent declaration:
Kotlinval pcIntent = Intent(this, ProductCategoriesActivity::class.java)After the call to
startActivity(intent), add the following line:KotlinstartActivity(pcIntent)This will cause the Product Categories screen to be the first screen seen when opening the app, but because the EntityList screen is opened first, it can be navigated to by pressing the Back button. The EntityList screen retains the Settings menu for convenience.
On Windows, press
Ctrl+N, or on a Mac, presscommand+O, and typeProductCategoriesListFragment, to openProductCategoriesListFragment.kt.On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typeprepareViewModelto move to theprepareViewModelmethod.Replace the
fragmentBinding.fab?.letblock with the following code:KotlinfragmentBinding.fab?.let {createButton -> parentEntityData?.let {parent -> navigationPropertyName?.let { if (!isNavigationPropertyConnection && entityList.isNotEmpty()){ createButton.hide() } else { createButton.show() } } } }Add the
onCreateMenumethod into the class right after theonCreateViewmethod.Kotlinoverride fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { super.onCreateMenu(menu, menuInflater) menu.removeItem(R.id.menu_home) }On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typesetOnClickListener, to move to thesetOnClickListenermethod.Replace the code with the following, which will enable the navigation from the Category list screen to the Product list screen.
Kotlinholder.itemView.setOnClickListener { view -> val productsIntent = Intent(currentActivity, ProductsActivity::class.java) productsIntent.putExtra("category", productCategoryEntity.categoryName) view.context.startActivity(productsIntent) }On Windows, press
Ctrl+N, or on a Mac, presscommand+O, and typeProductsListFragment, to openProductsListFragment.kt.On Windows, press
Ctrl+F, or on a Mac, presscommand+F, and search forlistAdapter.setItems(entityList). Replace that line with the following code, which will filter the products list to display only the products for the selected category:KotlincurrentActivity.intent.getStringExtra("category")?.let { category -> val matchingProducts = arrayListOf<Product>() for (product in entityList) { product.category?.let { if (it == category) { matchingProducts.add(product) } } } listAdapter.setItems(matchingProducts) } ?: listAdapter.setItems(entityList)Run the app again. You’ll see that the Product Categories screen is now the first screen displayed, the Home menu is no longer visible, and selecting a category shows the products list screen, which now displays only the products for that selected category.

Product category list screen
In this section you will add a search field to Product Categories screen, allowing users to filter the results displayed on the screen.
First, right-click the
res/drawablefolder to create a new Drawable Resource Fileic_search_icon.xml, and use the following XML content.XML<?xml version="1.0" encoding="utf-8"?> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> <path android:fillColor="#000" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/> </vector>We will now use the new XML file for the Product Categories screen.
On Windows, press
Ctrl+Shift+N, or on a Mac, presscommand+Shift+O, and typestrings_localized, to open thestrings_localized.xmlfile.Add the following content:
XML<!-- XMIT: Search menu item --> <string name="menu_search">Search</string>On Windows, press
Ctrl+N, or on a Mac, presscommand+O, and typeBaseOperationViewModel, to open theBaseOperationViewModelclass.Add the following to the bottom of the class:
Kotlinprivate val _showSearchInput = MutableStateFlow(false) val showSearchInput: StateFlow<Boolean> = _showSearchInput private val _searchQuery = MutableStateFlow("") val searchQuery: StateFlow<String> = _searchQuery fun showSearchInput() { _showSearchInput.value = true } fun hideSearchInput() { _showSearchInput.value = false } fun onSearchQueryChanged(newText: String) { // Handle query text change _searchQuery.value = newText }On Windows, press
Ctrl+Shift+N, or on a Mac, presscommand+Shift+O, and typeEntityScreenCommonUI, to open theEntityScreenCommonUI.ktfile.Add the following
ActionItemright before the otherActionItems in the functiongetSelectedItemActionsList(navigateToHome: () -> Unit, viewModel: ODataViewModel, deleteState: MutableState<Boolean>). (Note that there is another function with the same name that takes two parameters.)KotlinActionItem( nameRes = R.string.menu_search, iconRes = if (viewModel.showSearchInput.collectAsState().value) R.drawable.ic_sap_icon_decline else R.drawable.ic_search_icon, overflowMode = if((viewModel as EntityViewModel).entityType == ESPMContainerMetadata.EntityTypes.productCategory) OverflowMode.IF_NECESSARY else OverflowMode.NOT_SHOWN, doAction = if (viewModel.showSearchInput.collectAsState().value) viewModel::hideSearchInput else viewModel::showSearchInput ),On Windows, press
Ctrl+Shift+N, or on a Mac, presscommand+Shift+O, and typeBaseOperationScreen, to open theBaseOperationScreen.ktfile.On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typeODataAppBar, to move to theODataAppBarmethod.Replace the contents of the method with the following code, which uses the new
ActionItemto enable and listen to the text entered in theOutlinedTextField. (Make sure to import all the un-imported classes withalt+Enteron Windows oroption+Enteron Macs.)Kotlin@OptIn(ExperimentalMaterial3Api::class) @Composable fun ODataAppBar( title: String, modifier: Modifier = Modifier, navigateUp: (() -> Unit)?, actionItems: List<ActionItem>, actionEnabled: Boolean = true, showSearchInput: Boolean, onSearchQueryChanged: (String) -> Unit ) { Column(modifier = Modifier) { TopAppBar( title = { if (showSearchInput) { //import androidx.compose.runtime.getValue //import androidx.compose.runtime.mutableStateOf //import androidx.compose.runtime.setValue var query by remember { mutableStateOf("") } OutlinedTextField( singleLine = true, value = query, onValueChange = { query = it onSearchQueryChanged(it) }, label = { Text("Search") }, colors = OutlinedTextFieldDefaults.colors( //import androidx.compose.ui.graphics.Color focusedBorderColor = Color.Transparent, unfocusedBorderColor = Color.Transparent, cursorColor = Color.Gray, errorCursorColor = Color.Red ), modifier = Modifier.fillMaxWidth() ) } else { Text(title) } }, modifier = modifier, navigationIcon = { navigateUp?.also { IconButton(onClick = it) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "" ) } } }, actions = { ActionMenu(actionItems, isEnabled = actionEnabled) } ) } }On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typeOperationScreen, to move to theOperationScreenmethod.Update the
topBarofScaffoldin the functionOperationScreenaccordingly.KotlintopBar = { ODataAppBar( title = screenSettings.title, modifier = modifier, navigateUp = screenSettings.navigateUp, actionItems = screenSettings.actionItems, actionEnabled = !operationUiState.value.inProgress, showSearchInput = viewModel.showSearchInput.collectAsState().value, onSearchQueryChanged = viewModel::onSearchQueryChanged ) },On Windows, press
Ctrl+Shift+N, or on a Mac, presscommand+Shift+O, and typeProductCategoryEntitiesScreen, to open theProductCategoryEntitiesScreen.ktfile.Add the following variables.
Kotlinval showSearchInput = viewModel.showSearchInput.collectAsState().value val searchQuery = viewModel.searchQuery.collectAsState().valueOn Windows, press
Ctrl+F, or on a Mac, presscommand+F, and typereturn@items, to find the code lineval entity = entities[index] ?: return@items. Right after the line, add the following code to filter the product category list to display only the categories that contain the searched text:Kotlinif (showSearchInput) { (entity as ProductCategory).categoryName?.let { if (!it.lowercase().contains(searchQuery.lowercase())) { return@items } } }Run the app again. You’ll notice that there is now a search toolbar item.

Filter Categories in action 1 Try it out: click the search item, enter some text, and notice that the product categories that are displayed in the list are now filtered.

Filter Categories in action 2
First, right-click the
res/drawablefolder to create a new Drawable Resource Fileic_search_icon.xml, and use the following XML content.XML<?xml version="1.0" encoding="utf-8"?> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> <path android:fillColor="#000" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/> </vector>The current menu
res/menu/itemlist_menu.xmlis shared among all list screens. We will now use a new XML file for the Product Categories screen.Right-click the
res/menufolder to add a new Menu Resource File namedproduct_categories_menu.xml, and use the following XML for its contents.XML<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/action_search" android:icon="@drawable/ic_search_icon" android:title="Search" app:actionViewClass="com.sap.cloud.mobile.fiori.search.FioriSearchView" app:showAsAction="always|collapseActionView" style="@style/FioriSearchView" /> <item android:id="@+id/menu_refresh" android:icon="@drawable/ic_sap_icon_refresh" app:showAsAction="always" android:title="@string/menu_refresh"/> </menu>On Windows, press
Ctrl+N, or on a Mac, presscommand+O, and typeProductCategoryListAdapter, to open theProductCategoryListAdapterclass, which is in theProductCategoriesListFragment.ktfile.Add the following member and methods to the top of this class.
Kotlinvar allProductCategories = listOf<ProductCategory>() fun setProductCategories(productCategories: MutableList<ProductCategory>) { this.productCategories = productCategories }On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typesetItems, to move to thesetItemsmethod.Add the following to the top of the function:
Kotlinif (allProductCategories.isEmpty()) { allProductCategories = currentProductCategories }On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typeonCreateMenu, to move to theonCreateMenumethod.Replace the contents of the method with the following code, which uses the new
product_categories_menuand sets a listener that will filter the list of categories in the list when text is entered in the search view. (Make sure to import all the un-imported classes withalt+Enteron Windows oroption+Enteron Macs.)KotlinmenuInflater.inflate(R.menu.product_categories_menu, menu) val searchView = menu.findItem(R.id.action_search).actionView as FioriSearchView searchView.setBackgroundResource(R.color.transparent) // make sure to import androidx.appcompat.widget.SearchView searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener { override fun onQueryTextSubmit(s: String): Boolean { return false } override fun onQueryTextChange(newText: String): Boolean { adapter?.let { adapter -> val filteredCategoriesList = mutableListOf<ProductCategory>() if (newText.trim().isNotEmpty()) { for (i in adapter.allProductCategories.indices) { val pc = adapter.allProductCategories[i] pc.categoryName?.let { if (it.lowercase().contains(newText.lowercase())) { filteredCategoriesList.add(pc) } } } } else { for (i in adapter.allProductCategories.indices) { filteredCategoriesList.add(adapter.allProductCategories[i]) } } adapter.setProductCategories(filteredCategoriesList) return false } ?: return false } })Run the app again. You’ll notice that there is now a search toolbar item.

Filter Categories in action 1 Try it out: click the search item, enter some text, press
Enter, and notice that the product categories that are displayed in the list are now filtered.
Filter Categories in action 2
Further information on the Fiori search UI can be found at SAP Fiori for Android Design Guidelines and Fiori Search User Interface.
In this section, you will add a Top Products section to the Products screen, which displays the products that have the most sales, as shown below.

First, we’ll generate additional sales data in the sample OData service.
In SAP Mobile Services cockpit, navigate to Mobile Applications > Native/MDK > btp.sdk.wizapp and go to Sample OData ESPM.
Change the Entity Sets dropdown to
SalesOrderItemsand then click the generate sample sales orders icon five times. This will create additional sales order items, which we can use to base our top products on, based on the quantity sold.
Generating Sample Sales Orders on Mobile Services In Android Studio, on Windows, press
Ctrl+Shift+N, or on a Mac, presscommand+Shift+O, and typeProductEntitiesScreen, to openProductEntitiesScreen.kt.Add the following import libraries to the top of the document:
Kotlinimport android.util.Log import com.sap.cloud.android.odata.espmcontainer.ESPMContainerMetadata import com.sap.cloud.android.odata.espmcontainer.SalesOrderItem import com.sap.cloud.mobile.fiori.compose.objectcell.ui.FioriCollectionViewLine import com.sap.cloud.mobile.kotlin.odata.DataQuery import com.sap.cloud.mobile.kotlin.odata.http.HttpHeaders import kotlinx.coroutines.runBlocking import java.util.LinkedListAdd the following variables to the bottom of the file:
Kotlinprivate var salesList = HashMap<String, Int>() private val productTracker = HashMap<String, Product>() private val productCollectionViewDataList = mutableListOf<FioriCollectionViewData>() private val productList = mutableListOf<Product>()Add the following method after the new added variables:
Kotlin// Function to query the products private suspend fun queryProducts() { val httpHeaders: HttpHeaders = if (EntityMediaResource.isV4(SAPServiceManager.eSPMContainer!!.metadata.versionCode) && EntityMediaResource.hasMediaResources(ESPMContainerMetadata.EntityTypes.product)) { val header = HttpHeaders() header.set("Accept", "application/json;odata.metadata=full") header } else HttpHeaders.empty val queryProduct = DataQuery().orderBy(Product.productID) Log.d("ProductEntitiesScreen", "CollectionView: $queryProduct") SAPServiceManager.eSPMContainer?.let{ it.getProducts(queryProduct, httpHeaders).forEach {product -> Log.d("ProductEntitiesScreen", "CollectionView ${product.name} : ${product.productID} : ${product.price}") productTracker[product.productID.toString()] = product } Log.d("ProductEntitiesScreen", "CollectionView: size of topProducts = ${productTracker.size}") //Order product list by the sorted sales list for ((key, value) in salesList) { val product = productTracker[key] ?: continue productList.add(product) val data = product.pictureUrl?.let { FioriCollectionViewData( avatarImage = FioriImage(EntityMediaResource.getMediaResourceUrl(product, SAPServiceManager.serviceRoot)!!), headline = product.name, subheadline = product.categoryName + "" ) } ?: FioriCollectionViewData(// No picture is available, so use a character from the product string as the image thumbnail avatarText = product.name.substring(0, 1), headline = product.name, subheadline = product.categoryName + "" ) productCollectionViewDataList.add(data) } } } // Query the SalesOrderItems and order by gross amount received from sales // Change the orderBy arguments to SalesOrderItem.productID to rearrange the CollectionView order of products private suspend fun querySales() { val httpHeaders: HttpHeaders = if (EntityMediaResource.isV4(SAPServiceManager.eSPMContainer!!.metadata.versionCode) && EntityMediaResource.hasMediaResources(ESPMContainerMetadata.EntityTypes.salesOrderItem)) { val header = HttpHeaders() header.set("Accept", "application/json;odata.metadata=full") header } else HttpHeaders.empty val querySales = DataQuery().orderBy(SalesOrderItem.productID) SAPServiceManager.eSPMContainer?.let{ it.getSalesOrderItems(querySales, httpHeaders).forEach {sale -> if (salesList.containsKey(sale.productID.toString())) { salesList[sale.productID.toString()] = salesList[sale.productID.toString()]!! + sale.quantity } else { salesList[sale.productID.toString()] = sale.quantity } Log.d("ProductEntitiesScreen","CollectionView ${sale.productID} : ${sale.quantity} : ${sale.grossAmount}") } salesList = sortByValue(salesList) Log.d("ProductEntitiesScreen", "CollectionView: salesList size = ${salesList.size}") queryProducts() } } // Function to sort hashmap by values private fun sortByValue(hashmap: HashMap<String, Int>): HashMap<String, Int> { // Create a list from elements of HashMap val list: MutableList<Map.Entry<String, Int>> = LinkedList<Map.Entry<String, Int>>(hashmap.entries) // Sort the list list.sortWith { o1, o2 -> o2.value.compareTo(o1.value) } // Put data from sorted list into the linked hashmap val temp: HashMap<String, Int> = LinkedHashMap<String, Int>() for ((key, value) in list) { temp[key] = value Log.d("ProductEntitiesScreen", "CollectionView: id = $key, count = $value") } return temp }In the body of
ProductEntitiesScreen, add the following variable:Kotlinvar showCollectionView = trueBefore the
if (entities.loadState.refresh == LoadState.Loading) {line and afterFioriDivider(), add the following code to add aCollectionViewto the product list screen:Kotlinif (showCollectionView) { runBlocking { querySales() } FioriCollectionViewLine( label = "Top Products", data = productCollectionViewDataList, onClick = { position, _ -> // If any object is clicked in CollectionView then the Product's detail page for that object will open Log.d("ProductEntitiesScreen", "You clicked on: ${productList[position].name}(${productList[position].productID})") onClickChange(productList[position]) }, footerButton = FooterButton( label = "SEE ALL (${productTracker.size})", onClick = { // If the footer "SEE ALL" is clicked then the Products page will open showCollectionView = false viewModel.refreshEntities() }), scrollable = true ) }Run the app. You’ll notice that the Products screen now has a component at the top of the screen that allows horizontal scrolling to view the top products. Tap a product to see more details. Alternatively, tap SEE ALL to see all the products.

Collection View on Products Screen
In this section, you will add a Top Products section to the Products screen, which displays the products that have the most sales, as shown below.

First, we’ll generate additional sales data in the sample OData service.
In SAP Mobile Services cockpit, navigate to Mobile Applications > Native/Hybrid > btp.sdk.wizapp and go to Sample OData ESPM.
Change the Entity Sets dropdown to
SalesOrderItemsand then click the generate sample sales orders icon five times. This will create additional sales order items, which we can use to base our top products on, based on the quantity sold.
Generating Sample Sales Orders on Mobile Services In Android Studio, on Windows, press
Ctrl+Shift+N, or on a Mac, presscommand+Shift+O, and typefragment_entityitem_list, to openfragment_entityitem_list.xml.Replace the
fragment_entityitem_list.xmlcontent with the following code. This adds theCollectionViewto the Products pane when created.XML<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="@dimen/fab_margin" android:src="@drawable/ic_sap_icon_add" app:tint="@color/colorWhite" app:backgroundTint="?attr/sap_fiori_color_accent_7" app:fabSize="normal" /> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:id="@+id/wrapperLayout" > <com.sap.cloud.mobile.fiori.object.CollectionView app:layout_scrollFlags="scroll|enterAlways" android:id="@+id/collectionView" android:layout_height="wrap_content" android:layout_width="match_parent" android:background="@color/transparent" tools:minHeight="200dp"> </com.sap.cloud.mobile.fiori.object.CollectionView> <androidx.swiperefreshlayout.widget.SwipeRefreshLayout android:id="@+id/swiperefresh" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/item_list" android:name="ItemListFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </LinearLayout> </FrameLayout>On Windows, press
Ctrl+N, or on a Mac, presscommand+O, and typeProductsListFragment, to openProductsListFragment.kt.Add the following import libraries to the top of the document:
Kotlinimport android.widget.LinearLayout import androidx.fragment.app.FragmentActivity import com.sap.cloud.android.odata.espmcontainer.SalesOrderItem import com.sap.cloud.mobile.fiori.common.FioriItemClickListener import com.sap.cloud.mobile.fiori.`object`.AbstractEntityCell import com.sap.cloud.mobile.fiori.`object`.CollectionView import com.sap.cloud.mobile.fiori.`object`.CollectionViewItem import com.sap.cloud.mobile.odata.DataQuery import com.sap.cloud.mobile.odata.http.HttpHeaders import java.util.* import kotlin.collections.ArrayList import kotlin.collections.HashMap import kotlin.collections.LinkedHashMapAdd the following variables to the top of the
ProductsListFragmentclass:Kotlinprivate val productList = arrayListOf<Product>() private var salesList = HashMap<String, Int>() private val productTracker = HashMap<String, Product>()On Windows, press
ctrl+F12, or on a Mac, presscommand+F12, and typeresetSelected, to move to theresetSelectedmethod.Change the modifier from
privatetointernalDo the same to
resetPreviouslyClickedmethod.Add the following method to the
companion objectsection:Kotlin// Function to sort hashmap by values fun sortByValue(hashmap: HashMap<String, Int>): HashMap<String, Int> { // Create a list from elements of HashMap val list: MutableList<Map.Entry<String, Int>> = LinkedList<Map.Entry<String, Int>>(hashmap.entries) // Sort the list list.sortWith { o1, o2 -> o2.value.compareTo(o1.value) } // Put data from sorted list into the linked hashmap val temp: HashMap<String, Int> = LinkedHashMap<String, Int>() for ((key, value) in list) { temp[key] = value LOGGER.debug("CollectionView: id = $key, count = $value") } return temp }Add the following methods to the class:
Kotlin// Function to query the products private fun queryProducts() { val sapServiceManager = (currentActivity.application as SAPWizardApplication).sapServiceManager val query = DataQuery().orderBy(Product.productID) LOGGER.debug("CollectionView $query") val espmContainer = sapServiceManager?.eSPMContainer espmContainer?.let { val httpHeaders: HttpHeaders = if (EntityMediaResource.isV4(it.metadata.versionCode) && EntityMediaResource.hasMediaResources(EntityTypes.product)) { val header = HttpHeaders() header.set("Accept", "application/json;odata.metadata=full") header } else HttpHeaders.empty it.getProductsAsync(query, {queryProducts: List<Product> -> LOGGER.debug("CollectionView: executed query in onCreate") for (product in queryProducts) { LOGGER.debug("CollectionView ${product.name} : ${product.productID} : ${product.price}") productTracker[product.productID.toString()] = product } LOGGER.debug("CollectionView: size of topProducts = ${queryProducts.size}") createTopProductsList() val cv: CollectionView = currentActivity.findViewById(R.id.collectionView) createCollectionView(cv) }, {re: RuntimeException -> LOGGER.debug("CollectionView: An error occurred during products async query: ${re.message}") }, httpHeaders) } } // Function to order product list by the sorted sales list private fun createTopProductsList() { for ((key, value) in salesList) { productList.add(productTracker[key]!!) } } // Function to set features of the CollectionView private fun createCollectionView(cv: CollectionView) { LOGGER.debug("CollectionView: in createCollectionView method") cv.apply { setHeader(" Top Products") setFooter(" SEE ALL (${productTracker.size})") // If the footer "SEE ALL" is clicked then the Products page will open setFooterClickListener { visibility = View.GONE } // If any object is clicked in CollectionView then the Product's detail page for that object will open setItemClickListener(object: FioriItemClickListener { override fun onClick(view: View, position: Int) { LOGGER.debug("You clicked on: ${productList[position].name}(${productList[position].productID})") showProductDetailActivity(view.context, UIConstants.OP_READ, productList[position]) } override fun onLongClick(view: View, position: Int) { Toast.makeText(currentActivity.applicationContext, "You long clicked on: $position", Toast.LENGTH_SHORT).show() } }) val collectionViewAdapter = CollectionViewAdapter(currentActivity, productList.toList()) setCollectionViewAdapter(collectionViewAdapter) } if (resources.getBoolean(R.bool.two_pane)) { refreshLayout = currentActivity.findViewById(R.id.swiperefresh) val linearLayout = currentActivity.findViewById<LinearLayout>(R.id.wrapperLayout) val height = linearLayout.height - cv.height refreshLayout.minimumHeight = height } } // Opens the product's detail page activity private fun showProductDetailActivity(context: Context, operation: String, productEntity: Product?) { productEntity?.let { LOGGER.debug("within showProductDetailActivity for ${it.name}") val isNavigationDisabled = (currentActivity as ProductsActivity).isNavigationDisabled if (isNavigationDisabled) { Toast.makeText(currentActivity, "Please save your changes first...", Toast.LENGTH_LONG).show() } else { adapter?.resetSelected() adapter?.resetPreviouslyClicked() viewModel.setSelectedEntity(it) listener?.onFragmentStateChange(UIConstants.EVENT_ITEM_CLICKED, it) } } } private class CollectionViewAdapter(activity: FragmentActivity, productList: List<Product>) : CollectionView.CollectionViewAdapter() { private val products: List<Product> private val currentActivity: FragmentActivity override fun onBindViewHolder(collectionViewItemHolder: CollectionViewItemHolder, i: Int) { val cvi: CollectionViewItem = collectionViewItemHolder.collectionViewItem val prod = products[i] val productName = prod.name cvi.apply { detailImage = null headline = productName subheadline = prod.categoryName + "" imageOutlineShape = AbstractEntityCell.IMAGE_SHAPE_OVAL prod.pictureUrl?.let { val sapServiceManager = (currentActivity.application as SAPWizardApplication).sapServiceManager prepareDetailImageView().scaleType = ImageView.ScaleType.FIT_CENTER sapServiceManager?.let {sapServiceManager -> Glide.with(currentActivity.applicationContext) .load(EntityMediaResource.getMediaResourceUrl(prod, sapServiceManager.serviceRoot)) // Import com.bumptech.glide.Glide for RequestOptions() .apply(RequestOptions().fitCenter()) .transition(DrawableTransitionOptions.withCrossFade()) .into(prepareDetailImageView()) } } ?: run { // No picture is available, so use a character from the product string as the image thumbnail detailImageCharacter = productName.substring(0, 1) setDetailCharacterBackgroundTintList(com.sap.cloud.mobile.fiori.R.color.sap_ui_contact_placeholder_color_1) } } } override fun getItemCount(): Int { return products.size } init { products = productList currentActivity = activity } }On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typeprepareViewModel, to move to theprepareViewModelmethod.Replace the
ViewModelProvider(currentActivity).get(ProductViewModel::class.java)line of the method with the following code:KotlinViewModelProvider(currentActivity).get(ProductViewModel::class.java).also { it.initialRead{errorMessage -> showError(errorMessage) } val cv: CollectionView = currentActivity.findViewById(R.id.collectionView) createCollectionView(cv) }On Windows, press
Ctrl+F12, or on a Mac, presscommand+F12, and typeonCreate, to move to theonCreatemethod.Add the following lines of code at the end of the method:
Kotlin// Query the SalesOrderItems and order by gross amount received from sales // Change the orderBy arguments to SalesOrderItem.productID to rearrange the CollectionView order of products val dq = DataQuery().orderBy(SalesOrderItem.productID) // Get the DataService class, which we will use to query the back-end OData service val espmContainer = sapServiceManager?.eSPMContainer espmContainer?.let { val httpHeaders: HttpHeaders = if (EntityMediaResource.isV4(it.metadata.versionCode) && EntityMediaResource.hasMediaResources(EntityTypes.salesOrderItem)) { val header = HttpHeaders() header.set("Accept", "application/json;odata.metadata=full") header } else HttpHeaders.empty it.getSalesOrderItemsAsync(dq, { querySales: List<SalesOrderItem>? -> LOGGER.debug("CollectionView: executed sales order query in onCreate") querySales?.let { querysales -> for (sale in querysales) { if (salesList.containsKey(sale.productID.toString())) { salesList[sale.productID.toString()] = salesList[sale.productID.toString()]!!.toInt() + sale.quantity } else { salesList[sale.productID.toString()] = sale.quantity } LOGGER.debug("CollectionView ${sale.productID} : ${sale.quantity} : ${sale.grossAmount}") } salesList = sortByValue(salesList) LOGGER.debug("CollectionView: salesList size = ${salesList.size}") queryProducts() } ?: LOGGER.debug("CollectionView: sales query list is null") }, { re: RuntimeException -> LOGGER.debug("CollectionView: An error occurred during async sales query: ${re.message}") }, httpHeaders) }Run the app. You’ll notice that the Products screen now has a component at the top of the screen that allows horizontal scrolling to view the top products. Tap a product to see more details. Alternatively, tap SEE ALL to see all the products.

Collection View on Products Screen
For more details, see Collection View in SAP Fiori for Android Design Guidelines and Collection View
For more information on SAP Fiori for Android and the generated app, see Fiori UI Overview, SAP Fiori for Android Design Guidelines, Fiori UI Demo Application and the
WizardAppReadme.mdfile located in the generated app.
Congratulations! You have now made use of SAP Fiori for Android and have an understanding of some of the ways that the wizard-generated application can be customized to show different fields on the list screens, add or remove menu items, perform a search, and use a collection view.
Resources
Discussion
Share feedback on this tutorial or join the conversation in SAP Community.