高度な実装ガイド (オプション)
このオプションおよび高度な実装ガイドでは、コンテンツカードコードの考慮事項、当社チームが作成した3つのカスタムユースケース、付随するコードスニペット、およびロギングインプレッション、クリック、および削除に関するガイダンスについて説明します。こちらから Braze Demo リポジトリにアクセスしてください。この実装ガイドは、Kotlin 実装を中心に扱っていますが、興味のある人のために Java のスニペットが提供されています。
コードに関する考慮事項
ステートメントおよびヘルパーファイルのインポート
コンテンツカードを作成する場合は、単一のマネージャーシングルトンを介して Braze SDK を公開する必要があります。このパターンにより、ユースケースに適した共通の抽象化の背後にある Braze 実装の詳細からアプリケーションコードを保護します。また、コードの追跡、デバッグ、変更も容易になります。マネージャの実装例は、こちらでご覧いただけます。
カスタムオブジェクトとしてのコンテンツカード
アプリケーションで既に使用されている独自のカスタムオブジェクトを拡張して、コンテンツカードデータを運ぶことができます。これにより、データのソースをアプリケーションコードで既に理解されている形式に抽象化できます。データソースの抽象化は、異なるデータバックエンドと互換性があり、同時に動作する柔軟性を提供します。この例では、ContentCardable
抽象ベースクラスを定義して、既存のデータ (この例では、ローカル JSON ファイルからフィードされます) と Braze SDK からフィードされる新しいデータの両方を表します。また、ベースクラスは、元のCard
実装にアクセスする必要がある消費者のコンテンツカードの生データも公開します。
Braze SDK からContentCardable
インスタンスを初期化する場合、class_type
extra を使用して、コンテンツカードを具象サブクラスにマップします。次に、Braze ダッシュボード内で設定された追加のキーと値のペアを使用して、必要なフィールドに入力します。
これらのコードに関する考慮事項をしっかりと理解したら、ユースケースをチェックして、独自のカスタムオブジェクトの実装を開始します。
Card
依存関係なし
ContentCardData
は、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)
Card
依存関係なし
ContentCardData
は、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;
}
カスタムオブジェクトイニシャライザ
Card
からの MetaData は、具象サブクラスの変数を入力するために使用されます。サブクラスによっては、初期化時に異なる値を抽出する必要があります。Braze ダッシュボードで設定されたキーと値のペアは、「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()
}
}
カスタムオブジェクトイニシャライザ
Card
からの MetaData は、具象サブクラスの変数を入力するために使用されます。サブクラスによっては、初期化時に異なる値を抽出する必要があります。Braze ダッシュボードで設定されたキーと値のペアは、「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);
}
}
}
タイプの識別
ContentCardClass
enum は、Braze ダッシュボードのclass_type
値を表し、SDK によって提供される文字列から enum を初期化するメソッドを提供します。
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
}
}
}
}
タイプの識別
ContentCardClass
enum は、Braze ダッシュボードのclass_type
値を表し、SDK によって提供される文字列から enum を初期化するメソッドを提供します。
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;
}
}
}
}
カスタムカードレンダリング{#customizing-card-rendering-for-android}
次のリストは、recyclerView
でカードをレンダリングする方法の変更について示しています。IContentCardsViewBindingHandler
インターフェイスは、すべてのコンテンツカードのレンダリング方法を定義します。これをカスタマイズして、必要なものを変更することができます。
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
}
}
このコードはここにもある。 DefaultContentCardsViewBindingHandler
.
次に、このクラスの使用方法を示します。
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)
このトピックに関するその他の関連リソースは、Android Data Binding に関するこの記事で入手できます。
Jetpack Compose でカードを完全にカスタマイズする場合、カスタムの Composable 関数を作成すると次のようになります。
- Composable をレンダリングし、
true
を返します。 - 何もレンダリングせず、
false
を返します。false
が返されると、Braze はカードをレンダリングします。
次の例では、Composable 関数はTEXT_ANNOUNCEMENT
カードをレンダリングし、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
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
)
カードの却下
Swipe-to-Dismiss機能の無効化は、以下のメソッドによってカードごとに行われる。 card.isDismissibleByUser()
メソッドで行う。カードは表示される前に ContentCardsFragment.setContentCardUpdateHandler()
メソッドを使う。
ダークテーマのカスタマイズ
デフォルトでは、コンテンツカードビューは、テーマカラーとレイアウト変更のセットでデバイスのダークテーマの変更に自動的に応答します。
この動作をオーバーライドするには、android-sdk-ui/src/main/res/values-night/colors.xml
およびandroid-sdk-ui/src/main/res/values-night/dimens.xml
のvalues-night
の値をオーバーライドします。
インプレッション、クリック、却下の記録
カスタムオブジェクトをコンテンツカードとして機能するように拡張した後、BrazeManager
を参照してデータを提供するContentCardable
ベースクラスを使用して、インプレッション、クリック、および却下などの貴重なメトリクスをログに記録することができます。
実装コンポーネント
カスタムオブジェクトによるロギングメソッドの呼び出し
ContentCardable
ベースクラス内で、必要に応じてBrazeManager
を直接呼び出すことができます。この例では、オブジェクトがコンテンツカードから取得された場合、cardData
プロパティは NULL 以外になります。
1
2
3
4
5
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val tile = currentTiles[position]
tile.logContentCardImpression()
...
}
ContentCardId
からコンテンツカードを取得する
ContentCardable
ベースクラスは、BrazeManager
を呼び出し、カスタムオブジェクトに関連付けられたコンテンツカードから一意の識別子を渡すという負荷の大きい処理を行います。
1
2
3
fun logContentCardImpression() {
cardData?.let { BrazeManager.getInstance().logContentCardImpression(it.contentCardId) }
}
Card
関数を呼び出す
BrazeManager
は、コンテンツカードオブジェクト配列リストなどの Braze SDK 依存関係を参照して、Card
にロギングメソッドを呼び出させることができます。
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 }
}
カスタムオブジェクトによるロギングメソッドの呼び出し
ContentCardable
ベースクラス内で、必要に応じてBrazeManager
を直接呼び出すことができます。この例では、オブジェクトがコンテンツ・カードから来たものであれば、cardData
プロパティが非NULLになることを覚えておいてほしい。
1
2
3
4
5
6
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Tile tile = currentTiles.get(position);
tile.logContentCardImpression();
...
}
ContentCardId
からコンテンツカードを取得する
ContentCardable
ベースクラスは、BrazeManager
を呼び出し、カスタムオブジェクトに関連付けられたコンテンツカードから一意の識別子を渡すという負荷の大きい処理を行います。
1
2
3
4
5
public void logContentCardImpression() {
if (cardData != null){
BrazeManager.getInstance().logContentCardImpression(cardData.getContentCardId());
}
}
Card
関数を呼び出す
BrazeManager
は、コンテンツカードオブジェクト配列リストなどの Braze SDK 依存関係を参照して、Card
にロギングメソッドを呼び出させることができます。
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();
}
コントロールバリアントのコンテンツカードの場合、カスタムオブジェクトはインスタンス化されたままで、UI ロジックはオブジェクトの対応するビューを非表示に設定する必要があります。その後、オブジェクトはインプレッションをログに記録して、ユーザーがいつコントロールカードを表示したかを分析に知らせることができます。
ヘルパーファイル
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";
...