Looking for the basic in-app message developer integration guide? Find it here.
Advanced implementation guide
This optional and advanced implementation guide covers in-app message code considerations, three custom use cases built by our team, and accompanying code snippets. Visit our Braze Demo repository here! Note that this implementation guide is centered around a Kotlin implementation, but Java snippets are provided for those interested. Looking for HTML implementations? Take a look at our HTML template repository!
Code considerations
The following guide offers an optional custom developer integration to use in addition to default in-app messages. Custom view components and factories are included as needed with each use case, offering examples to extend functionality and natively customize the look and feel of your in-app messages. There are, in some instances, multiple ways to achieve similar results. The optimal implementation will depend on the specific use case.
Custom factories
The Braze SDK allows developers to override a number of defaults through custom factory objects. These can be registered with the Braze SDK as needed to achieve the desired results. In most cases, however, if you decide to override a factory, you will need to either explicitly defer to the default or reimplement the functionality provided by the Braze default. The following code snippet illustrates how to supply custom implementations of the IInAppMessageViewFactory
and the IInAppMessageViewWrapperFactory
interfaces. After you have a solid understanding of the concepts behind overriding our default factories, check out our use cases to get started implementing custom in-app messaging functionality.
In-app message types
1
2
3
4
5
6
7
8
class BrazeDemoApplication : Application(){
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(BrazeActivityLifecycleCallbackListener(true, true))
BrazeInAppMessageManager.getInstance().setCustomInAppMessageViewWrapperFactory(CustomInAppMessageViewWrapperFactory())
BrazeInAppMessageManager.getInstance().setCustomInAppMessageViewFactory(CustomInAppMessageViewFactory())
}
}
In-app message types
1
2
3
4
5
6
7
8
9
public class BrazeDemoApplication extends Application {
@Override
public void onCreate{
super.onCreate();
registerActivityLifecycleCallbacks(new BrazeActivityLifecycleCallbackListener(true, true));
BrazeInAppMessageManager.getInstance().setCustomInAppMessageViewWrapperFactory(new CustomInAppMessageViewWrapperFactory());
BrazeInAppMessageManager.getInstance().setCustomInAppMessageViewFactory(new CustomInAppMessageViewFactory());
}
}
Use cases
We’ve provided three use cases below. Each use case has code snippets and a look into how in-app messages may look and be used in the Braze dashboard:
Custom slide-up in-app message
While building out your slide-up in-app message, you may notice you aren’t able to modify the placement of the message using default methods. Modification like this is made possible by subclassing the DefaultInAppMessageViewWrapper
class to adjust the layout parameters. You can adjust the final position on the screen by overriding the getLayoutParams
method returning the modified LayoutParams
with your own custom positioning values. Visit the CustomSlideUpInAppMessageViewWrapper to get started.
Custom view wrapper
Override and return custom layout parameters
Within the getLayoutParams
method, you can use the superclass method to access the original LayoutParameters
for the in-app message. Then, you can adjust the position by adding or subtracting as desired.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class CustomSlideUpInAppMessageViewWrapper(inAppMessageView: View?,
inAppMessage: IInAppMessage?,
inAppMessageViewLifecycleListener: IInAppMessageViewLifecycleListener?,
configurationProvider: BrazeConfigurationProvider?,
openingAnimation: Animation?,
closingAnimation: Animation?,
clickableInAppMessageView: View?) : DefaultInAppMessageViewWrapper(inAppMessageView,
inAppMessage,
inAppMessageViewLifecycleListener,
configurationProvider,
openingAnimation,
closingAnimation,
clickableInAppMessageView) {
override fun getLayoutParams(inAppMessage: IInAppMessage?): ViewGroup.LayoutParams {
val params = super.getLayoutParams(inAppMessage) as FrameLayout.LayoutParams
params.bottomMargin = params.bottomMargin + 500 //move the view up by 500 pixels
return params
}
}
Override and return custom layout parameters
Within the getLayoutParams
method, you can use the superclass method to access the original LayoutParameters
for the in-app message. Then, you can adjust the position by adding or subtracting as desired.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class CustomSlideUpInAppMessageViewWrapper extends DefaultInAppMessageViewWrapper {
public CustomInAppMessageViewWrapper(View inAppMessageView,
IInAppMessage inAppMessage,
IInAppMessageViewLifecycleListener inAppMessageViewLifecycleListener,
BrazeConfigurationProvider configurationProvider,
Animation openingAnimation,
Animation closingAnimation,
View clickableInAppMessageView){
super(inAppMessageView,
inAppMessage,
inAppMessageViewLifecycleListener,
configurationProvider,
openingAnimation,
closingAnimation,
clickableInAppMessageView)
}
@Override
public ViewGroup.LayoutParams getLayoutParams(IInAppMessage inAppMessage){
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams)super.getLayoutParams(inAppMessage)
params.bottomMargin = params.bottomMargin + 500 //move the view up by 500 pixels
return params
}
}
Supply a custom factory to return your custom wrapper
In order for the Braze SDK to use your custom wrapper, you also need to supply a custom IInAppMessageViewWrapperFactory
implementation that returns your custom wrapper. You can either implement the IInAppMessageViewWrapperFactory
directly, or subclass BrazeInAppMessageViewWrapperFactory
and only override the createInAppMessageViewWrapper
method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class CustomInAppMessageViewWrapperFactory : BrazeInAppMessageViewWrapperFactory() {
override fun createInAppMessageViewWrapper(
inAppMessageView: View?,
inAppMessage: IInAppMessage?,
inAppMessageViewLifecycleListener: IInAppMessageViewLifecycleListener?,
configurationProvider: BrazeConfigurationProvider?,
openingAnimation: Animation?,
closingAnimation: Animation?,
clickableInAppMessageView: View?
): IInAppMessageViewWrapper {
return if (inAppMessage is InAppMessageSlideup) {
CustomSlideUpInAppMessageViewWrapper( //return our custom view wrapper only for slideups
inAppMessageView,
inAppMessage,
inAppMessageViewLifecycleListener,
configurationProvider,
openingAnimation,
closingAnimation,
clickableInAppMessageView
)
} else {
super.createInAppMessageViewWrapper( //defer to the default implementation for all other IAM types
inAppMessageView,
inAppMessage,
inAppMessageViewLifecycleListener,
configurationProvider,
openingAnimation,
closingAnimation,
clickableInAppMessageView
)
}
}
}
Supply a custom factory to return your custom wrapper
In order for the Braze SDK to use your custom wrapper, you need to supply a custom IInAppMessageViewWrapperFactory
implementation that returns your custom wrapper. You can either implement the IInAppMessageViewWrapperFactory
directly, or subclass BrazeInAppMessageViewWrapperFactory
and only override the createInAppMessageViewWrapper
method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class CustomInAppMessageViewWrapperFactory extends BrazeInAppMessageViewWrapperFactory {
@Override
public IInAppMessageViewWrapper createInAppMessageViewWrapper(View inAppMessageView,
IInAppMessage inAppMessage,
IInAppMessageViewLifecycleListener inAppMessageViewLifecycleListener,
BrazeConfigurationProvider configurationProvider,
Animation openingAnimation,
Animation closingAnimation,
View clickableInAppMessageView){
if (inAppMessage instanceof InAppMessageSlideup){
return new CustomSlideUpInAppMessageViewWrapper( //return our custom view wrapper only for slideups
inAppMessageView,
inAppMessage,
inAppMessageViewLifecycleListener,
configurationProvider,
openingAnimation,
closingAnimation,
clickableInAppMessageView);
}else{
return super.createInAppMessageViewWrapper(//defer to the default implementation for all other IAM types
inAppMessageView,
inAppMessage,
inAppMessageViewLifecycleListener,
configurationProvider,
openingAnimation,
closingAnimation,
clickableInAppMessageView);
}
}
}
Register your factory with Braze
Once you’ve created your custom wrapper factory, register it with the Braze SDK via the BrazeInAppMessageManager
:
1
BrazeInAppMessageManager.getInstance().setCustomInAppMessageViewWrapperFactory(CustomInAppMessageViewWrapperFactory())
Register your factory with Braze
Once you’ve created your custom wrapper factory, register it with the Braze SDK via the BrazeInAppMessageManager
:
1
BrazeInAppMessageManager.getInstance().setCustomInAppMessageViewWrapperFactory(new CustomInAppMessageViewWrapperFactory());
Custom modal in-app message
A BrazeInAppMessageModalView
can be subclassed to leverage a Spinner
offering engaging ways to collect valuable user attributes. The following example shows how you can use Connected Content to capture custom attributes from a dynamic list of items. Visit the TeamPickerView
to get started.
Using view_type
for UI display behavior
The IInAppMessage
object has an extras
dictionary that we can query to find the view_type
key (if any) and display the correct type of view. It’s important to note that in-app messages are configured on a per-message basis, so custom and default modal views can work harmoniously.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
override fun createInAppMessageView(activity: Activity, inAppMessage: IInAppMessage): View {
return when {
inAppMessage.extras?.get("view_type") == "picker" -> {
getCustomPickerView(activity, inAppMessage)
}
//...
else -> {
//Defer to default
BrazeInAppMessageManager
.getInstance()
.getDefaultInAppMessageViewFactory(inAppMessage).createInAppMessageView(activity, inAppMessage)
}
}
}
Using view_type
for UI display behavior
The IInAppMessage
object has an extras
dictionary that we can query to find the view_type
key (if any) and display the correct type of view. It’s important to note that in-app messages are configured on a per-message basis, so custom and default modal views can work harmoniously.
1
2
3
4
5
6
7
8
9
10
11
12
@Override
public View createInAppMessageView(Activity activity, IInAppMessage inAppMessage) {
if("picker".equals(inAppMessage.getExtras().get("view_type"))){
return getCustomPickerView(activity, inAppMessage);
} else {
//Defer to default
BrazeInAppMessageManager
.getInstance()
.getDefaultInAppMessageViewFactory(inAppMessage)
.createInAppMessageView(activity, inAppMessage);
}
}
Override and provide custom view
Provide a layout that mimics the standard modal in-app message, but supply your view as the root element, and then inflate that layout
1
2
3
4
5
6
7
8
9
10
11
<com.braze.advancedsamples.inapp.modal.TeamPickerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="0.0dp"
android:id="@+id/team_picker_view">
<!-- ... -->
<Spinner android:layout_width="match_parent" android:layout_height="wrap_content"
android:id="@+id/team_spinner"/>
<!-- ... -->
</com.braze.advancedsamples.inapp.modal.TeamPickerView>
Inflate and customize the view
Before reloading the Spinner
components, the inAppMessage
message variable is output as a string. This message must be formatted as an array of items to be displayed correctly. As an example, this can be achieved using String.split(",")
.
1
2
3
4
5
6
private fun getCustomView(activity: Activity, inAppMessage: IInAppMessage): TeamPickerView {
val view = activity.layoutInflater.inflate(R.layout.team_picker_dialog, null) as TeamPickerView
val teams = inAppMessage.message.split(",")
view.setTeams(teams)
return view
}
Inflate and customize the view
Before reloading the Spinner
components, the inAppMessage
message variable is output as a String. This message must be formatted as an array of items to be displayed correctly. As an example, this can be achieved using String.split(",")
.
1
2
3
4
5
6
private TeamPickerView getCustomView(Activity activity, IInAppMessage inAppMessage) {
TeamPickerView view = (TeamPickerView) activity.getLayoutInflater().inflate(R.layout.team_picker_dialog, null);
String[] teams = inAppMessage.getMessage().split(",");
view.setTeams(teams);
return view
}
Assign custom attribute
Using the view subclass, after a user presses submit, pass the attribute with its corresponding selected value to Braze and dismiss the in-app message by calling messageClickableView.performClick()
.
1
2
3
4
5
6
7
override fun onClick(v: View?) {
val selectedTeam = spinner.selectedItem as String
messageClickableView.performClick()
Braze.getInstance(ctx).getCurrentUser { brazeUser ->
brazeUser?.setCustomUserAttribute("FavoriteTeam", selectedTeam)
}
}
Assign custom attribute
Using the view subclass, after a user presses submit, pass the attribute with its corresponding selected value to Braze and dismiss the in-app message by calling messageClickableView.performClick()
.
1
2
3
4
5
6
7
8
@Override
public void onClick(View v) {
String selectedTeam = (String) spinner.getSelectedItem();
messageClickableView.performClick();
Braze.getInstance(ctx).getCurrentUser(brazeUser -> {
brazeUser.setCustomUserAttribute("FavoriteTeam", selectedTeam);
});
}
Custom full in-app message
Implementing a fully custom immersive (full screen) in-app message involves a similar approach outlined in the section for implementing a customized modal in-app message. In this instance, however, simply extend BrazeInAppMessageFullView
and customize as needed. Remember that the view will be displayed over the application UI, and views in Android by default are transparent. This means you will need to define a background such that the in-app message obscures the content behind it. By extending BrazeInAppMessageFullView
, the Braze SDK will handle intercepting touch events on the view and take the appropriate action. Like with the modal example, you can override this behavior for certain controls (like Switch
controls) to collect feedback from the user.
Using view_type
for UI display behavior
We will add another view_type
extra for our new immersive customization. Revisiting the createInAppMessageView
method, add a option for the “switches” UI:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
override fun createInAppMessageView(activity: Activity, inAppMessage: IInAppMessage): View {
return when {
inAppMessage.extras?.get("view_type") == "picker" -> {
getCustomPickerView(activity, inAppMessage)
}
inAppMessage.extras?.get("view_type") == "switches" -> {
getCustomImmersiveView(activity, inAppMessage) // new customization
}
else -> {
//Defer to default
BrazeInAppMessageManager
.getInstance()
.getDefaultInAppMessageViewFactory(inAppMessage).createInAppMessageView(activity, inAppMessage)
}
}
}
Using view_type
for UI display behavior
We will add another view_type
extra for our new immersive customization. Revisiting the createInAppMessageView
method, add a option for the “switches” UI:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public View createInAppMessageView(Activity activity, IInAppMessage inAppMessage) {
if("picker".equals(inAppMessage.getExtras().get("view_type"))){
return getCustomPickerView(activity, inAppMessage);
} else if ("switches".equals(inAppMessage.getExtras().get("view_type"))) {
return getCustomImmersiveView(activity, inAppMessage); // new customization
} else {
//Defer to default
BrazeInAppMessageManager
.getInstance()
.getDefaultInAppMessageViewFactory(inAppMessage)
.createInAppMessageView(activity, inAppMessage);
}
}
Override and provide custom view
Provide a layout that mimics the standard modal in-app message, but supply your view as the root element, and then inflate that layout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<com.braze.advancedsamples.immersive.CustomImmersiveInAppMessage
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="wrap_content">
<!-- giving the parent layout a white backround color will obscure the app behind the IAM. You could also do this within your custom view -->
<LinearLayout android:background="@color/white" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center">
<!-- ... -->
<androidx.recyclerview.widget.RecyclerView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/option_list"/>
<!-- ... -->
</LinearLayout>
</com.braze.advancedsamples.immersive.CustomImmersiveInAppMessage>
Inflate and customize the view
Before setting the options for the RecyclerView
component, the inAppMessage
message variable is output as a String. This message must be formatted as an array of items to be displayed correctly. As an example, this can be achieved using String.split(",")
. The title
and subtitle
are also extracted from the extras
bundle.
1
2
3
4
5
6
7
8
private fun getCustomImmersiveView(activity: Activity, inAppMessage: IInAppMessage): CustomImmersiveInAppMessage{
val view = activity.layoutInflater.inflate(R.layout.full_screen_iam, null) as CustomImmersiveInAppMessage
val options = inAppMessage.message.split(",")
view.setOptions(options)
inAppMessage.extras?.get("title").let { view.setTitle(it) }
inAppMessage.extras?.get("subtitle").let {view.setSubtitle(it) }
return view
}
Inflate and customize the view
Before setting the options for the RecyclerView
component, the inAppMessage
message variable is output as a String. This message must be formatted as an array of items to be displayed correctly. As an example, this can be achieved using String.split(",")
. The title
and subtitle
are also extracted from the extras
bundle.
1
2
3
4
5
6
7
8
9
10
private CustomImmersiveInAppMessage getCustomImmersiveView(Activity activity, IInAppMessage inAppMessage) {
CustomImmersiveInAppMessage view = (CustomImmersiveInAppMessage) activity.layoutInflater.inflate(R.layout.full_screen_iam, null);
String[] options = inAppMessage.message.split(",");
view.setOptions(options);
String title = inAppMessage.getExtras().get("title");
view.setTitle(title);
String subtitle = inAppMessage.getExtras().get("subtitle");
view.setSubtitle(subtitle);
return view;
}
Assign custom attribute
Using the view subclass, after a user toggles one of the switches, pass the associated attribute and the toggle status to Braze.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fun logClick(value:String, checked:Boolean){
Braze.getInstance(ctx).logCustomEvent("SwitchChanged", BrazeProperties())
}
inner class OptionViewHolder(item: View): RecyclerView.ViewHolder(item), View.OnClickListener{
var value: String = ""
override fun onClick(p0: View?) {
if (p0 is Switch){
val checked = p0.isChecked
p0.isChecked = !p0.isChecked
logClick(value, checked)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OptionViewHolder {
return OptionViewHolder(mInflater.inflate(R.layout.switch_cell, null))
}
override fun onBindViewHolder(holder: OptionViewHolder, position: Int) {
holder.itemView.findViewById<TextView>(R.id.label).text = options[position]
holder.value = options[position]
}
Assign custom attribute
Using the view subclass, after a user toggles one of the switches, pass the associated attribute and the toggle status to Braze.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void logClick(String value, boolean checked){
Braze.getInstance(ctx).logCustomEvent("SwitchChanged", new BrazeProperties());
}
private class OptionViewHolder extends RecyclerView.ViewHolder, implements View.OnClickListener{
private String value = "";
public OptionViewHolder(View item){
super(item);
}
@Override
public void onClick(View view) {
if (view instanceof Switch){
Switch switchView = (Switch) view;
boolean checked = switchView.isChecked;
switchView.isChecked = !switchView.isChecked;
logClick(value, checked)
}
}
}
@Override
public OptionViewHolder onCreateViewHolder(ViewGroup parent, Int viewType) {
return new OptionViewHolder(mInflater.inflate(R.layout.switch_cell, null));
}
@Override
public void onBindViewHolder(OptionViewHolder holder, Int position) {
((TextView)holder.getItemView().findViewById(R.id.label)).setText(options.get(position));
holder.value = options.get(position);
}
Intercepting in-app message touches
Intercepting in-app message touches is crucial in making the custom full in-app message buttons function correctly. By default, all in-app message views add onClick
listeners onto the message, so users can dismiss messages without buttons. When you add custom controls that should respond to user input (like custom buttons), you can register an onClick
listener with the view as normal. Any touches outside of the custom controls will dismiss the in-app message as usual, while touches received by the custom controls will invoke your onClick
listener.