高度な実装ガイド (オプション)
このオプションおよび高度な実装ガイドでは、コンテンツカードコードの考慮事項、当社チームが作成した3つのカスタムユースケース、付随するコードスニペット、およびロギングインプレッション、クリック、および削除に関するガイダンスについて説明します。こちらから Braze Demo リポジトリにアクセスしてください。この実装ガイドは、Kotlin 実装を中心に扱っていますが、興味のある人のために Java のスニペットが提供されています。
コードに関する考慮事項
ステートメントおよびヘルパーファイルのインポート
コンテンツカードを作成する場合は、単一のマネージャーシングルトンを介して Braze SDK を公開する必要があります。このパターンにより、ユースケースに適した共通の抽象化の背後にある Braze 実装の詳細からアプリケーションコードを保護します。また、コードの追跡、デバッグ、変更も容易になります。マネージャの実装例は、こちらでご覧いただけます。
カスタムオブジェクトとしてのコンテンツカード
アプリケーションで既に使用されている独自のカスタムオブジェクトを拡張して、コンテンツカードデータを運ぶことができます。これにより、データのソースをアプリケーションコードで既に理解されている形式に抽象化できます。データソースの抽象化は、異なるデータバックエンドと互換性があり、同時に動作する柔軟性を提供します。この例では、ContentCardable
抽象ベースクラスを定義して、既存のデータ (この例では、ローカル JSON ファイルからフィードされます) と Braze SDK からフィードされる新しいデータの両方を表します。また、ベースクラスは、元のCard
実装にアクセスする必要がある消費者のコンテンツカードの生データも公開します。
Braze SDK からContentCardable
インスタンスを初期化する場合、class_type
extra を使用して、コンテンツカードを具象サブクラスにマップします。次に、Braze ダッシュボード内で設定された追加のキーと値のペアを使用して、必要なフィールドに入力します。
これらのコードに関する考慮事項をしっかりと理解したら、ユースケースをチェックして、独自のカスタムオブジェクトの実装を開始します。
Card
依存関係なし
ContentCardData
は、Card
の解析された共通の値を表します。
```kotlin abstract class ContentCardable (){
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
の解析された共通の値を表します。
```java 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」ディクショナリに表示されます。
```java public class Tile extends ContentCardable {
1
2
3
4
5
6
7
8
9
10
11
12
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);
}
} } \`\`\`
カスタムカードレンダリング{#customizing-card-rendering-for-android}
次のリストは、recyclerView
でカードをレンダリングする方法の変更について示しています。IContentCardsViewBindingHandler
インターフェイスは、すべてのコンテンツカードのレンダリング方法を定義します。これをカスタマイズして、必要なものを変更することができます。
```java
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
1
2
3
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}. * {@link CardType} がキャッシュ内に見つからない場合、その {@link CardType} のビューバインディング実装 * が作成され、キャッシュに追加されます。 */ @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); }
// パーセル可能なインターフェイス方式 @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 // リソースがリークするため、パーセルが変更されないままになります } } ```
```kotlin
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()
}
1
2
3
override fun newArray(size: Int): Array<DefaultContentCardsViewBindingHandler?> {
return arrayOfNulls(size)
} }
/** * [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:Context?, cards: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
/**
-
Gets a cached instance of a [BaseContentCardView] for view creation/binding for a given [CardType]. * [CardType] がキャッシュに見つからない場合、その [CardType] のビューバイディング実装 * が作成され、キャッシュに追加されます。 */ @VisibleForTesting fun getContentCardsViewFromCache(context:Context?, cardType:CardType):BaseContentCardView
? { if (!mContentCardViewCache.containsKey(cardType)) { // ここでビューを作成します 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 ? } // パーセル可能なインターフェイス方式 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 // リソースがリークするため、パーセルが変更されないままになります } } ```
このコードは [`DefaultContentCardsViewBindingHandler`][56] にもあります。
次に、このクラスの使用方法を示します。
```java IContentCardsViewBindingHandler viewBindingHandler = new DefaultContentCardsViewBindingHandler();
ContentCardsFragment fragment = getMyCustomFragment(); fragment.setContentCardsViewBindingHandler(viewBindingHandler); ```
```kotlin val viewBindingHandler = DefaultContentCardsViewBindingHandler()
val fragment = getMyCustomFragment() fragment.setContentCardsViewBindingHandler(viewBindingHandler) ```
このトピックに関するその他の関連リソースは、Android Data Binding に関するこの記事で入手できます。
Jetpack Compose でカードを完全にカスタマイズする場合、カスタムの Composable 関数を作成すると次のようになります。
- Composable をレンダリングし、
true
を返します。 - 何もレンダリングせず、
false
を返します。false
が返されると、Braze はカードをレンダリングします。
次の例では、Composable 関数はTEXT_ANNOUNCEMENT
カードをレンダリングし、Braze は残りを自動的にレンダリングします。
```kotlin 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 ) ```
カードの却下
スワイプして閉じる機能を無効にするには、`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
にロギングメソッドを呼び出させることができます。
```kotlin fun logContentCardClicked(idString:String?) { getContentCard(idString)?.logClick() }
1
2
3
4
5
6
7
fun logContentCardImpression(idString: String?) {
getContentCard(idString)?.logImpression()
}
private fun getContentCard(idString: String?): Card? {
return cardList.find { it.id == idString }.takeIf { it != null }
} ```
Custom objects call the logging methods
Within your ContentCardable
base class, you can call the BrazeManager
directly, if appropriate. Remember, in this example, the cardData
property will be non-null if the object came from a Content Card.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Tile tile = currentTiles.get(position);
tile.logContentCardImpression();
...
}
\`\`\`
**`ContentCardId`からコンテンツカードを取得する**<br>
`ContentCardable`ベースクラスは、`BrazeManager`を呼び出し、カスタムオブジェクトに関連付けられたコンテンツカードから一意の識別子を渡すという負荷の大きい処理を行います。
```java
public void logContentCardImpression() {
if (cardData != null){
BrazeManager.getInstance().logContentCardImpression(cardData.getContentCardId());
}
}
Card
関数を呼び出す
BrazeManager
は、コンテンツカードオブジェクト配列リストなどの Braze SDK 依存関係を参照して、Card
にロギングメソッドを呼び出させることができます。
```java public void logContentCardClicked(String idString) { getContentCard(idString).ifPresent(Card::logClick); }
1
2
3
4
5
6
7
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 Helper File
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";
...