UI extensibility options
When following the guides such as HAAPI to OAuth, Theming and WidgetConfiguration.Builder options, the WidgetConfiguration,
and the theming/resources XML files drive the UI elements for HAAPI. These configurations might be sufficient to align with the design of the main application. However, if these configurations are not enough, it is possible to have "advanced" customizations such as:
- [providing custom fragments for any HAAPI states](#Providing custom fragments for any HAAPI states)
- [providing custom objects and overriding the mappings of objects that are passed to the UI elements for HAAPI](#Providing custom objects and overriding the mappings of objects that are passed to the UI elements for HAAPI)
Providing custom fragments for any HAAPI states
The HAAPI UI.Widget
uses HAAPI representations that are mapped into UIModel
that generate fragments via a FragmentResolver
.
The table below lists which fragments are handling the UIModels, by default.
UIModel | Fragment that is a subclass of HaapiFlowFragment |
---|---|
ContinueSameModel | FormFragment |
FormModel | FormFragment |
GenericModel | GenericFragment |
PollingModel | PollingFragment |
ProblemModel | ProblemFragment |
SelectorModel | SelectorFragment |
WebAuthnOperationModel | WebAuthnFragment |
The FragmentResolver
is an interface that is used by HaapiUIWidgetApplication
and the definition is the following.
interface FragmentResolver {
/**
* Resolves a Fragment instance for the provided model.
*
* @param uiModel An instance of [UIModel] with the current flow state information.
*/
fun <T: UIModel> getFragment(uiModel: T): HaapiFlowFragment<T>
}
By default, HaapiUIWidgetApplication
uses HaapiFlowFragmentResolver
which can be customized by using the HaapiFlowFragmentResolver.Builder
. The following snippet illustrates how to use the Builder:
class ClientApplication : Application(), HaapiUIWidgetApplication {
private val baseUri = URI.create("https://10.0.2.2:8443")
override val fragmentResolver: FragmentResolver =
HaapiFlowFragmentResolver.Builder()
// NewFormFragment is a subclass of FormFragment and uses the pattern `newInstance`
.customize(FormModel::class.java, NewFormFragment::newInstance)
.build()
...
}
Finally, HaapiFlowActivity
uses the generated fragments.
There are different methods to customize fragments for any HAAPI states:
Customization Method | What for |
---|---|
[Customizing an existing fragment using a different XML layout](#Customizing an existing fragment using a different XML layout) | Redesigns the structure of the XML, adds/removes static UI elements, and keeps the default implementation. Only the XML layout file is changed. |
[Customizing an existing fragment by subclassing default implementation](#Customizing an existing fragment by subclassing default implementation) | Adds/removes UI elements to the fragment and/or redesigns the structure of the XML. An implementation is required and the layout may change. |
[Providing a custom fragment that subclass HaapiFlowFragment](#Providing a custom fragment that subclass HaapiFlowFragment) | Unsatisfied with the UI and default implementation. Creates a new fragment that conforms to HaapiFlowFragment by managing on your own the UI design that uses the UIModel. |
Customizing an existing fragment using a different XML layout
An existing fragment can be customized by providing a different XML layout when :
- the customized fragment does not require additional code to be displayed
- the fragment default behaviour implementation does not change
- the structure of the XML for the customized fragment can be different than the original one: adding new static UI elements or removing existing UI elements.
To start, it is required to know which fragment to target.
To illustrate this method, FormFragment
is customized with a different XML layout displaying the following structure.
- Static text
- Links
- InfoView
- Form
instead of
- InfoView
- Form
- Links
The new XML that is named fragment_redesign_form
looks like this:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/hui_fragment_form_layout"
>
<TextView
android:id="@+id/a_custom_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="A new redesign view"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/hui_fragment_form_links_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/a_custom_text_view"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:overScrollMode="never"/>
<Space
android:id="@+id/hui_fragment_form_bottom_space"
android:layout_width="match_parent"
android:layout_height="@dimen/hui_fragment_form_spacing"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/hui_fragment_form_links_recycler_view"
app:layout_constraintEnd_toEndOf="parent"
/>
<se.curity.identityserver.haapi.android.ui.widget.components.InfoView
android:id="@+id/hui_fragment_form_info_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/hui_fragment_form_links_recycler_view"
app:layout_constraintEnd_toEndOf="parent"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/hui_fragment_form_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/hui_fragment_form_info_view"
app:layout_constraintEnd_toEndOf="parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:overScrollMode="never"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
Override HaapiUIWidgetApplication.fragmentResolver
in your application by using the new FormFragment.
class ClientApplication : Application(), HaapiUIWidgetApplication {
private val baseUri = URI.create("https://10.0.2.2:8443")
override val widgetConfiguration: WidgetConfiguration =
WidgetConfiguration.Builder(
clientId = "clientId",
baseUri = baseUri,
tokenEndpointUri = baseUri.resolve("/dev/oauth/token"),
authorizationEndpointUri = baseUri.resolve("/dev/oauth/authorize"),
appRedirect = "app://haapi"
)
.setKeyStoreAlias("keystore")
.setHttpUrlConnectionProvider(UNCHECKED_CONNECTION_PROVIDER)
.build()
override val fragmentResolver: FragmentResolver =
HaapiFlowFragmentResolver.Builder()
.customize(FormModel::class.java) { model ->
FormFragment.newInstance(uiModel, R.layout.fragment_redesign_form)
}
.build()
}
Customizing an existing fragment by subclassing default implementation
An existing fragment can be customized by subclassing its default implemenetation when:
- the customized fragment does require additional code to be displayed
- the fragment default behaviour implementation might change
- the structure of the XML for the customized fragment can be different than the original one: adding new static UI elements or removing existing UI elements.
To start, it is required to know which fragment to target including the naming requirements for the XML.
To illustrate this method, two examples are illustrated below:
- Subclassing
SelectorFragment
to display an advertisement banner at the top of the screen. - Subclassing
FormFragment
to display a captcha when a button is pushed.
Subclassing SelectorFragment
to display an advertisement banner at the top of the screen
Follow the prerequisites to import the Mobile Ads SDK dependency and set up the mandatory configuration. The following AndroidManifest looks like that:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".ClientApplication"
...>
...
<!-- Sample AdMob app ID: ca-app-pub-3940256099942544~3347511713 -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3940256099942544~3347511713"/>
...
</application>
</manifest>
Create a new XML (fragment_subclass_selector.xml
) for the BannerAd
by using the hui_fragment_selector
.
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.gms.ads.AdView
android:id="@+id/adView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:adSize="BANNER"
app:adUnitId="ca-app-pub-3940256099942544/9214589741"
/>
<se.curity.identityserver.haapi.android.ui.widget.components.InfoView
android:id="@+id/hui_fragment_selector_info_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/adView"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/hui_fragment_selector_items_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/hui_fragment_selector_info_view"
app:layout_constraintEnd_toEndOf="parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:overScrollMode="never" />
<Space
android:id="@+id/hui_fragment_selector_bottom_space"
android:layout_width="match_parent"
android:layout_height="@dimen/hui_fragment_form_spacing"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/hui_fragment_selector_items_recycler_view"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/hui_fragment_selector_links_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/hui_fragment_selector_bottom_space"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:overScrollMode="never" />
</androidx.constraintlayout.widget.ConstraintLayout>
Create the implementation file that hooks fragment_subclass_selector.xml
.
class AdsBannerSelectorFragment : SelectorFragment(layoutId = R.layout.fragment_subclass_selector) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Configuring the advertisement banner
MobileAds.initialize(requireContext())
val adView = view.findViewById<AdView>(R.id.adView)
adView.loadAd(AdRequest.Builder().build())
}
companion object {
// It is the recommended way to instantiate a fragment.
fun newInstance(uiModel: UIModel): AdsBannerSelectorFragment {
return AdsBannerSelectorFragment().apply {
arguments = uiModelBundle(uiModel)
}
}
}
}
Override HaapiUIWidgetApplication.fragmentResolver
in your application by using AdsBannerSelectorFragment
.
class ClientApplication : Application(), HaapiUIWidgetApplication {
private val baseUri = URI.create("https://10.0.2.2:8443")
override val widgetConfiguration: WidgetConfiguration =
WidgetConfiguration.Builder(
clientId = "clientId",
baseUri = baseUri,
tokenEndpointUri = baseUri.resolve("/dev/oauth/token"),
authorizationEndpointUri = baseUri.resolve("/dev/oauth/authorize"),
appRedirect = "app://haapi"
)
.setKeyStoreAlias("keystore")
.setHttpUrlConnectionProvider(UNCHECKED_CONNECTION_PROVIDER)
.build()
override val fragmentResolver: FragmentResolver =
HaapiFlowFragmentResolver.Builder()
.customize(SelectorModel::class.java, AdsBannerSelectorFragment::newInstance)
.build()
}
When starting the HAAPI flow and the SelectorModel
is displayed, a banner view is presented at the top of the screen as demonstrated below.

Subclassing FormFragment
to display a reCAPTCHA when a button is pushed.
Generate the reCAPTCHA site key and secret key here by using the mandatory configuration:
- reCAPTCHA v2
- reCAPTCHA Android
Import the following dependencies in your gradle file.
implementation 'com.google.android.gms:play-services-safetynet:18.0.1'
// Feel free to use a different http client.
implementation 'com.android.volley:volley:1.2.1'
Update the AndroidManifest to allow internet connection.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>
Create a subclass of FormFragment that uses reCaptcha.
class ReCaptchaFormFragment : FormFragment() {
private lateinit var queue: RequestQueue
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
queue = Volley.newRequestQueue(requireContext())
}
override fun preSubmit(
interactionAction: InteractionAction,
keysValues: Map<String, Any>,
block: (Boolean, Map<String, Any>) -> Unit
) {
// The reCAPTCHA is triggered when the user pushes the submit button.
// If the reCAPTCHA detects that the user is not a robot then the action of the submit button continues (see handleVerication method).
// Otherwise, in this example, the action of the submit button is not propagated and an exception is thrown.
lifecycleScope.launch {
SafetyNet.getClient(requireContext())
.verifyWithRecaptcha("6LcGAr0pAAAAAMeLGV_SITE_KEY") // The site key when generating the reCAPTCHA - Please keep it secret
.addOnSuccessListener {
if (it.tokenResult != null) {
handleVerification(it.tokenResult!!, block)
} else {
block(false, emptyMap())
throw IllegalStateException("tokenResult was null")
}
}
.addOnFailureListener {
throw it
}
}
}
private fun handleVerification(tokenResult: String, block: (Boolean, Map<String, Any>) -> Unit) {
val siteVerifyUrl = "https://www.google.com/recaptcha/api/siteverify"
val stringRequest = object : StringRequest(
Method.POST,
siteVerifyUrl,
Response.Listener {
try {
val jsonObject = JSONObject(it)
if (jsonObject.getBoolean("success")) {
// The user is not a robot then then the block can be invoked.
block(true, emptyMap())
}
} catch (exception: Exception) {
block(false, emptyMap())
throw exception
}
},
Response.ErrorListener {
block(false, emptyMap())
throw IllegalStateException(it.message)
}
) {
override fun getParams(): MutableMap<String, String>? {
val params = mutableMapOf<String, String>()
params["secret"] = "6LcGAr0pAAAAABrL8gDs5wi_SECRET_KEY" // The secret key when generating the reCAPTCHA - Please keep it secret
params["response"] = tokenResult
return params
}
}
queue.add(stringRequest)
}
companion object {
fun newInstance(uiModel: UIModel) : ReCaptchaFormFragment {
return ReCaptchaFormFragment().apply {
arguments = uiModelBundle(uiModel)
}
}
}
}
Override HaapiUIWidgetApplication.fragmentResolver
in your application by using ReCaptchaFormFragment
.
class ClientApplication : Application(), HaapiUIWidgetApplication {
private val baseUri = URI.create("https://10.0.2.2:8443")
override val widgetConfiguration: WidgetConfiguration =
WidgetConfiguration.Builder(
clientId = "clientId",
baseUri = baseUri,
tokenEndpointUri = baseUri.resolve("/dev/oauth/token"),
authorizationEndpointUri = baseUri.resolve("/dev/oauth/authorize"),
appRedirect = "app://haapi"
)
.setKeyStoreAlias("keystore")
.setHttpUrlConnectionProvider(UNCHECKED_CONNECTION_PROVIDER)
.build()
override val fragmentResolver: FragmentResolver =
HaapiFlowFragmentResolver.Builder()
.customize(FormModel::class.java, ReCaptchaFormFragment::newInstance)
.build()
}
When the button is pressed and before submiting the form, a reCAPTCHAT is displayed. When identifying successfully as a human, then the block of the submitting button is sent.

Providing a custom fragment that subclass HaapiFlowFragment
An existing fragment can be customized by subclassing HaapiFlowFragment when:
- the customized fragment does require additional code to be displayed
- the fragment default behaviour implementation may not be needed
- the structure of the XML for the customized fragment is different than the original one
With this customization method, it is required to fully implement the fragment behaviour according to the UIModel. More details on HaapiFlowFragment
The following snippet demonstrates a class that implements HaapiFlowFragment
for a FormModel
.
class MyHaapiFlowFragment : HaapiFlowFragment<FormModel>(R.layout.custom_my_haapi_flow_fragment) {
// Implement the whole behaviour
// ...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val formModel = extractUIModelFromArguments<FormModel>()
// Handle the formModel
}
companion object {
fun newInstance(formModel: FormModel): MyHaapiFlowFragment {
return MyHaapiFlowFragment().apply {
arguments = uiModelBundle(
model = formModel,
param = null
)
}
}
}
}
Override HaapiUIWidgetApplication.fragmentResolver
in your application by using MyHaapiFlowFragment
.
class ClientApplication : Application(), HaapiUIWidgetApplication {
private val baseUri = URI.create("https://10.0.2.2:8443")
override val widgetConfiguration: WidgetConfiguration =
WidgetConfiguration.Builder(
clientId = "clientId",
baseUri = baseUri,
tokenEndpointUri = baseUri.resolve("/dev/oauth/token"),
authorizationEndpointUri = baseUri.resolve("/dev/oauth/authorize"),
appRedirect = "app://haapi"
)
.setKeyStoreAlias("keystore")
.setHttpUrlConnectionProvider(UNCHECKED_CONNECTION_PROVIDER)
.build()
override val fragmentResolver: FragmentResolver =
HaapiFlowFragmentResolver.Builder()
.customize(FormModel::class.java, MyHaapiFlowFragment::newInstance)
.build()
}
Providing custom objects and overriding the mappings of objects that are passed to the UI elements for HAAPI
The previous section focuses on the custom fragment via using different XML, subclassing a fragment or custom fragment of HaapiFlowFragment. This section highlights the possibility of providing custom/modified objects to the fragments. Reminder: HAAPI representations are mapped into UIModel
and the latest generates fragments via a FragmentResolver
The requirements are:
-
Import the following configuration in the build.gradle of the application
plugins { ... id 'kotlin-parcelize' }
-
Create a class that conforms to the UIModel
-
Implement an object that conforms to
DataMappersFactory
orimplements
HaapiDataMappersFactory` -
Override HaapiUIWidgetApplication.dataMappersFactory when implementing
HaapiUIWidgetApplication
in the application
The custom object must conform to a UIModel. The UIModel can be divided into 3 categories or types:
Category/Type | Description |
---|---|
Interaction | An interaction model that requires user interaction. |
Operation | An operation model requires to launch a service inside or outside the application. |
Problem | A problem informs something unexpected happened. |
The following interfaces conform to UIModel/sub-model and are used by a fragment that is a subclass of HaapiFlowFragment.
UIModel | Type | Fragment that is a subclass of HaapiFlowFragment |
---|---|---|
ContinueSameModel | Interaction | FormFragment |
FormModel | Interaction | FormFragment |
GenericModel | Interaction | GenericFragment |
PollingModel | Operation | PollingFragment |
ProblemModel | Problem | ProblemFragment |
SelectorModel | Interaction | SelectorFragment |
WebAuthnOperationModel | Operation | WebAuthnFragment |
The following snippet illustrates a class which adds a new field for FormModel
:
@Parcelize
data class MyFormModel(
val userInfo: String, // New field
override val interactionItems: List<InteractionItem>,
override val linkItems: List<LinkItemModel>,
override val messageItems: List<InfoMessageModel>,
override val templateArea: String?,
override val viewName: String?
) : FormModel
The following snippet illustrates creating a subclass of HaapiDataMappersFactory
by using the new class.
class CustomHaapiDataMappersFactory(
redirectTo: String,
autoPollingDuration: Duration,
useDefaultExternalBrowser: Boolean
) : HaapiDataMappersFactory(redirectTo, autoPollingDuration, useDefaultExternalBrowser) {
override val mapHaapiRepresentationToInteraction: (HaapiRepresentation) -> UIModel.Interaction
get() = {
val defaultMappedObject = super.mapHaapiRepresentationToInteraction(it)
when (it) {
is InteractiveFormStep -> {
(defaultMappedObject as? FormModel)?.let { formModel ->
MyFormModel(
userInfo = "NEW INFO",
interactionItems = formModel.interactionItems,
linkItems = formModel.linkItems,
messageItems = formModel.messageItems,
templateArea = formModel.templateArea,
formModel.viewName
)
} ?: defaultMappedObject
}
else -> defaultMappedObject
}
}
}
// Uses the CustomHaapiDataMappersFactory in your application that implements HaapiUIWidgetApplication
class ClientApplication : Application(), HaapiUIWidgetApplication {
private val baseUri = URI.create("https://10.0.2.2:8443")
override val dataMappersFactory: DataMappersFactory
get() = CustomHaapiDataMappersFactory(
redirectTo = widgetConfiguration.haapiConfiguration.appRedirect,
autoPollingDuration = widgetConfiguration.autoPollingDuration,
useDefaultExternalBrowser = widgetConfiguration.useDefaultExternalBrowser
)
...
}
A fragment that uses the new MyFormModel
can access the client added field as demonstrated below, and an existing fragment that uses the bundled FormModel
can access the interface properties and maintain functionality
class UsingMyFormModelFragment(layoutId: Int) : FormFragment(layoutId) {
override fun preRender(uiModel: UIModel?, view: View, savedInstanceState: Bundle?) {
super.preRender(uiModel, view, savedInstanceState)
if (uiModel is MyFormModel) {
// To be implemented on the UI layout
Log.d("TO BE IMPLEMENTED", uiModel.userInfo)
}
}
companion object {
fun newInstance(uiModel: UIModel, layoutId: Int): UsingMyFormModelFragment {
return UsingMyFormModelFragment(layoutId).apply {
arguments = uiModelBundle(uiModel)
}
}
}
}
// Update the class that implements HaapiUIWidgetApplication
class ClientApplication : Application(), HaapiUIWidgetApplication {
...
override val fragmentResolver: FragmentResolver =
HaapiFlowFragmentResolver.Builder()
.customize(FormModel::class.java { model ->
UsingMyFormModelFragment.newInstance(uiModel, R.layout.fragment_redesign_form)
}
.build()
...
}