Skip to content

Memory-Efficient UI Development ​

Introduction to Memory Management in Mobile UI ​

Understanding Memory Constraints ​

Mobile devices have limited memory resources compared to desktop systems. Efficient memory management is crucial for maintaining smooth performance and preventing application crashes due to out-of-memory errors.

Quick Decision ​

SituationApproachWatch Out
Large imagesResize/cache limitOOM risk
Screen lifecycle objectsDispose/cancelRetained callbacks create leaks
Large list dataPaging/windowingDo not keep all items in memory
State growsNormalize/clear stateStale state should be cleaned

Production Checklist ​

  • Problem: After which flow does memory not drop back?
  • Solution: Are ownership, cache limit, image policy, lifecycle cleanup, and leak detection clear?
  • Trade-off: More cache improves speed; memory pressure creates crashes.
  • Failure mode: Activity leaks, retained closures, bitmap OOM, unbounded lists, and listener leaks should be handled.
  • Measurement: Track memory peak, retained objects, OOM rate, GC pressure, and session length.
  • Security/cost: Objects carrying sensitive data should be cleared; low-RAM device tests are required.

Memory Management Fundamentals ​

  • Heap Memory: Object allocation and deallocation
  • Stack Memory: Local variables and method calls
  • Memory Leaks: Objects that cannot be garbage collected
  • Memory Pressure: System state when available memory is low

Memory Leak Prevention ​

Common Memory Leak Patterns ​

Android Memory Leak Prevention ​

kotlin
// Avoid static references to Activities
class MemoryLeakPreventor {
    // BAD: Static reference to Activity
    // companion object {
    //     var activity: Activity? = null
    // }
    
    // GOOD: Use WeakReference for Activity references
    companion object {
        private var activityRef: WeakReference<Activity>? = null
        
        fun setActivity(activity: Activity) {
            activityRef = WeakReference(activity)
        }
        
        fun getActivity(): Activity? = activityRef?.get()
    }
}

// Proper listener cleanup
class ViewWithListener : View {
    private var listener: (() -> Unit)? = null
    
    fun setListener(listener: () -> Unit) {
        this.listener = listener
    }
    
    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        listener = null // Prevent memory leak
    }
}

iOS Memory Management ​

swift
// Use weak references to prevent retain cycles
class ViewController: UIViewController {
    weak var delegate: ViewControllerDelegate?
    private var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTimer()
    }
    
    private func setupTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            self?.updateUI()
        }
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        timer?.invalidate()
        timer = nil
    }
    
    private func updateUI() {
        // Update UI safely
    }
}

// Protocol with weak delegate pattern
protocol ViewControllerDelegate: AnyObject {
    func didUpdateData()
}

Automatic Memory Management ​

Flutter Memory Management ​

dart
// Proper disposal of controllers and streams
class MemoryEfficientWidget extends StatefulWidget {
  @override
  _MemoryEfficientWidgetState createState() => _MemoryEfficientWidgetState();
}

class _MemoryEfficientWidgetState extends State<MemoryEfficientWidget> {
  late StreamController<String> _controller;
  late ScrollController _scrollController;
  StreamSubscription<String>? _subscription;
  
  @override
  void initState() {
    super.initState();
    _controller = StreamController<String>();
    _scrollController = ScrollController();
    
    _subscription = _controller.stream.listen((data) {
      // Handle data
    });
  }
  
  @override
  void dispose() {
    // Critical: Dispose all resources
    _subscription?.cancel();
    _controller.close();
    _scrollController.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemBuilder: (context, index) {
        return ListTile(title: Text('Item $index'));
      },
    );
  }
}

Object Pooling and Recycling ​

View Recycling Patterns ​

Android RecyclerView Optimization ​

kotlin
class EfficientAdapter(private val items: List<Item>) : 
    RecyclerView.Adapter<EfficientAdapter.ViewHolder>() {
    
    // Object pool for expensive operations
    private val textPaintPool = Pools.SimplePool<Paint>(10)
    
    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val titleView: TextView = itemView.findViewById(R.id.title)
        val imageView: ImageView = itemView.findViewById(R.id.image)
        
        fun bind(item: Item) {
            titleView.text = item.title
            
            // Use efficient image loading
            Glide.with(itemView.context)
                .load(item.imageUrl)
                .placeholder(R.drawable.placeholder)
                .into(imageView)
        }
        
        fun recycle() {
            // Clear resources when view is recycled
            imageView.setImageDrawable(null)
            titleView.text = null
        }
    }
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_layout, parent, false)
        return ViewHolder(view)
    }
    
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(items[position])
    }
    
    override fun onViewRecycled(holder: ViewHolder) {
        super.onViewRecycled(holder)
        holder.recycle()
    }
    
    override fun getItemCount(): Int = items.size
}

iOS Cell Reuse Optimization ​

swift
class EfficientTableViewCell: UITableViewCell {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var subtitleLabel: UILabel!
    @IBOutlet weak var thumbnailImageView: UIImageView!
    
    override func prepareForReuse() {
        super.prepareForReuse()
        
        // Reset cell state
        titleLabel.text = nil
        subtitleLabel.text = nil
        thumbnailImageView.image = nil
        
        // Cancel any ongoing image downloads
        thumbnailImageView.sd_cancelCurrentImageLoad()
    }
    
    func configure(with item: Item) {
        titleLabel.text = item.title
        subtitleLabel.text = item.subtitle
        
        // Use SDWebImage for efficient image caching
        thumbnailImageView.sd_setImage(
            with: URL(string: item.imageURL),
            placeholderImage: UIImage(named: "placeholder")
        )
    }
}

Custom Object Pools ​

Generic Object Pool Implementation ​

kotlin
class ObjectPool<T>(
    private val factory: () -> T,
    private val reset: (T) -> Unit,
    maxSize: Int = 10
) {
    private val pool = LinkedList<T>()
    private val maxPoolSize = maxSize
    
    fun acquire(): T {
        return if (pool.isNotEmpty()) {
            pool.removeFirst()
        } else {
            factory()
        }
    }
    
    fun release(obj: T) {
        if (pool.size < maxPoolSize) {
            reset(obj)
            pool.addLast(obj)
        }
    }
}

// Usage example
class ExpensiveObject {
    var data: String = ""
    
    fun reset() {
        data = ""
    }
}

class PoolManager {
    private val expensiveObjectPool = ObjectPool(
        factory = { ExpensiveObject() },
        reset = { it.reset() }
    )
    
    fun useExpensiveObject(): String {
        val obj = expensiveObjectPool.acquire()
        try {
            obj.data = "Process some data"
            return obj.data
        } finally {
            expensiveObjectPool.release(obj)
        }
    }
}

Virtual Lists and Lazy Loading ​

Virtual Scrolling Implementation ​

React Native Virtual List ​

javascript
import React, { useMemo, useCallback } from 'react';
import { VirtualizedList, View, Text } from 'react-native';

const VirtualList = ({ data }) => {
  const getItem = useCallback((data, index) => data[index], []);
  const getItemCount = useCallback((data) => data.length, []);
  
  const renderItem = useCallback(({ item, index }) => (
    <View style={{ height: 50, justifyContent: 'center', padding: 10 }}>
      <Text>{item.title}</Text>
    </View>
  ), []);
  
  const keyExtractor = useCallback((item, index) => item.id || index.toString(), []);
  
  return (
    <VirtualizedList
      data={data}
      initialNumToRender={10}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
      getItemCount={getItemCount}
      getItem={getItem}
      maxToRenderPerBatch={5}
      windowSize={10}
      removeClippedSubviews={true}
    />
  );
};

Flutter Lazy Loading ​

dart
class LazyListView extends StatefulWidget {
  final List<Item> items;
  
  const LazyListView({Key? key, required this.items}) : super(key: key);
  
  @override
  _LazyListViewState createState() => _LazyListViewState();
}

class _LazyListViewState extends State<LazyListView> {
  final ScrollController _scrollController = ScrollController();
  final Set<int> _loadedItems = {};
  
  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
  }
  
  void _onScroll() {
    final double scrollOffset = _scrollController.offset;
    final double maxScrollExtent = _scrollController.position.maxScrollExtent;
    
    // Preload items near the viewport
    if (scrollOffset > maxScrollExtent * 0.8) {
      _preloadItems();
    }
  }
  
  void _preloadItems() {
    // Implement preloading logic
  }
  
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemCount: widget.items.length,
      itemBuilder: (context, index) {
        return LazyLoadItem(
          item: widget.items[index],
          isLoaded: _loadedItems.contains(index),
          onLoad: () => _loadedItems.add(index),
        );
      },
    );
  }
  
  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }
}

class LazyLoadItem extends StatelessWidget {
  final Item item;
  final bool isLoaded;
  final VoidCallback onLoad;
  
  const LazyLoadItem({
    Key? key,
    required this.item,
    required this.isLoaded,
    required this.onLoad,
  }) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    if (!isLoaded) {
      WidgetsBinding.instance.addPostFrameCallback((_) => onLoad());
      return Container(
        height: 60,
        child: Center(child: CircularProgressIndicator()),
      );
    }
    
    return ListTile(
      title: Text(item.title),
      subtitle: Text(item.subtitle),
    );
  }
}

Memory Profiling and Optimization ​

Memory Monitoring Tools ​

Android Memory Profiling ​

kotlin
class MemoryProfiler {
    fun logMemoryUsage() {
        val runtime = Runtime.getRuntime()
        val usedMemory = runtime.totalMemory() - runtime.freeMemory()
        val maxMemory = runtime.maxMemory()
        val availableMemory = maxMemory - usedMemory
        
        Log.d("Memory", "Used: ${usedMemory / 1024 / 1024}MB")
        Log.d("Memory", "Max: ${maxMemory / 1024 / 1024}MB")
        Log.d("Memory", "Available: ${availableMemory / 1024 / 1024}MB")
    }
    
    fun forceGarbageCollection() {
        System.gc()
        logMemoryUsage()
    }
}

// Memory leak detection with LeakCanary integration
class Application : Application() {
    override fun onCreate() {
        super.onCreate()
        
        if (BuildConfig.DEBUG) {
            if (LeakCanary.isInAnalyzerProcess(this)) {
                return
            }
            LeakCanary.install(this)
        }
    }
}

iOS Memory Tracking ​

swift
import os.log

class MemoryTracker {
    private let logger = OSLog(subsystem: "com.app.memory", category: "tracking")
    
    func logMemoryUsage() {
        let info = mach_task_basic_info()
        var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4
        
        let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
            $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
                task_info(mach_task_self_,
                         task_flavor_t(MACH_TASK_BASIC_INFO),
                         $0,
                         &count)
            }
        }
        
        if kerr == KERN_SUCCESS {
            let memoryUsage = info.resident_size / 1024 / 1024
            os_log("Memory usage: %{public}d MB", log: logger, type: .info, memoryUsage)
        }
    }
}

Best Practices for Memory Efficiency ​

Image Memory Management ​

kotlin
// Android - Efficient image handling
class ImageManager {
    companion object {
        fun loadImageEfficiently(
            imageView: ImageView,
            imagePath: String,
            targetWidth: Int,
            targetHeight: Int
        ) {
            // Calculate inSampleSize for memory efficiency
            val options = BitmapFactory.Options().apply {
                inJustDecodeBounds = true
            }
            
            BitmapFactory.decodeFile(imagePath, options)
            
            options.inSampleSize = calculateInSampleSize(options, targetWidth, targetHeight)
            options.inJustDecodeBounds = false
            
            val bitmap = BitmapFactory.decodeFile(imagePath, options)
            imageView.setImageBitmap(bitmap)
        }
        
        private fun calculateInSampleSize(
            options: BitmapFactory.Options,
            reqWidth: Int,
            reqHeight: Int
        ): Int {
            val height = options.outHeight
            val width = options.outWidth
            var inSampleSize = 1
            
            if (height > reqHeight || width > reqWidth) {
                val halfHeight = height / 2
                val halfWidth = width / 2
                
                while ((halfHeight / inSampleSize) >= reqHeight &&
                       (halfWidth / inSampleSize) >= reqWidth) {
                    inSampleSize *= 2
                }
            }
            
            return inSampleSize
        }
    }
}

This comprehensive guide provides practical strategies for memory-efficient UI development across all major mobile platforms, focusing on leak prevention, object pooling, virtual lists, and performance monitoring.

Created by Eren Demir.