高度な実装ガイド (オプション)
このオプションおよび高度な実装ガイドでは、コンテンツカードコードの考慮事項、当社チームが作成した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";
...
   Edit this page on GitHub