The Thing We Didn’t Notice

About Forgotten One Design pattern

YE MON KYAW
Arpalar Tech
7 min readApr 8, 2023

--

These days I am reading a lot of tutorials and reading medium articles about Android development. Most explain new technologies and how we improve in terms of UI.

In our daily learning, new things are very important but also understanding basic things is as well, At this time we don’t care about memory management because our phone memory is efficient as a normal computer so we didn’t care much about memory consumption.

Even though our phone memory is efficient, we need to care about memory management, In my opinion, a good application does not depend on how much DAU is in the app, it depends on how much they care about detail optimization. So Today I will explain one of the important optimizations which is Memory allocation.

In Android development, there are several ways to optimize memory usage and avoid unnecessary object creation

  1. Object Pooling: Developers can implement their own object pooling mechanism, where a fixed number of objects are created and reused instead of creating new objects every time they are needed. This can help reduce memory usage and improve performance, especially for objects that are expensive to create.
  2. Lazy Initialization: Objects can be lazily initialized, meaning they are only created when they are actually needed. This can help avoid unnecessary object creation up front, especially for objects that may not be needed during the entire lifecycle of an application.
  3. Caching: Developers can cache objects in memory to reuse them instead of creating new objects with the same properties. This can be achieved using techniques such as using LRU (Least Recently Used) caches, in-memory databases, or other caching mechanisms provided by Android frameworks or third-party libraries.
  4. Object Reuse: Developers can design their objects to be reusable, by minimizing their state and keeping them stateless, so that they can be used by multiple components or activities without the need for creating new objects.
  5. Efficient Data Structures: Developers can use efficient data structures, such as sparse arrays or maps, to reduce the memory usage when storing and retrieving data, instead of using collections that may create unnecessary objects.
  6. Performance Optimization: Developers can optimize their code for performance by avoiding unnecessary object allocations and ensuring efficient memory management practices, such as avoiding excessive object creation in tight loops or performance-critical sections of the code.

I think we all used these ways in our application, so I will not mention the detail of the above ways. Today I will explain one of the design patterns which we have forgotten.

This Forgotten design pattern is The Flyweight design pattern. It is a creational method that is used to optimize memory usage by sharing common parts of objects among multiple objects, rather than duplicating them.

It is specifically designed to address scenarios where a large number of objects with similar properties need to be created and used in an application. The main idea behind the Flyweight pattern is to separate the intrinsic state (shared state) of an object from the extrinsic state (unique state) of an object.

The intrinsic state consists of properties that are common among multiple objects and can be shared, while the extrinsic state consists of properties that are unique to each object and cannot be shared.

he Flyweight pattern typically involves the following components:

  1. Flyweight: Represents the shared objects that are stored and reused among multiple objects. It contains the intrinsic state, which is shared among objects.
  2. ConcreteFlyweight: Implements the Flyweight interface and represents the concrete shared objects that are created and reused.
  3. FlyweightFactory: Manages the creation and caching of Flyweight objects. It maintains a pool of Flyweight objects and provides methods to retrieve and create Flyweight objects.
  4. Client: Represents the objects that use the Flyweight objects. It stores the extrinsic state, which is unique to each object, and passes it to the Flyweight objects when needed

Now coding time, let's see together how flyweight can be applied in our Android application.

// WeatherInfo.kt
data class WeatherInfo(
val location: String,
val weather: String,
val weatherIcon: String
)

// WeatherService.kt
interface WeatherService {
fun getCurrentLocation(): String
fun getCurrentWeather(location: String): String
fun getWeatherIcon(weather: String): String
}

// WeatherInfoFactory.kt
object WeatherInfoFactory {
private val weatherInfoCache = mutableMapOf<String, WeatherInfo>()

fun getWeatherInfo(location: String, weather: String): WeatherInfo {
val key = "$location-$weather"
if (weatherInfoCache.containsKey(key)) {
return weatherInfoCache[key]!!
} else {
val weatherIcon = getWeatherIcon(weather)
val weatherInfo = WeatherInfo(location, weather, weatherIcon)
weatherInfoCache[key] = weatherInfo
return weatherInfo
}
}

private fun getWeatherIcon(weather: String): String {
// Implementation for getting weather icon based on weather
}
}

// ViewModel.kt
class WeatherViewModel(
private val weatherService: WeatherService
) : ViewModel() {
private val weatherInfo = MutableLiveData<WeatherInfo>()
val weatherInfoUI: LiveData<WeatherInfo> get() = weatherInfo

fun fetchWeatherInfo() {
viewModelScope.launch {
try {
val location = weatherService.getCurrentLocation()
val weather = weatherService.getCurrentWeather(location)
val weatherInfo = WeatherInfoFactory.getWeatherInfo(location, weather)
weatherInfoUI.value = weatherInfo
} catch (e: Exception) {
// Handle error
}
}
}
}

// MainActivity.kt
class MainActivity : AppCompatActivity() {
private val viewModel: WeatherViewModel by viewModels()
private lateinit var weatherTextView: TextView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

weatherTextView = findViewById(R.id.weatherTextView)

viewModel.weatherInfoUI.observe(this, { weatherInfo ->
weatherTextView.text = "Location: ${weatherInfo.location}\n" +
"Weather: ${weatherInfo.weather}\n" +
"Icon: ${weatherInfo.weatherIcon}"
})

// Fetch weather info
viewModel.fetchWeatherInfo()
}
}

In this example, we use the Flyweight design pattern to reduce the memory usage when creating WeatherInfo objects. The WeatherInfoFactory acts as a factory for creating and caching WeatherInfo objects. It maintains a cache of WeatherInfo objects based on the location and weather and returns the cached object if it already exists, otherwise, it creates a new object, caches it and returns it.

By using the Flyweight design pattern, we can avoid creating multiple WeatherInfo objects with the same location and weather, which can help optimize memory usage when dealing with a large number of similar objects. This can be especially useful in scenarios where creating new objects is expensive in terms of memory and performance, and there is a need to reuse objects with similar properties to reduce memory overhead.

At this point don’t confuse them with Singleton Design patterns, The Flyweight and Singleton design patterns are both creational design methods that are used to manage objects in software applications, but they serve different purposes and have different implementations.

  1. Purpose: The Flyweight pattern is used to optimize memory usage by sharing common parts of objects among multiple objects, while the Singleton pattern is used to ensure that a class has only one instance and provides a global point of access to that instance.
  2. Object Sharing: In the Flyweight pattern, objects with similar properties are shared and reused to avoid creating multiple objects with the same properties, which helps reduce memory usage. In the Singleton pattern, a single instance of a class is created and shared among all objects that need to access it, ensuring that only one instance of the class exists throughout the application.
  3. Object Creation: In the Flyweight pattern, objects are typically created by a factory or a factory method, which can cache and reuse objects based on their properties. In the Singleton pattern, the single instance of the class is typically created when the class is first accessed, and subsequent requests for the instance return the same instance without creating a new one.
  4. Instance Management: In the Flyweight pattern, the responsibility of managing the shared objects and their caching is typically handled by a factory or a factory method. In the Singleton pattern, the responsibility of managing a single instance of the class is typically handled by the class itself.
  5. Multiple Instances: The Flyweight pattern allows for the creation of multiple instances of similar objects with shared properties, while the Singleton pattern restricts the creation of multiple instances to only one instance of the class.
  6. Scope: The Flyweight pattern is typically used in scenarios where there are a large number of objects with similar properties, and memory optimization is a concern. The Singleton pattern is typically used in scenarios where there should be only one instance of a class throughout the application, such as for managing configuration settings, logging, or database connections.

In summary, the Flyweight pattern is used to optimize memory usage by sharing common parts of objects, while the Singleton pattern is used to ensure that a class has only one instance throughout the application. Both patterns have different use cases, implementations, and implications in terms of object creation, sharing, and instance management. So you will think why this design pattern is not popular among developers.

Here are some reasons why the Flyweight pattern may not be as popular among developers:

  1. Complexity: Implementing the Flyweight pattern can introduce additional complexity in the form of managing the shared state, maintaining a pool of objects, and handling the extrinsic state. This can require careful design and implementation, which may not be preferred in simple or small-scale applications.
  2. Limited Use Cases: The Flyweight pattern is most useful in scenarios where there are a large number of objects with similar properties, and memory optimization is a significant concern. However, not all applications may have such requirements, and developers may not see the need to implement the Flyweight pattern in every situation.
  3. Performance Overhead: While the Flyweight pattern can optimize memory usage, it may introduce additional overhead in terms of managing the shared state and pool of objects. This can impact performance, especially in applications where performance is critical, and developers may choose other approaches that provide a better trade-off between memory usage and performance.
  4. Simpler Alternatives: In some cases, there may be simpler alternatives available to achieve similar memory optimization goals, such as object pooling, lazy initialization, caching, or other memory management techniques, which may be easier to implement and maintain compared to the Flyweight pattern.

The popularity of the pattern among developers may depend on the specific requirements and constraints of the application, the complexity of implementation, performance considerations, and the availability of simpler alternatives for achieving similar memory optimization goals. It’s important to note that the popularity of a design pattern may vary depending on the context, requirements, and preferences of the development team.

Whatever you optimize your application’s memory in terms of Flyweight pattern or Preference for other design patterns that is fine. The important thing is we have to be careful with our application and our code.

Thank you for taking the time to read this article. I hope that my writing has been informative and thought-provoking.

--

--

YE MON KYAW
Arpalar Tech

Software Engineer who write code in Kotlin / Android