Skip to main content

Banners place

Starting with version 0.6.0, banner display functionality has been added to the SDK.

Setting banners place

Important

Do not use Padding (or similar) widget with BannerPlace widget, this may cause visual bugs (e.g. banner cropping)

Banners can be added with BannerPlace widget:

class BannersPage extends StatelessWidget {
const BannersPage({super.key});


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const Column(
children: [
BannerPlace(placeId: "placeId", height: 120),
],
),
);
}
}

Load banners

Banners are loaded automatically, but if you need to load them for some event, set the autoLoad field to false and then call method from BannerPlaceManager to load banners:

await BannerPlaceManager.instance.load("placeId");
Full manual loading example
class BannersPage extends StatelessWidget {
const BannersPage({super.key});


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: [
const BannerPlace(
placeId: "placeId",
height: 120,
// setting auto load to false
autoLoad: false,
),
ElevatedButton(
onPressed: () async {
// Load banners
await BannerPlaceManager.instance.load("placeId");
},
child: const Text('Load'),
),
],
),
);
}
}

Additionally, you can preload banners:

Future<void> preloadBanners(String placeId) async {
await BannerPlaceManager.instance.preload(placeId);
}

Reload banners

To forcefully reload banners from the server for a specific place (for instance, during a manual refresh or when returning to a screen), use the reload method from BannerPlaceManager:

Future<void> reloadBanners(String placeId) async {
await BannerPlaceManager.instance.reload(placeId);
}
Pull-to-refresh example with RefreshIndicator
class BannersPullToRefreshPage extends StatelessWidget {
const BannersPullToRefreshPage({super.key});

final String _placeId = "placeId";


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Banners Pull-to-Refresh')),
body: RefreshIndicator(
onRefresh: () async {
await BannerPlaceManager.instance.reload(_placeId);
},
child: ListView(
children: [
BannerPlace(placeId: _placeId, height: 120),
],
),
),
);
}
}

To control the display and movement of banners, you can use the methods of scrolling to the nearest ones or scrolling to the specific index:

Future<void> scrollToNext(String placeId) async {
await BannerPlaceManager.instance.showNext(placeId);
}

Future<void> scrollToPrevious(String placeId) async {
await BannerPlaceManager.instance.showPrevious(placeId);
}

Future<void> scrollToIndex(String placeId, int index) async {
await BannerPlaceManager.instance.showByIndex(placeId: placeId, index: index);
}

Similarly, there are banner playback control methods that control the internal page turn timer:

  • pauseAutoscroll(placeId) - stops the internal timer
  • resumeAutoscroll(placeId) - resumes the logic of the internal timers
Future<void> pauseAutoscroll(String placeId) async {
await BannerPlaceManager.instance.pauseAutoscroll(placeId);
}

Future<void> resumeAutoscroll(String placeId) async {
await BannerPlaceManager.instance.resumeAutoscroll(placeId);
}

Touch interaction

Starting with version 0.9.2, you can control touch interactions for BannerPlace.

Via BannerPlace widget

Use the isInteractionEnabled parameter to enable or disable touch events for the banner place widget:

BannerPlace(
placeId: "placeId",
height: 120,
isInteractionEnabled: false, // Disables touch interactions
)
info

By default, BannerPlace automatically tracks active ModalRoutes (such as bottom sheets or dialogs). When a modal route is displayed on top of the screen, touch interaction with the native banner view is automatically disabled to prevent it from intercepting gestures, and restored once the modal route is closed.

Via BannerPlaceManager

You can also programmatically control touch interaction for a specific banner place ID:

Future<void> setBannerInteraction(String placeId, bool enabled) async {
await BannerPlaceManager.instance.setInteraction(
placeId: placeId,
isInteractionEnabled: enabled,
);
}

Customization

BannerPlace widget can be customized with bannerDecoration (used when banner is loading) and placeDecoration (used for customizing banner place):

BannerPlace(
placeId: "placeId",
height: 120,
bannerDecoration: BannerDecoration(
color: Colors.indigo, // color of background
image: 'assets/icons/icon.png', // image asset
),
placeDecoration: BannerPlaceDecoration(
bannerOffset: 16, // default = 0.0
bannersGap: 20, // default = 8.0
cornerRadius: 16, // default = 16.0
loop: true, // default = true
),
)
Full appearance customization example
class BannersPage extends StatelessWidget {
const BannersPage({super.key});


Widget build(BuildContext context) {
final bannerPlaceholder = BannerDecoration(
color: Colors.indigo,
image: 'assets/icons/icon.png',
);

final bannerPlaceDecoration = BannerPlaceDecoration(
bannerOffset: 16,
bannersGap: 20,
cornerRadius: 16,
loop: true,
);

return Scaffold(
appBar: AppBar(),
body: Column(
children: [
BannerPlace(
placeId: "placeId",
height: 120,
bannerDecoration: bannerPlaceholder,
placeDecoration: bannerPlaceDecoration,
),
],
),
);
}
}

You can display a loader placeholder by implementing bannerPlaceLoaderBuilder with BannerPlacePlaceholder or with your own widget:

BannerPlace(
placeId: "placeId",
height: 120,
bannerPlaceLoaderBuilder: (context) {
return const BannerPlacePlaceholder();
},
)
Full placeholder example
class BannersPage extends StatelessWidget {
const BannersPage({super.key});


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: [
BannerPlace(
placeId: "placeId",
height: 120,
bannerPlaceLoaderBuilder: (context) {
return const BannerPlacePlaceholder();
},
),
],
),
);
}
}

Dynamic height

By default, BannerPlace requires a fixed initial height. However, the banner content configured on the server might have a different aspect ratio or height.

When banners are loaded from the server, the SDK calculates the required height and returns it via the onBannerPlaceLoaded(int size, int widgetHeight) callback. You can update the widget height dynamically and wrap BannerPlace with AnimatedSize for a smooth transition:

AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: BannerPlace(
placeId: _placeId,
height: _bannerPlaceHeight,
onBannerPlaceLoaded: (size, widgetHeight) {
if (widgetHeight > 0 && widgetHeight.toDouble() != _bannerPlaceHeight) {
setState(() {
_bannerPlaceHeight = widgetHeight.toDouble();
});
}
},
),
)
Full dynamic height example
class DynamicHeightBannerPage extends StatefulWidget {
const DynamicHeightBannerPage({super.key});


State<DynamicHeightBannerPage> createState() => _DynamicHeightBannerPageState();
}

class _DynamicHeightBannerPageState extends State<DynamicHeightBannerPage> {
// Initial approximate height before banners are loaded
double _bannerPlaceHeight = 120;
final String _placeId = "placeId";


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Dynamic Height Banner')),
body: Column(
children: [
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: BannerPlace(
placeId: _placeId,
height: _bannerPlaceHeight,
onBannerPlaceLoaded: (size, widgetHeight) {
if (widgetHeight > 0 && widgetHeight.toDouble() != _bannerPlaceHeight) {
setState(() {
_bannerPlaceHeight = widgetHeight.toDouble();
});
}
},
),
),
],
),
);
}
}

Hiding empty banner place

If there are no banners available for the user (for example, if all banners are inactive, expired, or filtered out by targeting), the onBannerPlaceLoaded callback is triggered with size == 0.

To prevent displaying an empty area or placeholder on the screen, collapse the height to 0 when size == 0:

ClipRect(
child: AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: SizedBox(
height: _bannerPlaceHeight,
child: BannerPlace(
placeId: _placeId,
height: _bannerPlaceHeight > 0 ? _bannerPlaceHeight : 120,
onBannerPlaceLoaded: (size, widgetHeight) {
setState(() {
_bannerPlaceHeight = size > 0 ? widgetHeight.toDouble() : 0.0;
});
},
),
),
),
)
Important

Do not remove BannerPlace from the widget tree (for example, using if (hasBanners)).

If BannerPlace is unmounted from the tree, its internal state and event listeners are disposed. As a result, subsequent calls to BannerPlaceManager.instance.reload(placeId) (such as during Pull-to-Refresh or when returning to a screen) will not be able to trigger onBannerPlaceLoaded or bring the widget back.

By keeping BannerPlace mounted and collapsing its height to 0 via SizedBox and AnimatedSize, the widget takes up no visual space while remaining active to receive updates on reload.

Full empty state & dynamic height example with reload
class BannerPlacePage extends StatefulWidget {
const BannerPlacePage({super.key});


State<BannerPlacePage> createState() => _BannerPlacePageState();
}

class _BannerPlacePageState extends State<BannerPlacePage> {
// Initial height for placeholder/loading state
double _bannerPlaceHeight = 120;
final String _placeId = "placeId";


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Banners Example')),
body: RefreshIndicator(
onRefresh: () async {
// If fresh banners become available on reload, onBannerPlaceLoaded
// will fire and smoothly expand the widget back
await BannerPlaceManager.instance.reload(_placeId);
},
child: ListView(
children: [
ClipRect(
child: AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: SizedBox(
height: _bannerPlaceHeight,
child: BannerPlace(
placeId: _placeId,
height: _bannerPlaceHeight > 0 ? _bannerPlaceHeight : 120,
onBannerPlaceLoaded: (size, widgetHeight) {
setState(() {
_bannerPlaceHeight = size > 0 ? widgetHeight.toDouble() : 0.0;
});
},
),
),
),
),
],
),
),
);
}
}

Page indicator

The InAppStory SDK does not provide a built-in page indicator widget, but you can easily implement one in your application. Below is an example of creating a page indicator using the popular third-party smooth_page_indicator package.

Because BannerPlace handles scrolling internally, you can use AnimatedSmoothIndicator, which works with explicit activeIndex and count parameters rather than requiring a Flutter PageController:

  • Store the total number of banners using the onBannerPlaceLoaded(int size, int widgetHeight) callback.
  • Track the active banner index using the onBannerScroll(int index) callback.
  • Optionally handle dot clicks via onDotClicked with BannerPlaceManager.instance.showByIndex(placeId: placeId, index: index).
Full page indicator example with smooth_page_indicator
import 'package:flutter/material.dart';
import 'package:inappstory_plugin/inappstory_plugin.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';

class BannerWithIndicatorPage extends StatefulWidget {
const BannerWithIndicatorPage({super.key});


State<BannerWithIndicatorPage> createState() => _BannerWithIndicatorPageState();
}

class _BannerWithIndicatorPageState extends State<BannerWithIndicatorPage> {
final String _placeId = "placeId";
int _bannerCount = 0;
int _activeIndex = 0;


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Banner with Indicator')),
body: Column(
children: [
BannerPlace(
placeId: _placeId,
height: 120,
onBannerPlaceLoaded: (size, widgetHeight) {
setState(() {
_bannerCount = size;
});
},
onBannerScroll: (index) {
setState(() {
_activeIndex = index;
});
},
),
if (_bannerCount > 1)
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: AnimatedSmoothIndicator(
activeIndex: _activeIndex,
count: _bannerCount,
effect: const WormEffect(
dotHeight: 8,
dotWidth: 8,
spacing: 8,
activeDotColor: Colors.blue,
dotColor: Colors.grey,
),
onDotClicked: (index) {
BannerPlaceManager.instance.showByIndex(
placeId: _placeId,
index: index,
);
},
),
),
],
),
);
}
}

Events

BannerPlace has callback functions to listen for events from the widget:

  • onBannerPlacePreloaded() - triggered after banners are preloaded
  • onBannerPlacePreloadedError() - triggered when banners preload failed
  • onActionWith(BannerData bannerData, String widgetEventName, Map<String, Object?>? widgetData) - triggered after clicking on widgets, contains in banner
    • BannerData - containing a brief description of the selected banner;
    • widgetEventName - name of widget;
    • widgetData - activated widget data, detailed data fields;
  • onBannerPlaceLoaded(int size, int widgetHeight) - triggered after banners are loaded:
    • size - number of loaded banners;
    • widgetHeight - height of the banner widget calculated by the SDK based on banner content;
  • onBannerScroll(int index) - triggered when BannerPlace changing banner
tip

For detailed examples of dynamically adjusting widget height, hiding the banner place when size == 0, or adding a page indicator, see Dynamic height, Hiding empty banner place, and Page indicator.

Events handling example
class BannersPage extends StatefulWidget {
const BannersPage({super.key});


State<BannersPage> createState() => _BannersPageState();
}

class _BannersPageState extends State<BannersPage> {
final _placeId = "placeId";


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: [
BannerPlace(
placeId: _placeId,
height: 120,
onBannerScroll: (index) {
// Do anything related
},
onActionWith: (bannerData, widgetEventName, widgetData) {
// Do anything related
},
onBannerPlaceLoaded: (size, widgetHeight) {
// Do anything related
},
),
],
),
);
}
}

Objects

class BannerData {
String? id;
String? bannerPlace;
String? payload;
}