Guia de implementação avançada (opcional)
Este guia de implementação opcional e avançado aborda considerações sobre o código do Content Card, três casos de uso personalizados criados por nossa equipe, acompanhando trechos de código e orientações sobre o registro de impressões, cliques e descartes de cartão. Visite nosso Repositório de Demonstrações Braze aqui! Note que este guia de implementação está centrado em uma implementação Kotlin, mas são fornecidos trechos em Java para os interessados.
Está procurando o guia básico de integração do desenvolvedor do cartão de conteúdo? Encontre-o aqui.
Mais informações sobre a personalização dos cartões de conteúdo podem ser encontradas no Guia de personalização.
Considerações sobre o código
Importar instruções e arquivos auxiliares
Ao criar cartões de conteúdo, você deve expor o SDK da Braze por meio de um único singleton do gerenciador. Esse padrão protege o código do seu aplicativo dos detalhes de implementação do Braze por meio de uma abstração compartilhada que faz sentido para o seu caso de uso. Isso também facilita o rastreamento, a depuração e a alteração do código. Um exemplo de implementação de gerenciador pode ser encontrado aqui.
Cartões de conteúdo como objetos personalizados
Seus próprios objetos personalizados já em uso em seu aplicativo podem ser estendidos para transportar dados do cartão de conteúdo, abstraindo assim a fonte dos dados em um formato já compreendido pelo código do aplicativo. As abstrações de fontes de dados oferecem flexibilidade para trabalhar com diferentes backends de dados de forma intercambiável e em conjunto. Neste exemplo, definimos a classe base abstrata ContentCardable
para representar nossos dados existentes (alimentados, neste exemplo, por um arquivo JSON local) e os novos dados alimentados pelo SDK da Braze. A classe base também expõe os dados brutos do cartão de conteúdo para os consumidores que precisam acessar a implementação original do Card
.
Ao inicializar instâncias ContentCardable
do SDK da Braze, usamos o class_type
extra para mapear o cartão de conteúdo para uma subclasse concreta. Em seguida, usamos os outros pares de chave/valor definidos no dashboard da Braze para preencher os campos necessários.
Depois de entender bem essas considerações de código, confira nossos casos de uso para começar a implementar seus próprios objetos personalizados.
Não há dependências de Card
ContentCardData
representa os valores comuns analisados de um Card
.
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
abstract class ContentCardable (){
var cardData: ContentCardData? = null
constructor(data:Map<String, Any>):this(){
cardData = ContentCardData(data[idString] as String,
ContentCardClass.valueFrom(data[classType] as String),
data[created] as Long,
data[dismissable] as Boolean)
}
val isContentCard: Boolean
get() = cardData != null
fun logContentCardClicked() {
BrazeManager.getInstance().logContentCardClicked(cardData?.contentCardId)
}
fun logContentCardDismissed() {
BrazeManager.getInstance().logContentCardDismissed(cardData?.contentCardId)
}
fun logContentCardImpression() {
BrazeManager.getInstance().logContentCardImpression(cardData?.contentCardId)
}
}
data class ContentCardData (var contentCardId: String,
var contentCardClassType: ContentCardClass,
var createdAt: Long,
var dismissable: Boolean)
Não há dependências de Card
ContentCardData
representa os valores comuns analisados de um Card
.
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
35
36
37
38
39
40
41
42
43
44
45
public abstract class ContentCardable{
private ContentCardData cardData = null;
public ContentCardable(Map<String, Object> data){
cardData = new ContentCardData()
cardData.contentCardId = (String) data.get(idString);
cardData.contentCardClassType = contentCardClassType.valueOf((String)data.get(classType));
cardData.createdAt = Long.parseLong((String)data.get(createdAt));
cardData.dismissable = Boolean.parseBoolean((String)data.get(dismissable));
}
public ContentCardable(){
}
public boolean isContentCard(){
return cardData != null;
}
public void logContentCardClicked() {
if (isContentCard()){
BrazeManager.getInstance().logContentCardClicked(cardData.contentCardId)
}
}
public void logContentCardDismissed() {
if(isContentCard()){
BrazeManager.getInstance().logContentCardDismissed(cardData.contentCardId)
}
}
public void logContentCardImpression() {
if(isContentCard()){
BrazeManager.getInstance().logContentCardImpression(cardData.contentCardId)
}
}
}
public class ContentCardData{
public String contentCardId;
public ContentCardClass contentCardClassType;
public long createdAt;
public boolean dismissable;
}
Inicializador de objeto personalizado
Os metadados de um Card
são usados para preencher as variáveis de sua subclasse concreta. Dependendo da subclasse, talvez você precise extrair valores diferentes durante a inicialização. Os pares de valores-chave configurados no dashboard do Braze são representados no dicionário “extras”.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Tile: ContentCardable {
constructor(metadata:Map<String, Any>):super(metadata){
val extras = metadata[extras] as? Map<String, Any>
title = extras?.get(Keys.title) as? String
image = extras?.get(Keys.image) as? String
detail = metadata[ContentCardable.detail] as? String
tags = (metadata[ContentCardable.tags] as? String)?.split(",")
val priceString = extras?.get(Keys.price) as? String
if (priceString?.isNotEmpty() == true){
price = priceString.toDouble()
}
id = floor(Math.random()*1000).toInt()
}
}
Inicializador de objeto personalizado
Os metadados de um Card
são usados para preencher as variáveis de sua subclasse concreta. Dependendo da subclasse, talvez você precise extrair valores diferentes durante a inicialização. Os pares de valores-chave configurados no dashboard do Braze são representados no dicionário “extras”.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Tile extends ContentCardable {
public Tile(Map<String, Object> metadata){
super(metadata);
this.detail = (String) metadata.get(ContentCardable.detail);
this.tags = ((String)metadata.get(ContentCardable.tags)).split(",");
if (metadata.containsKey(Keys.extras)){
Map<String, Object> extras = metadata.get(Keys.extras);
this.title = (String)extras.get(Keys.title);
this.price = Double.parseDouble((String)extras.get(Keys.price));
this.image = (String)extras.get(Keys.image);
}
}
}
Identificação de tipos
O enum ContentCardClass
representa o valor class_type
no dashboard da Braze e fornece um método para inicializar o enum a partir das strings fornecidas pelo SDK.
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
enum class ContentCardClass{
AD,
COUPON,
NONE,
ITEM_TILE,
ITEM_GROUP,
MESSAGE_FULL_PAGE,
MESSAGE_WEB_VIEW;
companion object {
// This value must be synced with the `class_type` value that has been set up in your
// Braze dashboard or its type will be set to `ContentCardClassType.none.`
fun valueFrom(str: String?): ContentCardClass {
return when(str?.toLowerCase()){
"coupon_code" -> COUPON
"home_tile" -> ITEM_TILE
"group" -> ITEM_GROUP
"message_full_page" -> MESSAGE_FULL_PAGE
"message_webview" -> MESSAGE_WEB_VIEW
"ad_banner" -> AD
else -> NONE
}
}
}
}
Identificação de tipos
O enum ContentCardClass
representa o valor class_type
no dashboard da Braze e fornece um método para inicializar o enum a partir das strings fornecidas pelo SDK.
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
35
enum ContentCardClass {
AD,
COUPON,
NONE,
ITEM_TILE,
ITEM_GROUP,
MESSAGE_FULL_PAGE,
MESSAGE_WEB_VIEW
public static valueFrom(String val){
switch(val.toLowerCase()){
case "coupon_code":{
return COUPON;
}
case "home_tile":{
return ITEM_TILE;
}
case "group":{
return ITEM_GROUP;
}
case "message_full_page":{
return MESSAGE_FULL_PAGE;
}
case "message_webview":{
return MESSAGE_WEB_VIEW;
}
case "ad_banner":{
return AD;
}
default:{
return NONE;
}
}
}
}
Renderização de cartões personalizados
A seguir, listamos informações sobre como alterar a forma como qualquer cartão é renderizado no site recyclerView
. A interface IContentCardsViewBindingHandler
define como todos os cartões de conteúdo são renderizados. Você pode personalizar isso para alterar o que quiser:
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class DefaultContentCardsViewBindingHandler implements IContentCardsViewBindingHandler {
// Interface that must be implemented and provided as a public CREATOR
// field that generates instances of your Parcelable class from a Parcel.
public static final Parcelable.Creator<DefaultContentCardsViewBindingHandler> CREATOR = new Parcelable.Creator<DefaultContentCardsViewBindingHandler>() {
public DefaultContentCardsViewBindingHandler createFromParcel(Parcel in) {
return new DefaultContentCardsViewBindingHandler();
}
public DefaultContentCardsViewBindingHandler[] newArray(int size) {
return new DefaultContentCardsViewBindingHandler[size];
}
};
/**
* A cache for the views used in binding the items in the {@link android.support.v7.widget.RecyclerView}.
*/
private final Map<CardType, BaseContentCardView> mContentCardViewCache = new HashMap<CardType, BaseContentCardView>();
@Override
public ContentCardViewHolder onCreateViewHolder(Context context, List<? extends Card> cards, ViewGroup viewGroup, int viewType) {
CardType cardType = CardType.fromValue(viewType);
return getContentCardsViewFromCache(context, cardType).createViewHolder(viewGroup);
}
@Override
public void onBindViewHolder(Context context, List<? extends Card> cards, ContentCardViewHolder viewHolder, int adapterPosition) {
Card cardAtPosition = cards.get(adapterPosition);
BaseContentCardView contentCardView = getContentCardsViewFromCache(context, cardAtPosition.getCardType());
contentCardView.bindViewHolder(viewHolder, cardAtPosition);
}
@Override
public int getItemViewType(Context context, List<? extends Card> cards, int adapterPosition) {
Card card = cards.get(adapterPosition);
return card.getCardType().getValue();
}
/**
* Gets a cached instance of a {@link BaseContentCardView} for view creation/binding for a given {@link CardType}.
* If the {@link CardType} is not found in the cache, then a view binding implementation for that {@link CardType}
* is created and added to the cache.
*/
@VisibleForTesting
BaseContentCardView getContentCardsViewFromCache(Context context, CardType cardType) {
if (!mContentCardViewCache.containsKey(cardType)) {
// Create the view here
BaseContentCardView contentCardView;
switch (cardType) {
case BANNER:
contentCardView = new BannerImageContentCardView(context);
break;
case CAPTIONED_IMAGE:
contentCardView = new CaptionedImageContentCardView(context);
break;
case SHORT_NEWS:
contentCardView = new ShortNewsContentCardView(context);
break;
case TEXT_ANNOUNCEMENT:
contentCardView = new TextAnnouncementContentCardView(context);
break;
default:
contentCardView = new DefaultContentCardView(context);
break;
}
mContentCardViewCache.put(cardType, contentCardView);
}
return mContentCardViewCache.get(cardType);
}
// Parcelable interface method
@Override
public int describeContents() {
return 0;
}
// Parcelable interface method
@Override
public void writeToParcel(Parcel dest, int flags) {
// Retaining views across a transition could lead to a
// resource leak so the parcel is left unmodified
}
}
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class DefaultContentCardsViewBindingHandler : IContentCardsViewBindingHandler {
// Interface that must be implemented and provided as a public CREATOR
// field that generates instances of your Parcelable class from a Parcel.
val CREATOR: Parcelable.Creator<DefaultContentCardsViewBindingHandler?> = object : Parcelable.Creator<DefaultContentCardsViewBindingHandler?> {
override fun createFromParcel(`in`: Parcel): DefaultContentCardsViewBindingHandler? {
return DefaultContentCardsViewBindingHandler()
}
override fun newArray(size: Int): Array<DefaultContentCardsViewBindingHandler?> {
return arrayOfNulls(size)
}
}
/**
* A cache for the views used in binding the items in the [RecyclerView].
*/
private val mContentCardViewCache: MutableMap<CardType, BaseContentCardView<*>?> = HashMap()
override fun onCreateViewHolder(context: Context?, cards: List<Card?>?, viewGroup: ViewGroup?, viewType: Int): ContentCardViewHolder? {
val cardType = CardType.fromValue(viewType)
return getContentCardsViewFromCache(context, cardType)!!.createViewHolder(viewGroup)
}
override fun onBindViewHolder(context: Context?, cards: List<Card>, viewHolder: ContentCardViewHolder?, adapterPosition: Int) {
if (adapterPosition < 0 || adapterPosition >= cards.size) {
return
}
val cardAtPosition = cards[adapterPosition]
val contentCardView = getContentCardsViewFromCache(context, cardAtPosition.cardType)
if (viewHolder != null) {
contentCardView!!.bindViewHolder(viewHolder, cardAtPosition)
}
}
override fun getItemViewType(context: Context?, cards: List<Card>, adapterPosition: Int): Int {
if (adapterPosition < 0 || adapterPosition >= cards.size) {
return -1
}
val card = cards[adapterPosition]
return card.cardType.value
}
/**
* Gets a cached instance of a [BaseContentCardView] for view creation/binding for a given [CardType].
* If the [CardType] is not found in the cache, then a view binding implementation for that [CardType]
* is created and added to the cache.
*/
@VisibleForTesting
fun getContentCardsViewFromCache(context: Context?, cardType: CardType): BaseContentCardView<Card>? {
if (!mContentCardViewCache.containsKey(cardType)) {
// Create the view here
val contentCardView: BaseContentCardView<*> = when (cardType) {
CardType.BANNER -> BannerImageContentCardView(context)
CardType.CAPTIONED_IMAGE -> CaptionedImageContentCardView(context)
CardType.SHORT_NEWS -> ShortNewsContentCardView(context)
CardType.TEXT_ANNOUNCEMENT -> TextAnnouncementContentCardView(context)
else -> DefaultContentCardView(context)
}
mContentCardViewCache[cardType] = contentCardView
}
return mContentCardViewCache[cardType] as BaseContentCardView<Card>?
}
// Parcelable interface method
override fun describeContents(): Int {
return 0
}
// Parcelable interface method
override fun writeToParcel(dest: Parcel?, flags: Int) {
// Retaining views across a transition could lead to a
// resource leak so the parcel is left unmodified
}
}
Esse código também pode ser encontrado aqui DefaultContentCardsViewBindingHandler
.
E aqui está como usar essa classe:
1
2
3
4
IContentCardsViewBindingHandler viewBindingHandler = new DefaultContentCardsViewBindingHandler();
ContentCardsFragment fragment = getMyCustomFragment();
fragment.setContentCardsViewBindingHandler(viewBindingHandler);
1
2
3
4
val viewBindingHandler = DefaultContentCardsViewBindingHandler()
val fragment = getMyCustomFragment()
fragment.setContentCardsViewBindingHandler(viewBindingHandler)
Outros recursos relevantes sobre esse tópico estão disponíveis neste artigo sobre Vinculação de dados do Android.
Para personalizar cartões no Jetpack Compose, crie uma função Composable personalizada da seguinte forma:
- Renderize a Composable e retorne
true
. - Não renderiza nada e retorna
false
. Quandofalse
for retornado, a Braze renderizará o cartão.
No exemplo a seguir, a função Composable renderiza os cartões TEXT_ANNOUNCEMENT
, enquanto a Braze renderiza automaticamente o restante:
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
val myCustomCardRenderer: @Composable ((Card) -> Boolean) = { card ->
if (card.cardType == CardType.TEXT_ANNOUNCEMENT) {
val textCard = card as TextAnnouncementCard
Box(
Modifier
.padding(10.dp)
.fillMaxWidth()
.background(color = Color.Red)
) {
Text(
modifier = Modifier
.align(Alignment.Center)
.fillMaxWidth()
.basicMarquee(iterations = Int.MAX_VALUE),
fontSize = 35.sp,
text = textCard.description
)
}
true
} else {
false
}
}
ContentCardsList(
customCardComposer = myCustomCardRenderer
)
Descarte de cartão
A desativação da funcionalidade de passar o dedo para recusar é feita por cartão por meio do método card.isDismissibleByUser()
método. Os cartões podem ser interceptados antes da exibição usando o método ContentCardsFragment.setContentCardUpdateHandler()
método.
Personalização do tema escuro
Por padrão, as exibições do cartão de conteúdo responderão automaticamente às alterações do tema escuro no dispositivo com um conjunto de cores temáticas e alterações de layout.
Para substituir esse comportamento, substitua os valores de values-night
em android-sdk-ui/src/main/res/values-night/colors.xml
e android-sdk-ui/src/main/res/values-night/dimens.xml
.
Registro de impressões, cliques e demissões
Depois de estender seus objetos personalizados para funcionar como cartões de conteúdo, o registro de métricas valiosas, como impressões, cliques e descartes de cartões, pode ser feito usando uma classe base ContentCardable
que faz referência e fornece dados para o BrazeManager
.
Componentes de implementação
Os objetos personalizados chamam os métodos de registro
Em sua classe base ContentCardable
, você pode chamar o BrazeManager
diretamente, se for o caso. Nesse exemplo, a propriedade cardData
será não nula se o objeto tiver vindo de um cartão de conteúdo.
1
2
3
4
5
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val tile = currentTiles[position]
tile.logContentCardImpression()
...
}
Recupere o cartão de conteúdo do ContentCardId
A classe de base ContentCardable
faz o trabalho pesado de chamar o BrazeManager
e passar o identificador exclusivo do cartão de conteúdo associado ao objeto personalizado.
1
2
3
fun logContentCardImpression() {
cardData?.let { BrazeManager.getInstance().logContentCardImpression(it.contentCardId) }
}
**Chame as funções Card
**
O BrazeManager
pode fazer referência às dependências do SDK da Braze, como a lista de vetores de objetos do cartão de conteúdo, para obter o Card
e chamar os métodos de registro da Braze.
1
2
3
4
5
6
7
8
9
10
11
fun logContentCardClicked(idString: String?) {
getContentCard(idString)?.logClick()
}
fun logContentCardImpression(idString: String?) {
getContentCard(idString)?.logImpression()
}
private fun getContentCard(idString: String?): Card? {
return cardList.find { it.id == idString }.takeIf { it != null }
}
Os objetos personalizados chamam os métodos de registro
Em sua classe base ContentCardable
, você pode chamar o BrazeManager
diretamente, se for o caso. Lembre-se: nesse exemplo, a propriedade cardData
será não nula se o objeto tiver vindo de um cartão de conteúdo.
1
2
3
4
5
6
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Tile tile = currentTiles.get(position);
tile.logContentCardImpression();
...
}
Recupere o cartão de conteúdo do ContentCardId
A classe de base ContentCardable
faz o trabalho pesado de chamar o BrazeManager
e passar o identificador exclusivo do cartão de conteúdo associado ao objeto personalizado.
1
2
3
4
5
public void logContentCardImpression() {
if (cardData != null){
BrazeManager.getInstance().logContentCardImpression(cardData.getContentCardId());
}
}
**Chame as funções Card
**
O BrazeManager
pode fazer referência às dependências do SDK da Braze, como a lista de vetores de objetos do cartão de conteúdo, para obter o Card
e chamar os métodos de registro da Braze.
1
2
3
4
5
6
7
8
9
10
11
public void logContentCardClicked(String idString) {
getContentCard(idString).ifPresent(Card::logClick);
}
public void logContentCardImpression(String idString) {
getContentCard(idString).ifPresent(Card::logImpression);
}
private Optional<Card> getContentCard(String idString) {
return cardList.filter(c -> c.id.equals(idString)).findAny();
}
Para uma variante de controle Content Card, um objeto personalizado ainda deve ser instanciado, e a lógica da interface do usuário deve definir a exibição correspondente do objeto como oculta. O objeto pode então registrar uma impressão para informar nossa análise de dados sobre quando um usuário teria visto o cartão de controle.
Arquivos auxiliares
Arquivo auxiliar do ContentCardKey
1
2
3
4
5
6
7
companion object Keys{
const val idString = "idString"
const val created = "created"
const val classType = "class_type"
const val dismissable = "dismissable"
//...
}
1
2
3
4
5
public static final String IDSTRING = "idString";
public static final String CREATED = "created";
public static final String CLASSTYPE = "class_type";
public static final String DISMISSABLE = "dismissable";
...