Piano Composer SDK for Flutter
Piano Composer Flutter SDK delivers personalized experiences, paywalls, and A/B tests from Piano's experience management platform, and reports user interactions back for attribution.
Installation
Add the dependency to your pubspec.yaml:
dependencies:
piano_composer: ^1.0.3
Getting Started
piano_composer builds on piano_common,
so you need a Piano instance first:
import 'package:piano_common/piano_common.dart';
import 'package:piano_composer/piano_composer.dart';
final piano = await Piano.init(
endpoint: PianoEndpoint.production,
aid: '<AID>',
);
Get a client via piano.composer(). Each call returns a new
PianoComposerClient, so retain the reference when you need to share
configuration or read back state (browserId, pageViewId) after a request:
final composer = piano.composer();
final request = PianoComposerRequest(
url: 'https://piano.io/article',
title: 'Article Title',
tags: ['technology', 'news'],
);
final response = await composer.execute(request);
Configuration
Configuration setters are chainable and return the same client:
final composer = piano.composer()
..userToken('<access_token>') // token for personalized experiences
..gaClientId('<ga_client_id>') // link experiences to Google Analytics
..browserIdProvider(() => '<browser_id>'); // supply a custom browser id
Experience Interceptors
Extend PianoComposerInterceptor to hook into the request lifecycle. Both
methods are optional:
class CustomInterceptor extends PianoComposerInterceptor {
@override
void beforeExecute(PianoComposerRequest request) {
// Inspect the request before it is sent.
}
@override
void afterExecute(
PianoComposerRequest request,
PianoComposerResponse response,
) {
// Handle the response.
}
}
final composer = piano.composer()
..addExperienceInterceptor(CustomInterceptor());
Building a Request
PianoComposerRequest carries the content metadata and targeting context.
All fields are optional:
| Field | Type | Description |
|---|---|---|
url |
String? |
Page URL |
referer |
String? |
Referring URL |
title |
String? |
Content title |
description |
String? |
Content description |
tags |
List<String> |
Content tags |
keywords |
List<String> |
Content keywords |
zone |
String? |
Targeting zone |
contentId |
String? |
Content identifier |
contentType |
String? |
Content type |
contentCreated |
String? |
ISO 8601 timestamp (use PianoComposerRequest.formatDate) |
contentAuthor |
String? |
Content author |
contentSection |
String? |
Content section |
contentIsNative |
bool? |
Whether the content is native |
customVariables |
Map<String, List<String>?> |
Custom targeting variables |
isDebug |
bool |
Enable verbose API responses |
final request = PianoComposerRequest(
url: 'https://piano.io/article/123',
title: 'Breaking News Article',
description: 'Latest updates on technology',
contentId: '123',
contentType: 'article',
contentCreated: PianoComposerRequest.formatDate(DateTime.now()),
contentAuthor: 'John Doe',
contentSection: 'Technology',
zone: 'homepage',
customVariables: {
'user_type': ['premium'],
'region': ['us-west'],
},
);
final response = await composer.execute(request);
Handling Events
execute returns a PianoComposerResponse whose result.events contains the
experience events. Handle them with typed listeners:
final response = await composer.execute(
request,
listeners: [
PianoComposerEventTypeListener<PianoComposerShowTemplate>(
onEvent: (event) => print('Show template: ${event.eventData.url}'),
),
PianoComposerEventTypeListener<PianoComposerMeter>(
onEvent: (event) => print('Meter event received'),
),
],
);
Or receive every event with a single callback:
final response = await composer.execute(
request,
onEvents: (events) {
for (final event in events) {
print('Event: ${event.eventData.runtimeType}');
}
},
);
Event types include PianoComposerExperienceExecute, PianoComposerMeter,
PianoComposerShowTemplate, PianoComposerShowForm,
PianoComposerShowLogin, PianoComposerShowRecommendations,
PianoComposerUserSegment, PianoComposerSetResponseVariable, and
PianoComposerNonSite.
Tracking
Report user interactions back to Piano through a PianoComposerTracking
instance so they are attributed to the originating experience:
final tracking = piano.composer().tracking;
// User dismissed a template or experience.
await tracking.trackCloseEvent('<tracking_id>');
// A recommendations widget was shown, then clicked.
await tracking.trackRecommendationsDisplay('<tracking_id>');
await tracking.trackRecommendationsClick('<tracking_id>', '<url>');
// A custom form was shown, then submitted.
await tracking.trackCustomFormImpression('<form_name>', '<tracking_id>');
await tracking.trackCustomFormSubmission('<form_name>', '<tracking_id>');
Every event received from execute also carries its own tracking reference
and tracking id, so there is no need to keep the client around:
await composer.execute(
request,
listeners: [
PianoComposerEventTypeListener<PianoComposerShowTemplate>(
onEvent: (event) => event.tracking?.trackCloseEvent(
event.eventExecutionContext.trackingId,
),
),
],
);
tracking is null only for events you construct yourself (for example via
PianoComposerEvent.fromJson), because the client attaches it while processing
the response.
Reading Response Data
// Retain the same instance to read back state after execute().
final composer = piano.composer();
final response = await composer.execute(request);
// Cookies
final tbCookie = response.tbCookie; // tracking cookie
final xbCookie = response.xbCookie; // experience cookie
final taCookie = response.taCookie; // analytics cookie
// Identifiers
final browserId = composer.browserId;
final pageViewId = composer.pageViewId;
final userId = response.userId;
// Edge cookies (if enabled)
final edgeCookies = composer.edgeCookies;
Clearing Stored Data
Clear all locally stored data and cookies:
piano.composer().clearStoredData();
Error Handling
execute throws a PianoException when the API returns errors:
try {
final response = await piano.composer().execute(request);
} on PianoException catch (e) {
print('Piano error: ${e.message}');
} catch (e) {
print('Unexpected error: $e');
}