デフォルトのコンテンツカードフィードのカスタマイズ
コンテンツカードフィードは、モバイルまたは Web アプリケーションの一連のコンテンツカードです。この記事では、フィードの更新時の設定、カードの順序、複数フィードの管理、「空のフィード」エラーメッセージについて説明します。コンテンツカードで使用するカスタマイズオプションのタイプの基本的な概要については、カスタマイズの概要を参照してください。
フィードの更新
デフォルトでは、コンテンツカードフィードは次の場合に自動的に更新されます。
- 新しいセッションが開始される場合
- フィードが開かれ、最後の更新から 60 秒以上経過した場合
SDK は、特定の時間に手動で更新するように設定することもできます。
手動で更新せずに最新のコンテンツカードを動的に表示するには、カード作成時に [最初のインプレッション発生時] を選択します。これらのカードは、使用可能になると更新されます。
Android SDK からrequestContentCardsRefresh
を呼び出すことで、いつでも手動で Braze コンテンツカードの更新を要求します。
1
Braze.getInstance(context).requestContentCardsRefresh();
1
Braze.getInstance(context).requestContentCardsRefresh()
Braze.ContentCards
クラスでrequestRefresh
メソッドを呼び出すことで、いつでも Swift SDK から Braze コンテンツカードの手動更新を要求します。
Swift では、オプションの補完ハンドラまたはネイティブの Swift concurrency API を使用した非同期リターンを使用して、コンテンツカードを更新できます。
完了ハンドラ
1
2
3
AppDelegate.braze?.contentCards.requestRefresh { result in
// Implement completion handler
}
非同期/待機
1
let contentCards = await AppDelegate.braze?.contentCards.requestRefresh()
1
2
3
[AppDelegate.braze.contentCards requestRefreshWithCompletion:^(NSArray<BRZContentCardRaw *> * contentCards, NSError * error) {
// Implement completion handler
}];
Web SDK からrequestContentCardsRefresh()
を呼び出して、いつでも手動で Braze コンテンツカードのリフレッシュを要求します。
また、getCachedContentCards
を呼び出して、最新のコンテンツカード更新から現在利用可能なすべてのカードを取得することもできます。
1
2
3
4
5
import * as braze from "@braze/web-sdk";
function refresh() {
braze.requestContentCardsRefresh();
}
最大5回まで連続して呼び出しを行うことができます。その後は、180秒ごとに1つの新しい呼び出しが可能になります。システムは、いつでも使用できるように最大5つの呼び出しを保持します。
表示されるカードの順序をカスタマイズする
コンテンツカードの表示順序を変更できます。これにより、時間的制約のあるプロモーションなど、特定のタイプのコンテンツに優先順位を付けることで、ユーザーエクスペリエンスを微調整できます。
ContentCardsFragment
は、IContentCardsUpdateHandler
に依存して、フィードに表示される前にコンテンツカードのソートまたは変更を処理します。カスタム更新ハンドラは、ContentCardsFragment
のsetContentCardUpdateHandler
で設定できます。
以下はデフォルトのIContentCardsUpdateHandler
であり、カスタマイズの開始点として使用できます。
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
public class DefaultContentCardsUpdateHandler implements IContentCardsUpdateHandler {
// 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<DefaultContentCardsUpdateHandler> CREATOR = new Parcelable.Creator<DefaultContentCardsUpdateHandler>() {
public DefaultContentCardsUpdateHandler createFromParcel(Parcel in) {
return new DefaultContentCardsUpdateHandler();
}
public DefaultContentCardsUpdateHandler[] newArray(int size) {
return new DefaultContentCardsUpdateHandler[size];
}
};
@Override
public List<Card> handleCardUpdate(ContentCardsUpdatedEvent event) {
List<Card> sortedCards = event.getAllCards();
// Sort by pinned, then by the 'updated' timestamp descending
// Pinned before non-pinned
Collections.sort(sortedCards, new Comparator<Card>() {
@Override
public int compare(Card cardA, Card cardB) {
// A displays above B
if (cardA.getIsPinned() && !cardB.getIsPinned()) {
return -1;
}
// B displays above A
if (!cardA.getIsPinned() && cardB.getIsPinned()) {
return 1;
}
// At this point, both A & B are pinned or both A & B are non-pinned
// A displays above B since A is newer
if (cardA.getUpdated() > cardB.getUpdated()) {
return -1;
}
// B displays above A since A is newer
if (cardA.getUpdated() < cardB.getUpdated()) {
return 1;
}
// At this point, every sortable field matches so keep the natural ordering
return 0;
}
});
return sortedCards;
}
// Parcelable interface method
@Override
public int describeContents() {
return 0;
}
// Parcelable interface method
@Override
public void writeToParcel(Parcel dest, int flags) {
// No state is kept in this class 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
class DefaultContentCardsUpdateHandler : IContentCardsUpdateHandler {
override fun handleCardUpdate(event: ContentCardsUpdatedEvent): List<Card> {
val sortedCards = event.allCards
// Sort by pinned, then by the 'updated' timestamp descending
// Pinned before non-pinned
sortedCards.sortWith(Comparator sort@{ cardA: Card, cardB: Card ->
// A displays above B
if (cardA.isPinned && !cardB.isPinned) {
return@sort -1
}
// B displays above A
if (!cardA.isPinned && cardB.isPinned) {
return@sort 1
}
// At this point, both A & B are pinned or both A & B are non-pinned
// A displays above B since A is newer
if (cardA.updated > cardB.updated) {
return@sort -1
}
// B displays above A since A is newer
if (cardA.updated < cardB.updated) {
return@sort 1
}
0
})
return sortedCards
}
// Parcelable interface method
override fun describeContents(): Int {
return 0
}
// Parcelable interface method
override fun writeToParcel(dest: Parcel, flags: Int) {
// No state is kept in this class so the parcel is left unmodified
}
companion object {
// 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<DefaultContentCardsUpdateHandler?> = object : Parcelable.Creator<DefaultContentCardsUpdateHandler?> {
override fun createFromParcel(`in`: Parcel): DefaultContentCardsUpdateHandler? {
return DefaultContentCardsUpdateHandler()
}
override fun newArray(size: Int): Array<DefaultContentCardsUpdateHandler?> {
return arrayOfNulls(size)
}
}
}
}
ContentCardsFragment
ソースは GitHub にあります。
Jetpack Compose でコンテンツカードをフィルタリングおよびソートするには、cardUpdateHandler
パラメータを設定します。以下に例を示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ContentCardsList(
cardUpdateHandler = {
it.sortedWith { cardA, cardB ->
// A displays above B
if (cardA.isPinned && !cardB.isPinned) {
return@sortedWith -1
}
// B displays above A
if (!cardA.isPinned && cardB.isPinned) {
return@sortedWith 1
}
// At this point, both A & B are pinned or both A & B are non-pinned
// A displays above B since A is newer
if (cardA.updated > cardB.updated) {
return@sortedWith -1
}
// B displays above A since A is newer
if (cardA.updated < cardB.updated) {
return@sortedWith 1
}
0
}
}
)
静的なAttributes.defaults
変数を直接変更して、カードフィードの順序をカスタマイズします。
1
2
3
4
5
6
7
8
9
10
11
12
13
var attributes = BrazeContentCardUI.ViewController.Attributes.defaults
attributes.transform = { cards in
cards.sorted {
if $0.pinned && !$1.pinned {
return true
} else if !$0.pinned && $1.pinned {
return false
} else {
return $0.createdAt > $1.createdAt
}
}
}
let viewController = BrazeContentCardUI.ViewController(braze: AppDelegate.braze, attributes: attributes)
BrazeContentCardUI.ViewController.Attributes
によるカスタマイズは Objective-C では使用できません。
showContentCards():
のfilterFunction
パラメーターを使用して、フィードのコンテンツカードの表示順序をカスタマイズします。以下に例を示します。
1
2
3
braze.showContentCards(null, (cards) => {
return sortBrazeCards(cards); // Where sortBrazeCards is your sorting function that returns the sorted card array
});
「空のフィード」メッセージのカスタマイズ
ユーザーがコンテンツカードに適格でない場合、SDK は次のような「空のフィード」エラーメッセージを表示します。「更新はありません。後で再度確認してください。」この「空のフィード」エラーメッセージは、次のようにカスタマイズできます。
ContentCardsFragment
によって、ユーザがコンテンツカードに対応していないと判断された場合は、空のフィードエラーメッセージが表示されます。
特殊なアダプタEmptyContentCardsAdapter
は、標準のContentCardAdapter
を置き換えてこのエラーメッセージを表示します。カスタムメッセージ自体を設定するには、文字列リソースcom_braze_feed_empty
を上書きします。
このメッセージを表示するために使用されるスタイルは、Braze.ContentCardsDisplay.Empty
で見つけることができ、次のコードスニペットで再現されます。
1
2
3
4
5
6
7
8
9
<style name="Braze.ContentCardsDisplay.Empty">
<item name="android:lineSpacingExtra">1.5dp</item>
<item name="android:text">@string/com_braze_feed_empty</item>
<item name="android:textColor">@color/com_braze_content_card_empty_text_color</item>
<item name="android:textSize">18.0sp</item>
<item name="android:gravity">center</item>
<item name="android:layout_height">match_parent</item>
<item name="android:layout_width">match_parent</item>
</style>
コンテンツカードスタイル要素のカスタマイズの詳細については、スタイルのカスタマイズを参照してください。
Jetpack Compose で「空のフィード」エラーメッセージをカスタマイズするには、emptyString
をContentCardsList
に渡します。emptyTextStyle
をContentCardListStyling
に渡して、このメッセージをさらにカスタマイズすることもできます。
1
2
3
4
5
6
ContentCardsList(
emptyString = "No messages today",
style = ContentCardListStyling(
emptyTextStyle = TextStyle(...)
)
)
代わりに表示するコンポーザブルがある場合は、emptyComposable
をContentCardsList
に渡します。emptyComposable
を指定した場合、emptyString
は使用されません。
1
2
3
4
5
6
7
8
ContentCardsList(
emptyComposable = {
Image(
painter = painterResource(id = R.drawable.noMessages),
contentDescription = "No messages"
)
}
)
関連するAttributes
を設定して、ビューコントローラーの空の状態をカスタマイズします。
1
2
3
4
var attributes = BrazeContentCardUI.ViewController.Attributes.defaults
attributes.emptyStateMessage = "This is a custom empty state message"
attributes.emptyStateMessageFont = .preferredFont(forTextStyle: .title1)
attributes.emptyStateMessageColor = .secondaryLabel
空のコンテンツカードフィードに自動的に表示される言語を変更するには、アプリのContentCardsLocalizable.strings
ファイルでローカライズ可能なコンテンツカード文字列を再定義します。
別のロケール言語でこのメッセージを更新する場合は、リソースフォルダ構造で対応する言語を文字列ContentCardsLocalizable.strings
で検索します。
Web SDK では、「空のフィード」の言語をプログラムで置き換えることはできません。フィードが表示されるたびに置き換えることを選択できますが、フィードが更新され、空のフィードテキストがすぐに表示されないため、これはお勧めしません。
複数のフィード
コンテンツカードは、特定のカードのみが表示されるようにアプリでフィルタリングできます。これにより、さまざまなユースケースで複数のコンテンツカードフィードを使用できます。たとえば、トランザクションフィードとマーケティングフィードの両方を維持できます。これを行うには、Braze ダッシュボードでキーと値のペアを設定して、コンテンツカードのさまざまなカテゴリーを作成します。次に、これらのタイプのコンテンツカードを異なる方法で処理し、一部のタイプをフィルタリングして他のタイプを表示するフィードをアプリまたはサイトに作成します。
ステップ1:カードにキーと値のペアを設定する
コンテンツカードキャンペーンを作成する場合は、各カードでキーと値のペアデータを設定します。このキーと値のペアを使用して、カードを分類します。キーと値のペアは、カードのデータモデルのextras
プロパティに保存されます。
この例では、カードが表示されるコンテンツカードフィードを指定するキーfeed_type
を使用してキーと値のペアを設定します。この値は、home_screen
やmarketing
など、カスタムフィードの値になります。
ステップ 2:コンテンツカードのフィルタリング
キーと値のペアが割り当てられたら、他のタイプのカードを表示およびフィルタリングするカードを表示するロジックを含むフィードを作成します。この例では、feed_type: "Transactional"
のキーと値のペアが一致するカードのみを表示します。
コンテンツカードのフィルタリングは、Card.getExtras()
を介してダッシュボードに設定されたキーと値のペアを読み取り、カスタム更新ハンドラを使用してフィルタリング (または他の必要なロジックを実行) することで実現できます。
詳細に説明すると、コンテンツカードフィードがContentCardsFragment
に表示されます。デフォルトのIContentCardsUpdateHandler
は、Braze SDK からContentCardsUpdatedEvent
を受け取り、表示するカードのリストを返しますが、カードの並べ替えのみを行い、それ自体は削除やフィルタリングを実行しません。
ContentCardsFragment
でフィルタリングできるようにするには、カスタムのIContentCardsUpdateHandler
を作成します。このIContentCardsUpdateHandler
を変更して、前に設定したfeed_type
の目的の値と一致しないカードをリストから削除します。以下に例を示します。
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
private IContentCardsUpdateHandler getUpdateHandlerForFeedType(final String desiredFeedType) {
return new IContentCardsUpdateHandler() {
@Override
public List<Card> handleCardUpdate(ContentCardsUpdatedEvent event) {
// Use the default card update handler for a first
// pass at sorting the cards. This is not required
// but is done for convenience.
final List<Card> cards = new DefaultContentCardsUpdateHandler().handleCardUpdate(event);
final Iterator<Card> cardIterator = cards.iterator();
while (cardIterator.hasNext()) {
final Card card = cardIterator.next();
// Make sure the card has our custom KVP
// from the dashboard with the key "feed_type"
if (card.getExtras().containsKey("feed_type")) {
final String feedType = card.getExtras().get("feed_type");
if (!desiredFeedType.equals(feedType)) {
// The card has a feed type, but it doesn't match
// our desired feed type, remove it.
cardIterator.remove();
}
} else {
// The card doesn't have a feed
// type at all, remove it
cardIterator.remove();
}
}
// At this point, all of the cards in this list have
// a feed type that explicitly matches the value we put
// in the dashboard.
return cards;
}
};
}
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
private fun getUpdateHandlerForFeedType(desiredFeedType: String): IContentCardsUpdateHandler {
return IContentCardsUpdateHandler { event ->
// Use the default card update handler for a first
// pass at sorting the cards. This is not required
// but is done for convenience.
val cards = DefaultContentCardsUpdateHandler().handleCardUpdate(event)
val cardIterator = cards.iterator()
while (cardIterator.hasNext()) {
val card = cardIterator.next()
// Make sure the card has our custom KVP
// from the dashboard with the key "feed_type"
if (card.extras.containsKey("feed_type")) {
val feedType = card.extras["feed_type"]
if (desiredFeedType != feedType) {
// The card has a feed type, but it doesn't match
// our desired feed type, remove it.
cardIterator.remove()
}
} else {
// The card doesn't have a feed
// type at all, remove it
cardIterator.remove()
}
}
// At this point, all of the cards in this list have
// a feed type that explicitly matches the value we put
// in the dashboard.
cards
}
}
IContentCardsUpdateHandler
を作成したら、それを使用するContentCardsFragment
を作成します。このカスタムフィードは、他のContentCardsFragment
と同様に使用できます。アプリのさまざまな部分で、ダッシュボードに用意されているキーに基づいて、さまざまなコンテンツカードフィードを表示します。各ContentCardsFragment
フィードには、各フラグメントのカスタムIContentCardsUpdateHandler
のおかげで一意のカードセットが表示されます。
以下に例を示します。
1
2
3
// We want a Content Cards feed that only shows "Transactional" cards.
ContentCardsFragment customContentCardsFragment = new ContentCardsFragment();
customContentCardsFragment.setContentCardUpdateHandler(getUpdateHandlerForFeedType("Transactional"));
1
2
3
// We want a Content Cards feed that only shows "Transactional" cards.
val customContentCardsFragment = ContentCardsFragment()
customContentCardsFragment.contentCardUpdateHandler = getUpdateHandlerForFeedType("Transactional")
このフィードに表示されるコンテンツカードをフィルタリングするには、cardUpdateHandler
を使用します。以下に例を示します。
1
2
3
4
5
6
7
ContentCardsList(
cardUpdateHandler = {
it.filter { card ->
card.extras["feed_type"] == "Transactional"
}
}
)
次の例では、Transactional
タイプカードのコンテンツカードフィードを示します。
1
2
// Filter cards by the `Transactional` feed type based on your key-value pair.
let transactionalCards = cards.filter { $0.extras["feed_type"] as? String == "Transactional" }
さらに一歩進むために、ビューコントローラーに表示されるカードは、transform
プロパティをAttributes
構造体に設定して、条件でフィルタリングされたカードのみを表示することでフィルタリングできます。
1
2
3
4
5
6
7
var attributes = BrazeContentCardUI.ViewController.Attributes.defaults
attributes.transform = { cards in
cards.filter { $0.extras["feed_type"] as? String == "Transactional" }
}
// Pass your attributes containing the transformed cards to the Content Card UI.
let viewController = BrazeContentCardUI.ViewController(braze: AppDelegate.braze, attributes: attributes)
1
2
3
4
5
6
7
// Filter cards by the `Transactional` feed type based on your key-value pair.
NSMutableArray<BRZContentCardRaw *> *transactionalCards = [[NSMutableArray alloc] init];
for (BRZContentCardRaw *card in AppDelegate.braze.contentCards.cards) {
if ([card.extras[@"feed_type"] isEqualToString:@"Transactional"]) {
[transactionalCards addObject:card];
}
}
次の例では、Transactional
タイプカードのコンテンツカードフィードを示します。
1
2
3
4
5
6
7
8
9
/**
* @param {String} feed_type - value of the "feed_type" KVP to filter
*/
function showCardsByFeedType(feed_type) {
braze.showContentCards(null, function(cards) {
return cards.filter((card) => card.extras["feed_type"] === feed_type);
});
}
次に、カスタムフィードのトグルを設定できます。
1
2
3
4
// show the "Transactional" feed when this button is clicked
document.getElementById("show-transactional-feed").onclick = function() {
showCardsByFeedType("Transactional");
};
詳細については、SDK メソッドドキュメントを参照してください。