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
-
Default Configuration (Balanced):
storiesView.setGifPosterEnabled(true)
storiesView.setVideoPosterEnabled(false) -
Performance-First Configuration:
storiesView.setGifPosterEnabled(false)
storiesView.setVideoPosterEnabled(false) -
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
Metric | Good | Acceptable | Poor |
---|---|---|---|
Load Time | < 1s | 1-3s | > 3s |
Memory Usage | < 50MB | 50-100MB | > 100MB |
Battery Impact | < 5% per hour | 5-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.