Skip to main content

Performance Optimization

This guide covers performance optimization techniques you can implement to ensure smooth StorifyMe integration in your Android application.

Poster Configuration​

Optimizing Poster Types​

The most impactful performance optimization you can control is managing poster types:

// ✅ Recommended for performance-sensitive apps
storiesView.setGifPosterEnabled(false) // Reduces memory usage significantly
storiesView.setVideoPosterEnabled(false) // Reduces memory and CPU usage

// ✅ For apps with good network and device performance
storiesView.setGifPosterEnabled(true) // Better user experience
storiesView.setVideoPosterEnabled(false) // Still conservative on resources

// ⚠️ Use with caution - highest resource usage
storiesView.setGifPosterEnabled(true)
storiesView.setVideoPosterEnabled(true) // Maximum visual appeal but highest cost

Performance Impact:

  • GIF Posters: 2-5x more memory usage per story
  • Video Posters: 3-8x more memory usage + CPU for decoding
  • Static Images: Baseline memory usage

Dynamic Poster Configuration​

class AdaptivePosterManager {
fun configurePostersBasedOnDevice(storiesView: StoriesView) {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memoryInfo)

val availableMemoryMB = memoryInfo.availMem / (1024 * 1024)
val totalMemoryMB = memoryInfo.totalMem / (1024 * 1024)

when {
totalMemoryMB < 2048 -> {
// Low-end device
storiesView.setGifPosterEnabled(false)
storiesView.setVideoPosterEnabled(false)
}
totalMemoryMB < 4096 -> {
// Mid-range device
storiesView.setGifPosterEnabled(true)
storiesView.setVideoPosterEnabled(false)
}
else -> {
// High-end device
storiesView.setGifPosterEnabled(true)
storiesView.setVideoPosterEnabled(true)
}
}
}
}

Loading Strategies​

Lazy Loading Pattern​

class LazyStoriesManager {
private var storiesView: StoriesView? = null
private var isLoaded = false

fun loadWhenVisible(view: StoriesView, rootView: View) {
storiesView = view

// Use ViewTreeObserver to detect when widget becomes visible
rootView.viewTreeObserver.addOnScrollChangedListener {
if (!isLoaded && isViewVisible(view, rootView)) {
loadStories()
}
}
}

private fun isViewVisible(view: View, rootView: View): Boolean {
val rect = Rect()
view.getGlobalVisibleRect(rect)

val rootRect = Rect()
rootView.getGlobalVisibleRect(rootRect)

return rect.intersect(rootRect) && rect.height() > view.height / 2
}

private fun loadStories() {
storiesView?.let { view ->
view.load()
isLoaded = true
}
}
}

// Usage in RecyclerView ViewHolder
class StoriesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val storiesView: StoriesView = itemView.findViewById(R.id.storiesView)
private val lazyManager = LazyStoriesManager()

fun bind(widgetId: Long, parentRecyclerView: RecyclerView) {
storiesView.widgetId = widgetId
lazyManager.loadWhenVisible(storiesView, parentRecyclerView)
}
}

Preloading Strategy​

class StoriesPreloader {
private val preloadedWidgets = mutableMapOf<Long, List<StoryWithSeen>>()

fun preloadWidget(widgetId: Long) {
if (preloadedWidgets.containsKey(widgetId)) return

// Create invisible view for preloading
val context = MyApplication.instance
val preloadView = StoriesView(context)

StorifyMe.instance.eventListener = object : StorifyMeEventListener() {
override fun onLoad(widgetId: Long, stories: List<StoryWithSeen>) {
preloadedWidgets[widgetId] = stories
// Clean up preload view
}

override fun onFail(widgetId: Long, error: String) {
Log.w("Preloader", "Failed to preload widget $widgetId: $error")
}
}

preloadView.widgetId = widgetId
preloadView.load()
}

fun isPreloaded(widgetId: Long): Boolean {
return preloadedWidgets.containsKey(widgetId)
}
}

Memory Management​

Widget Cleanup​

class StoriesMemoryManager {
companion object {
fun cleanupWidget(storiesView: StoriesView) {
// Clear any cached data if SDK provides cleanup method
try {
// storiesView.cleanup() // If available in future SDK versions

// Force garbage collection (use sparingly)
if (BuildConfig.DEBUG) {
System.gc()
}
} catch (e: Exception) {
Log.w("MemoryManager", "Error during cleanup", e)
}
}

fun optimizeForMemory(storiesView: StoriesView) {
storiesView.setGifPosterEnabled(false)
storiesView.setVideoPosterEnabled(false)
}
}
}

// In Activity/Fragment lifecycle
override fun onDestroy() {
StoriesMemoryManager.cleanupWidget(storiesView)
super.onDestroy()
}

override fun onLowMemory() {
super.onLowMemory()
// Switch to memory-optimized mode
StoriesMemoryManager.optimizeForMemory(storiesView)
}

Memory Monitoring​

class MemoryMonitor {
private val handler = Handler(Looper.getMainLooper())
private var monitoringRunnable: Runnable? = null

fun startMonitoring(callback: (MemoryStatus) -> Unit) {
monitoringRunnable = object : Runnable {
override fun run() {
val memoryStatus = getCurrentMemoryStatus()
callback(memoryStatus)

// Check every 5 seconds
handler.postDelayed(this, 5000)
}
}
handler.post(monitoringRunnable!!)
}

fun stopMonitoring() {
monitoringRunnable?.let { handler.removeCallbacks(it) }
}

private fun getCurrentMemoryStatus(): MemoryStatus {
val runtime = Runtime.getRuntime()
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
val maxMemory = runtime.maxMemory()
val availableMemory = maxMemory - usedMemory

val usagePercentage = (usedMemory.toFloat() / maxMemory.toFloat()) * 100

return MemoryStatus(
usedMemory = usedMemory,
availableMemory = availableMemory,
maxMemory = maxMemory,
usagePercentage = usagePercentage
)
}
}

data class MemoryStatus(
val usedMemory: Long,
val availableMemory: Long,
val maxMemory: Long,
val usagePercentage: Float
)

// Usage
class StoriesActivity : AppCompatActivity() {
private val memoryMonitor = MemoryMonitor()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

memoryMonitor.startMonitoring { status ->
if (status.usagePercentage > 80) {
// Switch to memory-optimized mode
optimizeForLowMemory()
}
}
}

private fun optimizeForLowMemory() {
storiesView.setGifPosterEnabled(false)
storiesView.setVideoPosterEnabled(false)
}

override fun onDestroy() {
memoryMonitor.stopMonitoring()
super.onDestroy()
}
}

Network Optimization​

Connection-Aware Loading​

class NetworkAwareStoriesManager(private val context: Context) {
private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

fun configureForCurrentNetwork(storiesView: StoriesView) {
val networkInfo = connectivityManager.activeNetworkInfo
val connectionType = getConnectionType()

when (connectionType) {
ConnectionType.WIFI -> {
// Full quality on Wi-Fi
storiesView.setGifPosterEnabled(true)
storiesView.setVideoPosterEnabled(true)
}
ConnectionType.MOBILE_HIGH_SPEED -> {
// Good quality on 4G/5G
storiesView.setGifPosterEnabled(true)
storiesView.setVideoPosterEnabled(false)
}
ConnectionType.MOBILE_LOW_SPEED -> {
// Conservative on 3G or slower
storiesView.setGifPosterEnabled(false)
storiesView.setVideoPosterEnabled(false)
}
ConnectionType.NO_CONNECTION -> {
// Handle offline state
handleOfflineMode(storiesView)
}
}
}

private fun getConnectionType(): ConnectionType {
val networkInfo = connectivityManager.activeNetworkInfo
?: return ConnectionType.NO_CONNECTION

return when (networkInfo.type) {
ConnectivityManager.TYPE_WIFI -> ConnectionType.WIFI
ConnectivityManager.TYPE_MOBILE -> {
when (networkInfo.subtype) {
TelephonyManager.NETWORK_TYPE_LTE,
TelephonyManager.NETWORK_TYPE_NR -> ConnectionType.MOBILE_HIGH_SPEED
else -> ConnectionType.MOBILE_LOW_SPEED
}
}
else -> ConnectionType.NO_CONNECTION
}
}

private fun handleOfflineMode(storiesView: StoriesView) {
// Show cached content or offline message
storiesView.visibility = View.GONE
// Show offline indicator
}
}

enum class ConnectionType {
WIFI, MOBILE_HIGH_SPEED, MOBILE_LOW_SPEED, NO_CONNECTION
}

RecyclerView Optimization​

Efficient ViewHolder Pattern​

class StoriesAdapter : RecyclerView.Adapter<StoriesAdapter.StoriesViewHolder>() {
private val items = mutableListOf<WidgetItem>()
private val viewHolderPool = mutableListOf<StoriesViewHolder>()

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StoriesViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_stories, parent, false)
return StoriesViewHolder(view)
}

override fun onBindViewHolder(holder: StoriesViewHolder, position: Int) {
holder.bind(items[position])
}

override fun onViewRecycled(holder: StoriesViewHolder) {
super.onViewRecycled(holder)
holder.cleanup()
viewHolderPool.add(holder)
}

class StoriesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val storiesView: StoriesView = itemView.findViewById(R.id.storiesView)
private var currentWidgetId: Long = 0

fun bind(item: WidgetItem) {
if (currentWidgetId != item.widgetId) {
cleanup()
currentWidgetId = item.widgetId

// Configure for performance
storiesView.setGifPosterEnabled(item.allowGifPosters)
storiesView.setVideoPosterEnabled(item.allowVideoPosters)

storiesView.widgetId = item.widgetId
storiesView.load()
}
}

fun cleanup() {
StoriesMemoryManager.cleanupWidget(storiesView)
currentWidgetId = 0
}
}
}

data class WidgetItem(
val widgetId: Long,
val allowGifPosters: Boolean = true,
val allowVideoPosters: Boolean = false
)

Performance Monitoring​

Performance Metrics Collection​

class StoriesPerformanceMonitor {
private var loadStartTime: Long = 0
private val performanceMetrics = mutableMapOf<String, Long>()

fun startLoadTimer() {
loadStartTime = System.currentTimeMillis()
}

fun recordLoadTime(widgetId: Long) {
val loadTime = System.currentTimeMillis() - loadStartTime
performanceMetrics["widget_${widgetId}_load_time"] = loadTime

// Log performance for monitoring
Log.d("Performance", "Widget $widgetId loaded in ${loadTime}ms")

// Send to analytics if load time is concerning
if (loadTime > 3000) {
// Track slow loading
trackSlowLoading(widgetId, loadTime)
}
}

fun recordMemoryUsage(widgetId: Long) {
val runtime = Runtime.getRuntime()
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
performanceMetrics["widget_${widgetId}_memory"] = usedMemory

Log.d("Performance", "Widget $widgetId using ${usedMemory / 1024 / 1024}MB")
}

private fun trackSlowLoading(widgetId: Long, loadTime: Long) {
// Send to your analytics service
// Firebase.analytics.logEvent("slow_widget_loading", bundleOf(
// "widget_id" to widgetId,
// "load_time" to loadTime
// ))
}
}

// Usage in event listener
StorifyMe.instance.eventListener = object : StorifyMeEventListener() {
private val performanceMonitor = StoriesPerformanceMonitor()

override fun onLoad(widgetId: Long, stories: List<StoryWithSeen>) {
performanceMonitor.recordLoadTime(widgetId)
performanceMonitor.recordMemoryUsage(widgetId)
}
}

Battery Optimization​

Power-Efficient Configuration​

class BatteryOptimizedStoriesManager(private val context: Context) {
private val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager

fun configureForBatteryMode(storiesView: StoriesView) {
val isInPowerSaveMode = powerManager.isPowerSaveMode

if (isInPowerSaveMode) {
// Aggressive battery saving
storiesView.setGifPosterEnabled(false)
storiesView.setVideoPosterEnabled(false)

// Disable auto-play if SDK supports it
// storiesView.setAutoPlayEnabled(false)
} else {
// Normal operation
storiesView.setGifPosterEnabled(true)
storiesView.setVideoPosterEnabled(false) // Still conservative
}
}

fun registerBatteryModeListener(callback: (Boolean) -> Unit) {
val filter = IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val isInPowerSaveMode = powerManager.isPowerSaveMode
callback(isInPowerSaveMode)
}
}
context.registerReceiver(receiver, filter)
}
}

Performance Best Practices Summary​

Configuration Recommendations​

  1. Default Configuration (Balanced):

    storiesView.setGifPosterEnabled(true)
    storiesView.setVideoPosterEnabled(false)
  2. Performance-First Configuration:

    storiesView.setGifPosterEnabled(false)
    storiesView.setVideoPosterEnabled(false)
  3. Visual-First Configuration (use carefully):

    storiesView.setGifPosterEnabled(true)
    storiesView.setVideoPosterEnabled(true)

Monitoring Checklist​

  • Monitor memory usage during development
  • Test on low-end devices (2GB RAM or less)
  • Measure widget loading times
  • Test with different network conditions
  • Monitor battery usage during extended use
  • Test with multiple widgets in single screen
  • Verify performance in RecyclerView scenarios

Key Performance Indicators​

MetricGoodAcceptablePoor
Load Time< 1s1-3s> 3s
Memory Usage< 50MB50-100MB> 100MB
Battery Impact< 5% per hour5-10% per hour> 10% per hour

By following these optimization strategies, you can ensure StorifyMe integrates smoothly into your app while maintaining excellent performance across all device types and network conditions.