Creational design patterns are a set of solutions to common software development problems that deal with how objects are being created. Using such patterns will ensure that your code is flexible and reusable, and that you avoid hard-coded dependencies and tight coupling between classes.
In this blog post, I will show you some of the most important and widely used creational design patterns in Kotlin Android. You will learn how to apply these patterns to your projects and how they can help you write better and more maintainable code. I will cover four creational design patterns:
- Factory and abstract factory (provider model) method
- Singleton
- Builder
- Dependency injection
Factory and abstract factory (provider model) method
The factory method pattern defines an interface for creating an object, but lets subclasses decide which class to instantiate. The abstract factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
These patterns are useful when you want to decouple the creation of objects from their usage, or when you want to provide different implementations of the same interface depending on some conditions. For example, you can use these patterns to create different types of views or fragments based on the device configuration or user preferences.
In Kotlin, you can use the provider model to implement these patterns. The provider model is a way of creating objects using lambda expressions or function references that act as factories. For example, you can use a provider function to create different types of fragments based on a parameter:
// An interface for fragments that display some content interface ContentFragment { fun showContent(content: String) } // A concrete implementation of ContentFragment that shows content in a text view class TextFragment : Fragment(), ContentFragment { private lateinit var textView: TextView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.text_fragment, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) textView = view.findViewById(R.id.text_view) } override fun showContent(content: String) { textView.text = content } } // Another concrete implementation of ContentFragment that shows content in a web view class WebFragment : Fragment(), ContentFragment { private lateinit var webView: WebView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.web_fragment, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) webView = view.findViewById(R.id.web_view) } override fun showContent(content: String) { webView.loadUrl(content) } } // A provider function that returns a ContentFragment based on a parameter fun provideContentFragment(type: String): ContentFragment = when (type) { "text" -> TextFragment() "web" -> WebFragment() else -> throw IllegalArgumentException("Unknown type: $type") }
Singleton
The singleton pattern ensures that a class has only one instance and provides a global point of access to it. This pattern is useful when you want to have a single source of truth for some data or functionality in your app. For example, you can use this pattern to create a repository that handles data access from different sources.
In Kotlin, you can use the object declaration to create a singleton class. The object declaration combines a class declaration and a single instance of that class into one expression. For example, you can use an object declaration to create a news repository that fetches data from a remote data source:
// A singleton class that acts as a repository for news data object NewsRepository { // A reference to the remote data source private val newsRemoteDataSource = // getDataSource() // A flow that emits the latest news from the remote data source val latestNews: Flow<List<ArticleHeadline>> = newsRemoteDataSource.latestNews // A function that returns the details of an article by its id suspend fun getArticleDetails(id: String): ArticleDetails { return newsRemoteDataSource.getArticleDetails(id) } }
Builder
The builder pattern separates the construction of a complex object from its representation so that the same construction process can create different representations. This pattern is useful when you want to create objects with many optional parameters or when you want to have more control over how the object is constructed. For example, you can use this pattern to create an alert dialog with various options.
In Kotlin, you can use named arguments and default values to implement this pattern. Named arguments allow you to specify the name of a parameter when calling a function, which makes the code more readable and avoids errors when there are many parameters. Default values allow you to omit some parameters when calling a function if they have a predefined value. For example, you can use named arguments and default values to create an alert dialog builder class:
// A class that represents an alert dialog with various options class AlertDialog( val title: String, val message: String, val positiveButton: String = "OK", val negativeButton: String? = null, val icon: Int? = null, val onPositiveClick: () -> Unit = {}, val onNegativeClick: () -> Unit = {} ) { // A function that shows the alert dialog on the screen fun show() { // Create and display an alert dialog using the Android SDK ... } } // A builder class that creates an AlertDialog instance using named arguments and default values class AlertDialogBuilder { // A function that returns an AlertDialog instance with the given parameters fun build( title: String, message: String, positiveButton: String = "OK", negativeButton: String? = null, icon: Int? = null, onPositiveClick: () -> Unit = {}, onNegativeClick: () -> Unit = {} ): AlertDialog { return AlertDialog( title = title, message = message, positiveButton = positiveButton, negativeButton = negativeButton, icon = icon, onPositiveClick = onPositiveClick, onNegativeClick = onNegativeClick ) } }
Dependency injection
The dependency injection pattern is a technique whereby one object supplies the dependencies of another object. A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it.
This pattern is useful when you want to reduce coupling and increase testability between classes by delegating the responsibility of creating and providing dependencies to another object or framework. For example, you can use this pattern to inject dependencies into your activities or view models.
In Kotlin Android, you can use frameworks like Dagger or Koin to implement this pattern. These frameworks provide annotations or DSLs to define dependencies and inject them into your classes. For example, you can use Koin to inject dependencies into your view model:
// A class that represents a user profile view model with some dependencies class UserProfileViewModel( private val userRepository: UserRepository, private val analyticsService: AnalyticsService ) : ViewModel() { // Some view model logic using userRepository and analyticsService ... } // A module that defines dependencies using Koin DSL val appModule = module { // Define UserRepository as a singleton using factory function single<UserRepository> { UserRepositoryImpl(get()) } // Define AnalyticsService as a singleton using constructor injection single<AnalyticsService> { AnalyticsServiceImpl() } // Define UserProfileViewModel using constructor injection viewModel { UserProfileViewModel(get(), get()) } } // Start Koin with appModule in Application class class MyApp : Application() { override fun onCreate() { super.onCreate() startKoin { androidContext(this@MyApp) modules(appModule) } } } // Get UserProfileViewModel instance using Koin extension function in Activity class class UserProfileActivity : AppCompatActivity() { // Inject UserProfileViewModel private val viewModel by viewModel<UserProfileViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_user_profile) // Use viewModel ... } }
Conclusion
In this blog post, you learned how to use creational design patterns in Kotlin Android. You learned how to apply these patterns to your projects and how they can help you write better and more maintainable code. You learned how to use factory and abstract factory (provider model) method, singleton, builder, and dependency injection patterns.
If you want to learn more about design patterns and other Kotlin features for Android development, check out these resources:
- Design Patterns for Dummies
- Mastering Design Patterns in Android with Kotlin
- Kotlin flows on Android
- Dependency injection with Koin
I hope you enjoyed this blog post and found it useful. Happy coding! π