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.