TCDigital Flyer
The Transcontinental Android SDK provides an easy way to interact with the Transcontinental Web API in order to access digital flyers. The SDK also provides pre-built UI components to allow users to browse through flyers and retrieve detailed product information.
Getting Started
The SDK can be installed using Gradle/Maven following the Instructions
below, followed by Using the SDK
for a quick summary of interacting with the SDK.
Upgrading from a previous version? Refer to the Migration Guide
.
Minimum Requirements
Android Studio
Android (API level 26+)
Installation
Using Gradle to install the SDK from a private JitPack repository:
Overview
The latest Android library resides in a private repository, accessible through Gradle/Maven. Upon registering your application with Transcontinental, you'll receive an authentication token to download the SDK.
Authentication
Add your Jitpack authentication token to your local.properties:
TC_JITPACK_TOKEN=my-authentication-token
Add the Jitpack repository to your root
build.gradle
(orsettings.gradle
) at the end of the repositories:
allprojects {
repositories {
...
maven {
url "https://jitpack.io"
credentials { username properties.getProperty("TC_JITPACK_TOKEN") }
}
}
}
Depending on which file you've put your repositories
in, you may have to use different techniques to access the TC_JITPACK_TOKEN
property.
// From the root build.gradle
def Properties properties = new Properties()
properties.load(project.rootProject.file("local.properties").newDataInputStream())
// From the settings.gradle
Properties properties = new Properties()
File propertiesFile = new File("local.properties")
if (propertiesFile.exists()) {
properties.load(new FileInputStream(propertiesFile))
}
Add the dependency:
dependencies {
implementation 'com.github.transcontinentalSdk:sdk-android-internal:2.4.2'
}
Using the SDK
Logging API
The SDK uses the following protocol for internal logging:
interface Logger {
fun verbose(message: String)
fun debug(message: String)
fun info(message: String)
fun warning(message: String)
fun error(message: String)
}
Consequently, nothing will print to the console from within the SDK until you set up a specific logger within the host application. Although the example below demonstrates a logger using Timber
statements, it's expected that this logger will utilize the same console, file, or cloud logging system as the host application.
The logger is a static object within the TCDigitalFlyer module. It's advisable to assign it before initializing the SDK, though you can do so at any time or leave it nil.
For production, the host application should refrain from logging debug and verbose volumes as they may contain sensitive URLs or tokens.
import com.tctranscontinental.android.digitalflyer.DigitalFlyer.logger
logger = object : Logger {
override fun verbose(message: String) {
Timber.v(message)
}
override fun debug(message: String) {
Timber.d(message)
}
override fun info(message: String) {
Timber.i(message)
}
override fun warning(message: String) {
Timber.w(message)
}
override fun error(message: String) {
Timber.e(message)
}
}
Initializing the SDK
To retrieve your flyers, you will need the Transcontinental subscription key
for your account. This key will be provided when registering your application with Transcontinental. The SDK uses this key internally to communicate with the Web API.
Before using any APIs, initialize the SDK with your subscription key and client name:
import com.tctranscontinental.android.digitalflyer.DigitalFlyer
val digitalFlyer = DigitalFlyer(
subscriptionKey = subscriptionKey,
client = client,
banner = banner,
environment = environment
)
subscriptionKey
- You receive this key when you register your application with Transcontinentalclient
- The client’s name, as provided by Transcontinentalbanner
- An optional banner, as provided by Transcontinental (defaults to the value provided to client)environment
- The API Environment (defaults toEnvironment.LIVE
)
While it’s technically possible to instantiate multiple DigitalFlyer objects with the same inputs on-demand, it’s recommended to use a single instance throughout your application (e.g. a reference in your Application class).
Instantiating multiple DigitalFlyer objects with different inputs is not currently supported. It will result in undefined behavior.
Listing Available Publications
The DigitalFlyer SDK provides a simple and efficient way to retrieve digital flyers using the listPublications
function. Upon successful retrieval, a list of Publication.Attributes
objects will be returned.
val publications = digitalFlyer.listPublications(
storeId = storeId,
language = language,
date = date
)
storeId
- The store’s identifierlanguage
- ThePublication
’s displayLanguage
(an enum defaulting toLanguage.ENGLISH
)date
- The date to search for available Publications (defaults to today)
Retrieving a Publication
Once you have a list of Publication.Attributes
objects, you can retrieve a Publication
object using the getPublication
function.
val publication = digitalFlyer.getPublication(
attributes = attributes,
orientation = FlyerLayout.VERTICAL
)
attributes
- ThePublication.Attributes
object returned fromlistPublications
orientation
- The layout version of the publication, specified via constsFlyerLayout.VERTICAL
andFlyerLayout.HORIZONTAL
(defaults toFlyerLayout.VERTICAL
)
Rendering a Publication
The publication can be rendered with 2 options: Compose view or legacy XML view.
Compose
The FlyerViewUI
is a custom view that serves as the entry point for displaying the digital flyer in Compose. It accepts the attributes of a publication to display, along with a style extension that governs its visual behaviour.
The FlyerViewUI is otherwise a fully controlled component, managing it's own functionality and API calls internally.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
setContent {
FlyerViewUI(
digitalFlyer = digitalFlyer,
attributes = attributes,
flyerStyle = myCustomStyle,
) { event ->
// This will be called when an event is emitted
when (event) {
is Event.Sku -> println(event.sku)
is Event.Url -> println(event.url)
else -> TODO("Handle other events")
}
}
}
}
digitalFlyer
- The provisioned instance of theDigitalFlyer
to useattributes
- ThePublication.Attributes
object returned fromlistPublications
flyerStyle
- The style extension to apply to the FlyerViewUI controlsevent
- TheEvent
object that represents an event emitted by theFlyerView
The following events are emitted by the FlyerViewUI
:
Event.Sku
- The SKU of the product whose click region was tappedEvent.Url
- The URL of the internal or external link that was tapped
XML
For the legacy XML variant, add <FlyerView>
to your layout file:
<?xml version="1.0" encoding="utf-8"?>
<!-- activity_flyer_view.xml -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.tctranscontinental.android.digitalflyer.ui.xml.FlyerView
android:id="@+id/flyer_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
or use it as a root element for your activity/fragment:
<?xml version="1.0" encoding="utf-8"?>
<!-- activity_flyer_view.xml -->
<com.tctranscontinental.android.digitalflyer.ui.xml.FlyerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/flyer_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
and pass a publication to it via FlyerView.setActivePublication()
:
import com.tctranscontinental.android.digitalflyer.ui.xml.FlyerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
val flyerView: FlyerView = findViewById(R.id.flyer_view)
flyerView.setActivePublication(publication) { event ->
when (event) {
is Event.Sku -> println("SKU: ${event.sku}") // This will be called when an event is emitted
else -> TODO("Handle other events")
}
}
}
Alternatively, everything can be added dynamically via code (skipping xml):
import com.tctranscontinental.android.digitalflyer.ui.xml.FlyerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
val flyerView = FlyerView(this)
setContentView(flyerView)
flyerView.setActivePublication(publication) { event ->
when (event) {
is Event.Sku -> println("SKU: ${event.sku}") // This will be called when an event is emitted
else -> TODO("Handle other events")
}
}
}
Error Handling
Error handling in the DigitalFlyer SDK is represented through the DFError
enum, which covers various cases including unauthorized access, missing subscription key, or missing resources.
Here are the possible error scenarios:
UNAUTHORIZED
: Represents an unauthorized request, usually due to an invalid subscription key.NOT_FOUND
: The requested resource was not found on the server. This could happen if the resource was deleted, or if the resource was not correctly specified.PUBLICATION_NOT_FOUND
: Indicates a failed request due to an unavailable publication.MISSING_SUBSCRIPTION_KEY
: This error occurs when the subscriptionKey is not provided during DigitalFlyer initialization.MISSING_CLIENT
: Occurs when the client parameter is missing during DigitalFlyer initialization.MISSING_BANNER
: This error indicates that the banner parameter is missing during DigitalFlyer initialization.INVALID
: A generic error case for any other unidentified issue.
Styling the Controls
The default FlyerStyle
is minimal and uses native styling where possible. As a result, users of the FlyerViewUI
will want to create a custom style and apply that as a view modifier to the FlyerViewUI
.
A simple customization is shown below, where only the Masthead's logo is configured.
// Uses a locally available logo in the masthead (recommended)
private val myCustomStyle = FlyerStyle(masthead = MastheadStyle(logo = R.drawable.my_logo))
// Uses the default TC logo in the masthead
private val myCustomStyle = FlyerStyle(masthead = MastheadStyle())
// Attempts to use the logo available via the API
// Not recommended, as it incurs a network request on each flyer viewing
private val myCustomStyle = FlyerStyle(masthead = MastheadStyle(logo = null))
Refer to com.tctranscontinental.android.digitalflyer.ui.FlyerStyle for details on available modifiers.
Note: As the FlyerStyle
does not represent core rendering functionality, breaking changes to the style parameters will not result in a major SDK version update.
Migration Guide
Transition existing apps from v1.x to v2.x
Overview
The v2.0.0 update introduces substantial performance improvements for the SDK, driven by several optimizations and technical enhancements:
Reduced time to first image when opening a flyer with the new rendering engine by re-writing manifest parsing
Reduced total memory usage by 5-10x by eagerly caching to disk and evicting off-screen images from memory
Improved scrolling responsiveness by moving most/all download/parsing/rendering-related actions to worker threads. This results in less "janky" scrolling when viewing very large flyers
Reduced bandwidth requirements by enabling HTTP Range Requests to allow partial downloads, instead of re-downloads
Reduced likelihood of temporarily blank screens by dynamically changing download/rendering priorities to prefer what should be currently on-screen, and deferring all off-screen images
These updates will facilitate some upcoming performance enhancements, namely pre-fetching. Pre-fetching will allow the SDK to optionally download and cache images before they are needed, resulting in a much smoother user experience and an even lower time to first image.
These improvements are the result of a significant re-write of the SDK's rendering engine, and as such, we are releasing this as a breaking version change.
Initializing the SDK
In v1.x, the SDK was initialized via a singleton and using two methods. In v2.x, interaction with the SDK occurs through an instance of DigitalFlyer
, and it's up to the consumer to manage the lifecycle of this object however fits their workflow.
Refer to DigitalFlyer
and Using the SDK
for more information.
// v1.x
TCDigitalFlyer(context).initialize("MY_SUBSCRIPTION_KEY")
TCSdkModel.setSdkParams(
"myBanner",
"myClient",
"staging",
"myStoreId",
"en",
Date()
)
// v2.x
val digitalFlyer = DigitalFlyer(
subscriptionKey = subscriptionKey,
client = client,
banner = banner,
environment = environment
)
Listing Available Publications
In v1.x, available Publications were retrieved via the getFlyerListingData
using static SDK parameters and helper methods. In lieu of this, v2.x provides a listPublications
method on the DigitalFlyer
object, which returns a list of Publication.Attributes
objects. Refer to Using the SDK
for details.
// v1.x
TCSdkModel.setStoreId("myStoreId")
TCSdkModel.setLanguage("en")
TCSdkModel.setStoreIdAndLanguage("myStoreId", "en")
val listener = object : TCSdkListener {
override fun onLandingResponse(landingDataResponse: LandingDataResponse) {
// landingDataResponse.flyers of type List<LandingData>
}
override fun onError(errorType: ErrorStatus, error: String) {
when (errorType) {
ErrorStatus.ERR_NO_INTERNET_CONNECTION -> TODO()
ErrorStatus.ERR_UNAUTHORIZED -> TODO()
ErrorStatus.ERR_API_CALL_FAIL -> TODO()
ErrorStatus.ERR_NO_DATA_FOUND -> TODO()
ErrorStatus.ERR_PARAM_VALIDATION -> TODO()
ErrorStatus.ERR_LOADER -> TODO()
}
}
}
TCDigitalFlyer(context).getFlyerListingData(TCSdkModel, listener)
// v2.x
val publications = digitalFlyer.listPublications(
storeId = storeId,
language = language,
date = date
)
Retrieving and Rendering a Publication
The v1.x SDK did not have an explicit method for retrieving a Publication. Instead, the TCDigitalFlyer(context).gotoFlyerDetailPage
created a view for the passed in listener and landingData
. The publication would be downloaded and rendered in one step (with a lot of boilerplate).
In lieu of this, v2.x provides an explicit getPublication
method on the DigitalFlyer
object, which returns a Publication
object. This object is passed downstream to the FlyerView
renderer. Refer to Using the SDK
for more details.
// v1.x
val productListener = object : TCSdkDetailListener {
override fun onProductClicked(sku: String) {
// The SKU of the clicked product
}
override fun onError(
errorType: ErrorStatus,
error: String
) {
when (errorType) {
ErrorStatus.ERR_NO_INTERNET_CONNECTION -> TODO()
ErrorStatus.ERR_UNAUTHORIZED -> TODO()
ErrorStatus.ERR_API_CALL_FAIL -> TODO()
ErrorStatus.ERR_NO_DATA_FOUND -> TODO()
ErrorStatus.ERR_PARAM_VALIDATION -> TODO()
ErrorStatus.ERR_LOADER -> TODO()
}
}
}
val uiComponents = TCSdkUIComponents()
uiComponents.apiLoaderColor = "#6200EE"
uiComponents.orientation = Orientation.VERTICAL
val flyerView = TCDigitalFlyer(context).gotoFlyerDetailPage(
productListener,
uiComponents,
landingData
)
// v2.x
val publication = digitalFlyer.getPublication(
attributes = attributes,
orientation = FlyerLayout.VERTICAL
)
// The publication can be rendered with 2 options: Compose view
...
FlyerView(publication) { event ->
when (event) {
is Event.Sku -> println("SKU: ${event.sku}") // This will be called when an event is emitted
else -> TODO("Handle other events")
}
}
...
// or, legacy XML view
...
val flyerView: FlyerView = findViewById(R.id.flyer_view)
flyerView.setActivePublication(publication) { event ->
when (event) {
is Event.Sku -> println("SKU: ${event.sku}") // This will be called when an event is emitted
else -> TODO("Handle other events")
}
}
...