47 |
48 | A factory design pattern is a generative design pattern that helps to abstract how an object is
49 | created. This makes your code more flexible and extensible.
50 |
51 | The basic idea of the factory design pattern is to delegate object creation to a factory class. This
52 | factory class determines which object is created.
53 |
54 |
The factory design pattern has two main components
55 |
56 | - **Product:** The object to be created.
57 | - **Factory:** The class that creates the product object.
58 |
59 |
Advantages of factory design pattern:
60 |
61 | - Makes your code more flexible and extensible.
62 | - It makes your code more readable and understandable by abstracting the object creation process.
63 | - It makes the software development process more efficient.
64 |
65 |
Disadvantages of the factory design pattern:
66 |
67 | - It may be difficult to use in complex applications.
68 | - It may cause you to write more code.
69 |
70 | **Sample Scenario**
71 |
72 | Here's a real-world example of the Factory Design Pattern in Jetpack Compose, focusing on a scenario
73 | where you want to implement different card layouts for displaying various types of content in a news
74 | application:
75 |
76 | Scenario: You have a news app where each news item can be displayed in several formats like '
77 | simple' (only text), 'rich' (with image), or 'interactive' (includes interactive elements like a
78 | poll).
79 |
80 | ### 1. Define an Interface for Card Factories:
81 |
82 | ```kotlin
83 | interface NewsCardFactory {
84 | @Composable
85 | fun CreateCard(newsItem: NewsItem)
86 | }
87 |
88 | data class NewsItem(val title: String, val content: String, val imageUrl: String?)
89 | ```
90 |
91 | ### 2. Implement Concrete Factories for Each Card Type:
92 |
93 | - Simple Card Factory:
94 |
95 | ```kotlin
96 | class SimpleCardFactory : NewsCardFactory {
97 | @Composable
98 | override fun CreateCard(newsItem: NewsItem) {
99 | Card(
100 | modifier = Modifier
101 | .fillMaxWidth()
102 | .padding(8.dp),
103 | shape = RoundedCornerShape(8.dp)
104 | ) {
105 | Column(
106 | modifier = Modifier.padding(16.dp)
107 | ) {
108 | Text(newsItem.title, style = MaterialTheme.typography.titleSmall)
109 | Text(newsItem.content, style = MaterialTheme.typography.bodyMedium)
110 | }
111 | }
112 | }
113 | }
114 | ```
115 |
116 | - Rich Card Factory:
117 |
118 | ```kotlin
119 | class RichCardFactory : NewsCardFactory {
120 | @Composable
121 | override fun CreateCard(newsItem: NewsItem) {
122 | Card(
123 | modifier = Modifier
124 | .fillMaxWidth()
125 | .padding(8.dp),
126 | shape = RoundedCornerShape(8.dp)
127 | ) {
128 | Column(modifier = Modifier.padding(16.dp)) {
129 | newsItem.imageUrl?.let { url ->
130 | AsyncImage(
131 | model = url,
132 | contentDescription = null,
133 | contentScale = ContentScale.FillBounds,
134 | modifier = Modifier
135 | .fillMaxWidth()
136 | .height(200.dp)
137 | .clip(RoundedCornerShape(3.dp))
138 | )
139 | }
140 | Text(newsItem.title, style = MaterialTheme.typography.titleSmall)
141 | Text(newsItem.content, style = MaterialTheme.typography.bodyMedium)
142 |
143 | }
144 | }
145 | }
146 | }
147 | ```
148 |
149 | ### 3. Create a Factory Provider:
150 |
151 | ```kotlin
152 | enum class CardType { SIMPLE, RICH }
153 |
154 | @Composable
155 | fun CardFactoryProvider(
156 | cardType: CardType = CardType.SIMPLE,
157 | content: @Composable () -> Unit
158 | ) {
159 | val factory = when (cardType) {
160 | CardType.SIMPLE -> SimpleCardFactory()
161 | CardType.RICH -> RichCardFactory()
162 | }
163 | CompositionLocalProvider(LocalCardFactory provides factory) {
164 | content()
165 | }
166 | }
167 |
168 | private val LocalCardFactory = staticCompositionLocalOf {
169 | SimpleCardFactory() // Default
170 | }
171 | ```
172 |
173 | ### 4. Usage in your App:
174 |
175 | ```kotlin
176 | @Composable
177 | fun NewsFeed(
178 | newsItems: List,
179 | modifier: Modifier,
180 | ) {
181 | var selectedCardType by remember { mutableStateOf(CardType.SIMPLE) }
182 |
183 | CardFactoryProvider(cardType = selectedCardType) { // Or dynamically choose based on item type
184 | LazyColumn(modifier = modifier) {
185 | item {
186 | Row(modifier = Modifier.padding(8.dp)) {
187 | Button(onClick = { selectedCardType = CardType.SIMPLE }) {
188 | Text("Simple Card")
189 | }
190 | Spacer(modifier = Modifier.width(8.dp))
191 | Button(onClick = { selectedCardType = CardType.RICH }) {
192 | Text("Rich Card")
193 | }
194 | }
195 | }
196 | items(newsItems) { item ->
197 | val cardFactory = LocalCardFactory.current
198 | cardFactory.CreateCard(item)
199 | }
200 | }
201 | }
202 | }
203 | ```
204 |
205 | This approach allows for a flexible and extensible design where new types of cards can be added by
206 | creating new factories without altering existing code that uses these cards. It uses the Factory
207 | Pattern to manage the creation of different UI components based on the type of news item, enhancing
208 | modularity and maintainability of the UI.
209 |
210 | [Back to the beginning of the documentation](#head)
211 |
212 | -
213 |
214 | The abstract factory design pattern uses a factory class to create objects from multiple families.
215 | This pattern abstracts the object creation process, making your code more readable and flexible.
216 |
217 |
The abstract factory design pattern has two main components
218 |
219 | - **Abstract factory:** A class used to create objects from multiple families.
220 | - **Concrete factory:** A class that concretises the abstract factory and is used to create objects
221 | from a specific family.
222 |
223 |
Advantages of the abstract factory design pattern
224 |
225 | - Makes your code more flexible and extensible.
226 | - It makes your code more readable and understandable by abstracting the object creation process.
227 | - It makes the software development process more efficient.
228 |
229 |
Disadvantages of the abstract factory design pattern
322 | The Singleton design pattern allows only one object to be created from a class. This pattern is
323 | used when a single object is needed.
324 |
325 |
The Singleton design pattern has two main components
326 |
327 | - **Singleton class:** This class allows only one object to be created.
328 | - **Singleton object:** The only object created from the Singleton class.
329 |
330 |
Advantages of Singleton design pattern
331 |
332 | - Useful in situations where a single object is needed.
333 | - Makes your code more readable and understandable.
334 | - It makes the software development process more efficient.
335 |
336 |
Disadvantages of the Singleton design pattern
337 |
338 | - It may be difficult to use in complex applications.
339 | - It may cause you to write more code.
340 |
341 | **Sample Scenario**
342 | Imagine we want to manage a global theme configuration for an app, allowing access to the theme
343 | state from multiple places without passing it explicitly.
344 |
345 | ### Singleton Implementation
346 |
347 | ```kotlin
348 | object ThemeConfig {
349 | private var darkModeEnabled: Boolean = false
350 |
351 | fun isDarkModeEnabled(): Boolean = darkModeEnabled
352 |
353 | fun toggleDarkMode() {
354 | darkModeEnabled = !darkModeEnabled
355 | }
356 | }
357 | ```
358 |
359 | ### Usage in Composable Functions
360 |
361 | ```kotlin
362 | @Composable
363 | fun ThemeToggleButton() {
364 | val isDarkMode = remember { mutableStateOf(ThemeConfig.isDarkModeEnabled()) }
365 |
366 | Button(onClick = {
367 | ThemeConfig.toggleDarkMode()
368 | isDarkMode.value = ThemeConfig.isDarkModeEnabled()
369 | }) {
370 | Text(if (isDarkMode.value) "Switch to Light Mode" else "Switch to Dark Mode")
371 | }
372 | }
373 |
374 | @Composable
375 | fun AppContent() {
376 | val isDarkMode = ThemeConfig.isDarkModeEnabled()
377 | MaterialTheme(colorScheme = if (isDarkMode) darkColors() else lightColors()) {
378 | ThemeToggleButton()
379 | }
380 | }
381 | ```
382 |
383 | ## Ways to Implement Singleton in Kotlin
384 |
385 | ### 1. **Object Declaration** (Most Common)
386 |
387 | - Kotlin's `object` keyword inherently implements the singleton pattern.
388 |
389 | ```kotlin
390 | object MySingleton {
391 | fun doSomething() {
392 | println("Singleton Instance")
393 | }
394 | }
395 | ```
396 |
397 | ### 2. **Lazy Initialization**
398 |
399 | - Use the `lazy` delegate to create a singleton only when accessed for the first time.
400 |
401 | ```kotlin
402 | class MySingleton private constructor() {
403 | companion object {
404 | val instance: MySingleton by lazy { MySingleton() }
405 | }
406 | }
407 | ```
408 |
409 | ### 3. **Double-Checked Locking** (Thread-Safe Singleton)
410 |
411 | - Ensures thread safety in a multithreaded environment.
412 |
413 | ```kotlin
414 | class MySingleton private constructor() {
415 | companion object {
416 | @Volatile
417 | private var instance: MySingleton? = null
418 |
419 | fun getInstance(): MySingleton {
420 | return instance ?: synchronized(this) {
421 | instance ?: MySingleton().also { instance = it }
422 | }
423 | }
424 | }
425 | }
426 | ```
427 |
428 | ## Choosing the Right Approach
429 |
430 | - **Object Declaration**: Best for simplicity and Kotlin idiomatic code.
431 | - **Lazy Initialization**: Ideal when the instance creation is resource-intensive and you want to
432 | delay it until needed.
433 | - **Double-Checked Locking**: Use for thread safety in Java-style singletons.
434 |
435 | [Return to the beginning of the documentation](#head)
436 |
437 | -
438 | A prototype design pattern is a design pattern that uses a prototype object to create copies of
439 | objects. This can be more efficient than creating objects directly, especially if the creation of
440 | objects is complex or time-consuming.
441 |
442 |
The Prototype design pattern has three main components
443 |
444 | - **Prototype:** The object to be copied.
445 | - **Copier:** The class that copies the prototype object.
446 | - **Users:** Classes that use the copied objects.
447 |
448 |
Advantages of the Prototype design pattern
449 |
450 | - Makes the creation of objects more efficient.
451 | - Facilitates the creation of a number of copies with the same properties of objects.
452 | - It allows objects to be created independently of a given state.
453 |
454 |
Disadvantages of the Prototype design pattern
455 |
456 | - Changing the prototype object can also change all copied objects.
457 | - When the property of the prototype object is changed, it is also necessary to change the
458 | properties of the copied objects.
459 |
460 | **Sample Scenario**
461 |
462 | In Kotlin, `data class` provides a built-in `copy()` method that simplifies the implementation of
463 | the Prototype Pattern. This is particularly useful when creating multiple variations of an object
464 | with similar properties.
465 |
466 | ### Prototype Implementation
467 |
468 | ```kotlin
469 | data class Document(
470 | val title: String,
471 | val content: String,
472 | val author: String
473 | )
474 | ```
475 |
476 | ```kotlin
477 | @Composable
478 | fun DocumentCard(document: AppDocument, modifier: Modifier = Modifier) {
479 | Card(
480 | modifier = modifier.padding(8.dp),
481 | shape = MaterialTheme.shapes.medium
482 | ) {
483 | Column(modifier = Modifier.padding(16.dp)) {
484 | Text(text = "Title: ${document.title}", style = MaterialTheme.typography.bodyLarge)
485 | Text(text = "Content: ${document.content}", style = MaterialTheme.typography.bodyMedium)
486 | Text(text = "Author: ${document.author}", style = MaterialTheme.typography.bodySmall)
487 | }
488 | }
489 | }
490 | ```
491 |
492 | ```kotlin
493 | @Composable
494 | fun ProtoTypeView() {
495 | // Original prototype
496 | val originalDocument = AppDocument(
497 | title = "Prototype Pattern",
498 | content = "This is the original document content.",
499 | author = "John Doe"
500 | )
501 |
502 | // Clone the prototype and modify properties
503 | val clonedDocument = originalDocument.copy(
504 | title = "Cloned Prototype",
505 | content = "This is the cloned document content."
506 | )
507 |
508 | // UI Layout
509 | Column(
510 | modifier = Modifier
511 | .fillMaxSize()
512 | .padding(16.dp),
513 | verticalArrangement = Arrangement.spacedBy(16.dp)
514 | ) {
515 | Text("Documents", style = MaterialTheme.typography.titleLarge)
516 |
517 | // Display Original Document
518 | DocumentCard(document = originalDocument, modifier = Modifier.fillMaxWidth())
519 |
520 | // Display Cloned Document
521 | DocumentCard(document = clonedDocument, modifier = Modifier.fillMaxWidth())
522 | }
523 | }
524 | ```
525 |
526 | [Return to the beginning of the documentation](#head)
527 |
528 | -
529 | The Adapter design pattern is a structural design pattern that allows objects with incompatible
530 | interfaces to work together. This pattern is applied to reuse an existing class or interface class
531 | by adapting it to a different interface class.
532 |
533 | The Adapter pattern makes the interfaces of two different classes or interfaces similar to each
534 | other, allowing these classes or interfaces to be used together. In this way, it is possible to use
535 | an existing class or interface class in a new system or project without having to change or rewrite
536 | it.
537 |
538 |
The adapter design pattern has two main components
539 |
540 | - **Adapted class or interface:** The purpose of the adapter pattern is to adapt this class or
541 | interface to have a different interface.
542 | - **Adaptor class:** The adapter class is the class that adapts the adapted class or interface to
543 | have a different interface.
544 | - **Customer class:** A class that uses the interface of the adapter class.
545 |
546 |
Adapter design pattern advantages
547 |
548 | - It allows you to use an existing class or interface in a new system or project without changing
549 | it.
550 | - It makes it easier to bring together different technologies or platforms.
551 | - It allows to extend the functionality of a class or interface.
552 |
553 |
Disadvantages of the Adapter design pattern
554 |
555 | - The adapter class must support the full functionality of the adapted class or interface.
556 | - The adapter class may be dependent on the code of the adapted class or interface.
557 |
558 | **Sample Scenario**
559 |
560 | This example demonstrates the Adapter pattern in a scenario where you want to adapt one type of data
561 | model to another for display purposes within Jetpack Compose, without involving any legacy
562 | components. You're building a weather app where you fetch weather data in a particular format (
563 | `WeatherData`) from an API. However, your UI layer expects a different format (
564 | `WeatherPresentation`) for rendering. Instead of changing the data fetching logic or the UI layer,
565 | you can use an Adapter to transform `WeatherData` to `WeatherPresentation`.
566 |
567 | ### Data Models:
568 |
569 | ```kotlin
570 | data class WeatherData(
571 | val temperature: Float,
572 | val humidity: Float,
573 | val windSpeed: Float,
574 | val condition: String
575 | )
576 |
577 | data class WeatherPresentation(
578 | val tempDisplay: String,
579 | val humidityDisplay: String,
580 | val windDisplay: String,
581 | val weatherIcon: Int
582 | )
583 | ```
584 |
585 | ### Problem:
586 |
587 | The WeatherData format needs to be converted into WeatherPresentation for display in the UI.
588 |
589 | ### Solution:
590 |
591 | Create an Adapter to convert WeatherData to WeatherPresentation.
592 |
593 | ### Implementation
594 |
595 | 1. Create the Adapter:
596 |
597 | ```kotlin
598 | class WeatherDataAdapter {
599 | fun adapt(data: WeatherData): WeatherPresentation {
600 | return WeatherPresentation(
601 | tempDisplay = "${data.temperature}°C",
602 | humidityDisplay = "${data.humidity}%",
603 | windDisplay = "${data.windSpeed} m/s",
604 | weatherIcon = when (data.condition.lowercase()) {
605 | "sunny" -> R.drawable.logo
606 | "cloudy" -> R.drawable.ic_launcher_background
607 | "rainy" -> R.drawable.ic_launcher_foreground
608 | else -> R.drawable.logo
609 | }
610 | )
611 | }
612 | }
613 | ```
614 |
615 | This adapter class takes WeatherData and formats it into WeatherPresentation, which is more suitable
616 | for UI display. It converts temperatures, humidity, and wind speed into user-friendly strings and
617 | selects an appropriate icon based on the weather condition.
618 |
619 | ```kotlin
620 | @Composable
621 | fun WeatherScreen(weatherData: WeatherData) {
622 | val adapter = remember { WeatherDataAdapter() }
623 | val presentation = adapter.adapt(weatherData)
624 |
625 | Column(modifier = Modifier.padding(16.dp)) {
626 | Text("Temperature: ${presentation.tempDisplay}")
627 | Text("Humidity: ${presentation.humidityDisplay}")
628 | Text("Wind Speed: ${presentation.windDisplay}")
629 | Image(
630 | painter = painterResource(presentation.weatherIcon),
631 | contentDescription = "Weather Icon",
632 | modifier = Modifier.size(50.dp)
633 | )
634 | }
635 | }
636 | ```
637 |
638 | Here, we use the adapter to transform the incoming WeatherData into WeatherPresentation format
639 | before displaying it in our Composable UI.
640 |
641 | [Return to the beginning of the documentation](#head)
642 |
643 | -
644 | Bridge design pattern is a design pattern used to combine two independent hierarchical
645 | structures (abstraction and implementation) and to allow them to be modified separately. This
646 | pattern aims to create a more flexible structure by separating the abstraction of an object and
647 | the functionality (implementation) that operates on that abstraction.
648 |
649 |
Bridge design pattern has 4 main components
650 |
651 | **Abstraction:** This is the layer where the client interacts with an interface and where
652 | functionality is not fully realised.
653 |
654 | **Refined Abstraction:** These are subclasses of Abstraction and address a specific situation.
655 |
656 | **Implementation:** This is the layer that actually implements the abstraction.
657 |
658 | **Concrete Implementation:** These are subclasses of Implementation and actually implement a
659 | specific case.
660 |
661 |
Advantages of the Bridge design pattern
662 |
663 | - Flexibility and Extensibility: The abstraction and implementation can be changed independently of
664 | each other, which facilitates changes to the system.
665 |
666 | - Encapsulation: Application details can be hidden from the abstraction. The client interacts only
667 | with the abstraction.
668 |
669 | - Change Management: Changes on one side do not affect the other. For example, only the abstraction
670 | can change and the application can remain unchanged, or vice versa.
671 |
672 |
Disadvantages of the Bridge design pattern
673 |
674 | - Complexity: The implementation of the pattern can sometimes lead to complexity, especially if the
675 | size of the project is small or the requirements are simple, this complexity may be unnecessary.
676 |
677 | **Sample Scenario**
678 |
679 | So how can we implement this in a real application, package, etc. Let's look at it. Due to our
680 | scenario, we want to use our own video processing technology instead of the video processing
681 | technology of applications such as Youtube, Netflix, Amazon Prime, etc. in our project. While doing
682 | this, we need to consider the potential for applications with different video processing
683 | technologies to be included in our project in the future. At this point, **Birdge Design Pattern**
684 | comes into play. Our aim is to ensure that the old code structure can be renewed and continue to
685 | function whenever it is renewed.
686 |
687 | As per our scenario, we are writing an **abstract** class for our own **Video Processor**technology.
688 | In it we have a method signature named **process(String videoFile)**.
689 |
690 | ```kotlin
691 | // 1. Define the VideoProcessor abstraction.
692 | interface VideoProcessor {
693 | fun process(videoFile: String)
694 | }
695 | ```
696 |
697 | Now it is time to implement our **Video Processor** technology for the related video/videos. For
698 | this, let's assume that we support HD, UHD (4K) and QUHD (8K) video quality. For each video quality,
699 | we get **instantiation** from our **Video Processor** **abstract** class.
700 |
701 | ```kotlin
702 | class HDProcessor : VideoProcessor {
703 | override fun process(videoFile: String) {
704 | println("$videoFile is being processed with HD quality.")
705 | }
706 | }
707 | ```
708 |
709 | ```kotlin
710 | class UHD4KProcessor : VideoProcessor {
711 | override fun process(videoFile: String) {
712 | println("$videoFile is being processed with UHD 4K quality.")
713 | }
714 | }
715 | ```
716 |
717 | ```kotlin
718 | class QUHD8KProcessor : VideoProcessor {
719 | override fun process(videoFile: String) {
720 | println("$videoFile is being processed with QUHD 8K quality.")
721 | }
722 | }
723 | ```
724 |
725 | Then we define an interface for **Video**. In it, we ensure that our **Video Processor** technology
726 | is implemented compulsorily. Then we define an empty method named **play(String videoFile)** for the
727 | video.
728 |
729 | ```kotlin
730 | abstract class Video(private val processor: VideoProcessor) {
731 | abstract fun play(videoFile: String)
732 |
733 | protected fun process(videoFile: String) {
734 | processor.process(videoFile)
735 | }
736 | }
737 | ```
738 |
739 | ```kotlin
740 | class YoutubeVideo(processor: VideoProcessor) : Video(processor) {
741 | override fun play(videoFile: String) {
742 | process(videoFile)
743 | println("Playing $videoFile on YouTube.")
744 | }
745 | }
746 | ```
747 |
748 | Now let's start running our scenario for Netflix and Youtube. We create separate classes for both
749 | Netflix and Youtube and inherit from the **Video** interface.
750 |
751 | ```kotlin
752 | class NetflixVideo(processor: VideoProcessor) : Video(processor) {
753 | override fun play(videoFile: String) {
754 | process(videoFile)
755 | println("Playing $videoFile on Netflix.")
756 | }
757 | }
758 | ```
759 |
760 | ```kotlin
761 | class AmazonPrimeVideo(processor: VideoProcessor) : Video(processor) {
762 | override fun play(videoFile: String) {
763 | process(videoFile)
764 | println("Playing $videoFile on Amazon Prime.")
765 | }
766 | }
767 | ```
768 |
769 | So how can we use this on the UI side?
770 |
771 | ```kotlin
772 | @Composable
773 | fun BridgeView() {
774 | val context = LocalContext.current
775 |
776 | Column(
777 | modifier = Modifier.fillMaxSize(),
778 | verticalArrangement = Arrangement.Center,
779 | horizontalAlignment = Alignment.CenterHorizontally
780 | ) {
781 | Button(
782 | onClick = {
783 | val youtubeVideo = YoutubeVideo(HDProcessor())
784 | youtubeVideo.play("video_hd.mp4")
785 | Toast.makeText(context, "Playing HD video on YouTube", Toast.LENGTH_SHORT).show()
786 | }) {
787 | Text("Watch HD Video on YouTube")
788 | }
789 |
790 | Spacer(modifier = Modifier.height(16.dp))
791 |
792 | Button(
793 | onClick = {
794 | val netflixVideo = NetflixVideo(UHD4KProcessor())
795 | netflixVideo.play("video_4k.mp4")
796 | Toast.makeText(context, "Playing UHD 4K video on Netflix", Toast.LENGTH_SHORT)
797 | .show()
798 | }) {
799 | Text("Watch UHD 4K Video on Netflix")
800 | }
801 |
802 | Spacer(modifier = Modifier.height(16.dp))
803 |
804 | Button(
805 | onClick = {
806 | val amazonPrimeVideo = AmazonPrimeVideo(QUHD8KProcessor())
807 | amazonPrimeVideo.play("video_8k.mp4")
808 | Toast.makeText(context, "Playing QUHD 8K video on Amazon Prime", Toast.LENGTH_SHORT)
809 | .show()
810 | }) {
811 | Text("Watch QUHD 8K Video on Amazon Prime")
812 | }
813 | }
814 | }
815 | ```
816 |
817 | -
818 | The Composite design pattern is a powerful structural pattern that allows you to treat individual
819 | objects and composites of objects in the same way. It helps you create object hierarchies that
820 | treat both parts (individual objects) and wholes (composite objects) in the same way.
821 |
822 |
There are three main components of the composite design pattern
823 |
824 | - **Abstract Interface:** This is the foundation of the model. It defines the common behaviour that
825 | all objects in the hierarchy, both individual and composite, must follow.
826 | - **Concrete Classes:** These are implementations of the abstract interface representing specific
827 | types of objects. Each class defines its own behaviour for the interface methods.
828 | - **Client Code:** This is the code that interacts with objects in the hierarchy. Clients only see
829 | the abstract interface, allowing them to treat both individual objects and composites in the same
830 | way
831 |
832 |
Advantages of composite design pattern
833 |
834 | - Clients do not need to handle different object types differently.
835 | - More flexible and reusable code You can easily add new item types that fit the common interface.
836 | - Easier maintenance Changes to one item type do not necessarily affect others.
837 | - Improved code readability: The code reflects the real-world structure of your data.
838 |
839 |
Disadvantages of the Composite design pattern
840 |
841 | - Let's not make a big hierarchical scan, performance can be impressive.
842 |
843 | **Sample Scenario**
844 |
845 | For example, there are certain categories that you feel belong to the same group. For example, there
846 | are certain categories in an e-commerce application. Under these categories, there are subcategories
847 | or headings related to the relevant category. To build these structures more easily and flexibly, *
848 | *Composite Design Pattern** comes into play. We can either handle related objects individually, or
849 | we can handle categories as multiple and build the hierarchy. Our goal will be to provide this
850 | flexibility.
851 |
852 | Now the first thing we will build according to our scenario will be **Abstract Class** which will
853 | provide the common structure.
854 |
855 | ```kotlin
856 | interface CartItem {
857 | fun getName(): String
858 |
859 | fun getPrice(): Double
860 |
861 | @Composable
862 | fun Render()
863 | }
864 | ```
865 |
866 | Then we create a class named **Product** for any product. We start building the structure for a
867 | single product by inheriting from the **Abstract** class named **CartItem** that we have created
868 | this class. Since it will hold information about the product, we write the necessary variables.
869 | Remember, our scenario will be an E-commerce application. Since the **buildItemWidget()** method
870 | returns generic, we prefer to write a **Card** for the product.
871 |
872 | ```kotlin
873 | data class Product(
874 | val title: String,
875 | val description: String,
876 | val imageUrl: String,
877 | private val price: Double,
878 | var quantity: Int = 0
879 | ) : CartItem {
880 | override fun getName() = title
881 | override fun getPrice() = price * quantity
882 |
883 | @Composable
884 | override fun Render() {
885 | Card(
886 | modifier = Modifier
887 | .fillMaxWidth()
888 | .padding(8.dp)
889 | ) {
890 | Row(
891 | modifier = Modifier
892 | .padding(8.dp)
893 | .fillMaxWidth(),
894 | verticalAlignment = Alignment.CenterVertically
895 | ) {
896 | Image(
897 | painter = rememberAsyncImagePainter(imageUrl),
898 | contentDescription = null,
899 | modifier = Modifier
900 | .size(60.dp)
901 | .clip(RoundedCornerShape(8.dp))
902 | )
903 | Spacer(modifier = Modifier.width(8.dp))
904 | Column(modifier = Modifier.weight(1f)) {
905 | Text(text = title, style = MaterialTheme.typography.titleMedium)
906 | Text(text = description, style = MaterialTheme.typography.bodyMedium)
907 | }
908 | Row {
909 | IconButton(onClick = { if (quantity > 0) quantity-- }) {
910 | Icon(imageVector = Icons.Default.Clear, contentDescription = "Remove")
911 | }
912 | Text(text = "$quantity", modifier = Modifier.align(Alignment.CenterVertically))
913 | IconButton(onClick = { quantity++ }) {
914 | Icon(imageVector = Icons.Default.Add, contentDescription = "Add")
915 | }
916 | }
917 | }
918 | }
919 | }
920 | }
921 | ```
922 |
923 | Now it is time to build a product tree collectively. In our scenario, we will consider the **Car**
924 | and **Desktop Computer** categories. Since these categories can be divided into many parts (wheels,
925 | motherboard, etc.)
926 | We create a class named **Category** that inherits from **CartItem** **Abstract** class to manage
927 | related structures in a common way. The **buildItemWidget()** method returns the **ExpansionPanel**
928 | and we collect other similar products in the **final List> children** list under a
929 | single common category heading.
930 |
931 | ```kotlin
932 | data class Category(
933 | private val name: String,
934 | val children: List,
935 | var isExpanded: Boolean = false
936 | ) : CartItem {
937 | override fun getName() = name
938 | override fun getPrice() = children.sumOf { it.getPrice() }
939 |
940 | @Composable
941 | override fun Render() {
942 | var expanded by remember { mutableStateOf(isExpanded) }
943 | Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
944 | Row(
945 | modifier = Modifier
946 | .fillMaxWidth()
947 | .clickable { expanded = !expanded }
948 | .padding(8.dp),
949 | verticalAlignment = Alignment.CenterVertically
950 | ) {
951 | Text(text = name, style = MaterialTheme.typography.titleMedium)
952 | Spacer(modifier = Modifier.weight(1f))
953 | Icon(
954 | imageVector = if (expanded) Icons.Default.ArrowDropDown else Icons.Default.KeyboardArrowUp,
955 | contentDescription = null
956 | )
957 | }
958 | if (expanded) {
959 | Column {
960 | children.forEach { it.Render() }
961 | }
962 | }
963 | }
964 | }
965 | }
966 | ```
967 |
968 | Let's see what kind of usage scenario can be on the UI side. First of all, we create a list to set
969 | the relevant category and single products. As I said before, our categories will be **Desktop
970 | Computer** and **Car**. There will be related products in the sub-products.
971 |
972 | ```kotlin
973 | val categories = listOf(
974 | Category(
975 | name = "Desktop Computer",
976 | children = listOf(
977 | Product(
978 | "Main Board",
979 | "Part of the computer",
980 | "https://via.placeholder.com/150",
981 | 1000.0
982 | ),
983 | Product("CPU", "Part of the computer", "https://via.placeholder.com/150", 2000.0)
984 | )
985 | ),
986 | Category(
987 | name = "Car",
988 | children = listOf(
989 | Product("Electric Car", "Type of the car", "https://via.placeholder.com/150", 9000.0),
990 | Product("Wheel", "Part of the car", "https://via.placeholder.com/150", 1000.0)
991 | )
992 | )
993 | )
994 |
995 | ```
996 |
997 | We display categories and single product using **ExpansionPanelList**.
998 |
999 | ```kotlin
1000 | @Composable
1001 | fun ShoppingCartScreen(categories: List) {
1002 | LazyColumn(modifier = Modifier.fillMaxSize().padding(8.dp)) {
1003 | items(categories) { category ->
1004 | category.Render()
1005 | }
1006 | }
1007 | }
1008 |
1009 | @Preview(showBackground = true)
1010 | @Composable
1011 | fun PreviewShoppingCart() {
1012 | MaterialTheme {
1013 | ShoppingCartScreen(categories)
1014 | }
1015 | }
1016 | ```
1017 |
1018 | [Return to the beginning of the documentation](#head)
1019 |
1020 | -
1021 |
1022 | The Decorator design pattern is a design pattern used to dynamically add new properties to an
1023 | object. This is done without changing or extending the functionality of the base object. The
1024 | Decorator design pattern provides flexibility and maintainability when used correctly. However, like
1025 | any design pattern, it is important to evaluate whether it suits your application requirements.
1026 |
1027 |
The Decorator design pattern has 3 main components
1028 |
1029 | - **Abstract Component Interface(OPTIONAL):** This interface is completely optional. You can create
1030 | an **abstract** behaviour for the component to be decorated.
1031 | - **Concrete Component:** This is the pure form of the component to be decorated. Optionally *
1032 | *abstract component interface** can be implemented.
1033 | - **Abstract Decorator Class:** Provides an **abstract** layer to decorator classes for the
1034 | component to be decorated. The decor classes to be used inherit from this class.
1035 | - **Decorator Class:** Decorates the component to be decorated. More than one **decorator** class
1036 | can be made for the component to be decorated.
1037 |
1038 |
Advantages of the Decorator design pattern
1039 |
1040 | - Flexibility: The Decorator pattern provides a flexible way of dynamically adding behaviour to
1041 | objects. Adding new responsibilities or removing existing ones can be done without changing
1042 | classes.
1043 | - Open-Closed Principle: The Decorator pattern ensures that classes are open (allowing adding new
1044 | behaviours) and closed (not modifying existing code). This helps your code to be more
1045 | maintainable.
1046 | - Composite Objects: The Decorator pattern allows you to combine other objects on top of an object.
1047 | This allows you to create complex structures by combining an object in different combinations.
1048 |
1049 |
Disadvantages of the Decorator design pattern
1050 |
1051 | - Code Complexity: When the Decorator pattern is used, a number of classes are created to add
1052 | additional responsibilities to an object. This can lead to code complexity over time.
1053 | - Lots of Small Objects: The Decorator pattern requires a class to be created for each decorator
1054 | class. This can lead to a large number of small objects and an increase in project size.
1055 | - Logical Ordering of Wrappers: The order of the decorators is important in the Decorator pattern.
1056 | In some cases, incorrect determination of the order of the decorators may lead to unexpected
1057 | results.
1058 | - Complexity of Composite Objects: The complexity of composite objects can increase by adding
1059 | multiple decorators. This can lead to a structure that is difficult to understand and maintain.
1060 |
1061 | **Example Scenario**
1062 | We want to create a `Card` composable that can be dynamically enhanced with additional features like
1063 | shadows, borders, or padding. This ensures flexibility and reusability without modifying the core
1064 | implementation of the `Card`.
1065 |
1066 | ### Step 1: Base Composable
1067 |
1068 | This is the base composable that will be decorated.
1069 |
1070 | ```kotlin
1071 | @Composable
1072 | fun BaseCard(content: @Composable () -> Unit) {
1073 | Card(
1074 | modifier = Modifier.padding(8.dp),
1075 | elevation = CardDefaults.cardElevation(2.dp)
1076 | ) {
1077 | content()
1078 | }
1079 | }
1080 | ```
1081 |
1082 | ### Step 2: Decorator Composables
1083 |
1084 | **Add Border Decorator:**
1085 | Wrap the `BaseCard` with a border.
1086 |
1087 | ```kotlin
1088 | @Composable
1089 | fun BorderDecorator(content: @Composable () -> Unit) {
1090 | Box(
1091 | modifier = Modifier.border(2.dp, Color.Blue)
1092 | ) {
1093 | content()
1094 | }
1095 | }
1096 | ```
1097 |
1098 | **Add Shadow Decorator:**
1099 | Add shadow to the `BaseCard`.
1100 |
1101 | ```kotlin
1102 | @Composable
1103 | fun ShadowDecorator(content: @Composable () -> Unit) {
1104 | Box(
1105 | modifier = Modifier.shadow(8.dp, shape = RoundedCornerShape(8.dp))
1106 | ) {
1107 | content()
1108 | }
1109 | }
1110 | ```
1111 |
1112 | **Add Padding Decorator:**
1113 | Add extra padding around the `BaseCard`.
1114 |
1115 | ```kotlin
1116 | @Composable
1117 | fun PaddingDecorator(content: @Composable () -> Unit) {
1118 | Box(
1119 | modifier = Modifier.padding(16.dp)
1120 | ) {
1121 | content()
1122 | }
1123 | }
1124 | ```
1125 |
1126 | #### Step 3: Combine Decorators
1127 |
1128 | You can dynamically combine these decorators as needed.
1129 |
1130 | ```kotlin
1131 | @Composable
1132 | fun DecoratedCard() {
1133 | ShadowDecorator {
1134 | BorderDecorator {
1135 | PaddingDecorator {
1136 | BaseCard {
1137 | Text(
1138 | text = "This is a decorated card!",
1139 | modifier = Modifier.padding(8.dp)
1140 | )
1141 | }
1142 | }
1143 | }
1144 | }
1145 | }
1146 | ```
1147 |
1148 | ### Usage in a Jetpack Compose Screen
1149 |
1150 | ```kotlin
1151 | @Composable
1152 | fun DecoratorPatternExample() {
1153 | Scaffold(
1154 | topBar = {
1155 | TopAppBar(title = { Text("Decorator Pattern Example") })
1156 | }
1157 | ) {
1158 | Column(
1159 | modifier = Modifier
1160 | .fillMaxSize()
1161 | .padding(16.dp),
1162 | verticalArrangement = Arrangement.Center,
1163 | horizontalAlignment = Alignment.CenterHorizontally
1164 | ) {
1165 | DecoratedCard()
1166 | }
1167 | }
1168 | }
1169 | ```
1170 |
1171 | [Return to the beginning of the documentation](#head)
1172 |
1173 | -
1174 | The Facade design pattern is a structural design pattern used to manage complex systems with a
1175 | simple interface. This pattern is used to facilitate the use of systems and hide their complexity.
1176 | The Facade design pattern facilitates the understandability and use of code, especially in large
1177 | software systems, by limiting direct access to subsystems and combining a set of subsystem
1178 | functions into a single, high-level interface.
1179 |
1180 | Facade enables complex subsystems to be exposed to the outside world through a simplified interface.
1181 | Users can use these systems without having in-depth knowledge about the complex structures and
1182 | functioning of the subsystems.
1183 |
1184 |
The Facade design pattern has two main components
1185 |
1186 | - **Facade:** Provides the simplified interface presented to the outside world. It combines the
1187 | functions of subsystems and presents them to the user.
1188 | - **Subsystems:** Classes that contain the complex functionality covered by the Facade interface.
1189 | These are not called directly by the user, but are managed by the Facade class.
1190 |
1191 |
Advantages of the Facade design pattern
1192 |
1193 | - Enables the use of complex systems with a simpler interface.
1194 | - Reduces direct interaction with subsystems, making code easier to maintain and update.
1195 | - Facilitates testing of subsystems individually.
1196 |
1197 |
Disadvantages of the Facade design pattern
1198 |
1199 | - An extra layer of abstraction can sometimes lead to performance loss.
1200 | - A very simplified interface can in some cases restrict access to all features of subsystems.
1201 |
1202 | **Sample Scenario**
1203 | Imagine a music player app. The app has different subsystems like audio playback, notifications, and
1204 | analytics. Instead of interacting directly with these subsystems, the client (UI) interacts with a
1205 | `MusicPlayerFacade` that simplifies the process.
1206 |
1207 | ### Step 1: Create Subsystems
1208 |
1209 | #### Audio Player Subsystem
1210 |
1211 | ```kotlin
1212 | class AudioPlayer {
1213 | fun play(track: String) {
1214 | println("Playing track: $track")
1215 | }
1216 |
1217 | fun pause() {
1218 | println("Audio paused")
1219 | }
1220 |
1221 | fun stop() {
1222 | println("Audio stopped")
1223 | }
1224 | }
1225 | ```
1226 |
1227 | #### Notification Subsystem
1228 |
1229 | ```kotlin
1230 | class NotificationManager {
1231 | fun showNotification(track: String) {
1232 | println("Showing notification: Now playing $track")
1233 | }
1234 |
1235 | fun clearNotification() {
1236 | println("Clearing notification")
1237 | }
1238 | }
1239 | ```
1240 |
1241 | #### Analytics Subsystem
1242 |
1243 | ```kotlin
1244 | class AnalyticsManager {
1245 | fun logEvent(event: String) {
1246 | println("Logging event: $event")
1247 | }
1248 | }
1249 | ```
1250 |
1251 | ### Step 2: Create the Facade
1252 |
1253 | The `MusicPlayerFacade` simplifies interactions with these subsystems.
1254 |
1255 | ```kotlin
1256 | class MusicPlayerFacade(
1257 | private val audioPlayer: AudioPlayer,
1258 | private val notificationManager: NotificationManager,
1259 | private val analyticsManager: AnalyticsManager
1260 | ) {
1261 |
1262 | fun playTrack(track: String) {
1263 | audioPlayer.play(track)
1264 | notificationManager.showNotification(track)
1265 | analyticsManager.logEvent("Track played: $track")
1266 | }
1267 |
1268 | fun pauseTrack() {
1269 | audioPlayer.pause()
1270 | analyticsManager.logEvent("Track paused")
1271 | }
1272 |
1273 | fun stopTrack() {
1274 | audioPlayer.stop()
1275 | notificationManager.clearNotification()
1276 | analyticsManager.logEvent("Track stopped")
1277 | }
1278 | }
1279 | ```
1280 |
1281 | ### Step 3: Using the Facade in Jetpack Compose
1282 |
1283 | The UI layer interacts only with the `MusicPlayerFacade`.
1284 |
1285 | ```kotlin
1286 | @Composable
1287 | fun FacadeView() {
1288 | val musicPlayerFacade = remember {
1289 | MusicPlayerFacade(
1290 | audioPlayer = AudioPlayer(),
1291 | notificationManager = NotificationManager(),
1292 | analyticsManager = AnalyticsManager()
1293 | )
1294 |
1295 | }
1296 |
1297 | var isPlaying by remember { mutableStateOf(false) }
1298 | val track = "My Favorite Song"
1299 |
1300 | Scaffold {
1301 |
1302 | Column(
1303 | modifier = Modifier
1304 | .padding(16.dp)
1305 | .padding(it)
1306 | ) {
1307 | Text("Music Player", style = MaterialTheme.typography.titleMedium)
1308 | Spacer(modifier = Modifier.height(16.dp))
1309 |
1310 | Button(
1311 | onClick = {
1312 | if (isPlaying) {
1313 | musicPlayerFacade.pauseTrack()
1314 | } else {
1315 | musicPlayerFacade.playTrack(track)
1316 | }
1317 | isPlaying = !isPlaying
1318 | }) {
1319 | Text(if (isPlaying) "Pause" else "Play")
1320 | }
1321 |
1322 | Spacer(modifier = Modifier.height(8.dp))
1323 |
1324 | Button(
1325 | onClick = {
1326 | musicPlayerFacade.stopTrack()
1327 | isPlaying = false
1328 | }) {
1329 | Text("Stop")
1330 | }
1331 | }
1332 | }
1333 | }
1334 | ```
1335 |
1336 | [Return to the beginning of the documentation](#head)
1337 |
1338 | -
1339 | The Flyweight design pattern is a structural design pattern used to optimize memory usage. This
1340 | pattern aims to reduce repetitive states by separating intrinsic states and non-shareable states (
1341 | extrinsic states) between objects, thus efficiently reducing memory usage. It becomes especially
1342 | important in cases where many similar objects are created. An example of using the Flyweight
1343 | design pattern in Jetpack Compose would be optimizing repeating composables, especially in widget trees. In
1344 | Jetpack Compose applications, some composables are used repeatedly, especially in list or grid views. In this
1345 | case, by applying the Flyweight pattern, we can optimize memory usage and improve the performance
1346 | of the application.
1347 |
1348 |
The Flyweight design pattern has 4 main components
1349 |
1350 | - **Flyweight Interface:** Defines a common interface of shared objects.
1351 | - **Concrete Flyweight:** Class that implements the Flyweight interface and stores the intrinsic
1352 | state.
1353 | - **Flyweight Factory:** Creates and manages Flyweight objects. If the same object has been created
1354 | before, it allows it to be reused.
1355 | - **Client:** Uses Flyweight objects. It provides the extrinsic state and combines it with
1356 | Flyweight.
1357 |
1358 |
Advantages of the Flyweight design pattern
1359 |
1360 | - Reduces memory usage by preventing similar objects from being created over and over again.
1361 | - Performance increases because fewer objects are created.
1362 |
1363 |
Disadvantages of the Flyweight design pattern
1364 |
1365 | - Design can get complicated.
1366 | - Management of internal and external situations may become difficult.
1367 |
1368 | **Sample Scenario**
1369 |
1370 | How about doing this in an actual application, package, etc. How can we implement it? Let's look at
1371 | it. According to our scenario, we want to make a social media application. Let's imagine a list
1372 | showing posts in this application. Instead of creating the same icons over and over again for
1373 | actions such as comments, likes and shares on each post, we will optimize them with the **Flyweight
1374 | ** design pattern.
1375 |
1376 | First, we start by making the **Flyweight Interface** layer. We place a method with the method
1377 | signature **Widget createWidget(Color color, double size)**, which returns **Widget**.
1378 |
1379 | ```kotlin
1380 | interface Flyweight {
1381 | @Composable
1382 | fun render(color: Color, size: Dp)
1383 | }
1384 | ```
1385 |
1386 | Then it's time for the **Concrete Flyweight** layer. We will store the **intrinsic state** in this
1387 | layer. In our case, this will be an icon. At the same time, we **@override** the **createWidget**
1388 | method by **implementing** the **Flyweight** layer.
1389 |
1390 | ```kotlin
1391 | class IconFlyweight(private val icon: ImageVector) : Flyweight {
1392 |
1393 | @Composable
1394 | override fun render(color: Color, size: Dp) {
1395 | Icon(imageVector = icon, tint = color, modifier = Modifier.size(size))
1396 | }
1397 | }
1398 | ```
1399 |
1400 | It's time to create **Flyweight** objects in the **Flyweight Factory** layer. Here, if there is a
1401 | previously created object, **icons** are pulled from the map. If it is an object that comes for the
1402 | first time, it is added to the map.
1403 |
1404 | ```kotlin
1405 | object IconFactory {
1406 | private val icons = mutableMapOf()
1407 |
1408 | fun getIcon(icon: ImageVector): IconFlyweight {
1409 | return icons.getOrPut(icon) { IconFlyweight(icon) }
1410 | }
1411 | }
1412 | ```
1413 |
1414 | So how can we use this on the UI (**Client**) side?
1415 |
1416 | ```kotlin
1417 | @Composable
1418 | fun PostList(posts: List) {
1419 | Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
1420 | posts.forEach { post ->
1421 | PostItem(postContent = post)
1422 | }
1423 | }
1424 | }
1425 |
1426 | @Composable
1427 | fun PostItem(postContent: String) {
1428 | val likeIcon = IconFactory.getIcon(Icons.Default.Favorite)
1429 | val shareIcon = IconFactory.getIcon(Icons.Default.Share)
1430 |
1431 | Column(
1432 | modifier = Modifier
1433 | .fillMaxWidth()
1434 | .padding(vertical = 8.dp)
1435 | ) {
1436 | Text(text = postContent, style = MaterialTheme.typography.bodyMedium)
1437 | Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
1438 | likeIcon.render(color = Color.Red, size = 24.dp)
1439 | shareIcon.render(color = Color.Blue, size = 24.dp)
1440 | }
1441 | }
1442 | }
1443 | ```
1444 |
1445 | [Return to the beginning of the documentation](#head)
1446 |
1447 | -
1448 | A proxy design pattern is a structural design pattern used to control access to an object or to
1449 | make this access through another object. While this pattern is used to extend or modify the
1450 | functionality of an object, it operates without changing the structure of the original object. The
1451 | proxy serves as a kind of interface or representative to the real object.
1452 |
1453 |
The proxy design pattern has three main components
1454 |
1455 | - **Subject Interface:** The actual object and the interface that the proxy should implement.
1456 | - **Real Subject:** The actual object that the client wants to access.
1457 | - **Proxy:** An object that controls access to or replaces the real object.
1458 |
1459 |
Advantages of proxy design pattern
1460 |
1461 | - Proxy allows you to control access to real objects. For example, you can add security controls or
1462 | access permissions.
1463 | - Can improve the performance of the application by delaying the loading of expensive resources. It
1464 | is especially useful for large objects or data coming over the network.
1465 | - It can increase performance by reducing unnecessary network traffic, especially when retrieving
1466 | data from remote servers. For example, by caching data, it can prevent the same data from being
1467 | loaded repeatedly.
1468 | - The proxy can log operations performed on the real object and add extra layers of security.
1469 | - Users or other objects can interact with real objects without being aware of the existence of the
1470 | proxy.
1471 |
1472 |
Disadvantages of proxy design pattern
1473 |
1474 | - Implementation of proxy pattern can increase the overall complexity of the system. For simple
1475 | cases, this extra complexity may be unnecessary.
1476 | - Proxy class may create extra processing load in some cases. In particular, going through a proxy
1477 | on every request can increase processing time.
1478 | - Proper management of the proxy is necessary, especially if features such as caching or security
1479 | have been added. A mismanaged proxy can lead to data inconsistency or security vulnerabilities.
1480 | - Implementing the proxy pattern correctly can make the design difficult to understand and extend in
1481 | some cases.
1482 | - The layers added by the proxy can make testing processes more complex in some cases.
1483 |
1484 | **Sample Scenario**
1485 |
1486 | How about doing this in an actual application, package, etc. How can we implement it? Let's look at
1487 | it. As a real-life scenario in Jetpack Compose, a proxy can be used to access a remote API. For example,
1488 | when an application is pulling data from a remote server, it can use a proxy to manage these
1489 | requests and add a caching mechanism if necessary. Let's assume we are using a **Weather** API in
1490 | this scenario.
1491 |
1492 | First of all, we create an **interface** named **WeatherService** as **Subject Interface**. This
1493 | interface has a method called **getWeatherData** to retrieve data from the API. We will implement
1494 | this layer to the original object and the proxy layer.
1495 |
1496 | ```kotlin
1497 | interface WeatherService {
1498 | suspend fun getWeatherData(): String
1499 | }
1500 | ```
1501 |
1502 | Then, we implement the **Subject Interface** while writing a **Real Subject** layer named *
1503 | *WeatherApiService**.
1504 |
1505 | ```kotlin
1506 | class WeatherApiService : WeatherService {
1507 | override suspend fun getWeatherData(): String {
1508 | return "Sunny, 25°C"
1509 | }
1510 | }
1511 | ```
1512 |
1513 | Now it's time for the **Proxy** layer, which is the key point of the **Proxy Design Pattern**. The
1514 | proxy layer captures API requests and, if necessary, adds a cache mechanism or logs the requests.
1515 |
1516 | ```kotlin
1517 | class WeatherServiceProxy(private val apiService: WeatherApiService = WeatherApiService()) :
1518 | WeatherService {
1519 | private var cachedData: String? = null
1520 |
1521 | override suspend fun getWeatherData(): String {
1522 | return if (cachedData == null) {
1523 | println("Fetching data from API...")
1524 | cachedData = apiService.getWeatherData()
1525 | cachedData!!
1526 | } else {
1527 | println("Returning cached data...")
1528 | cachedData!!
1529 | }
1530 | }
1531 | }
1532 | ```
1533 |
1534 | So how can we use this? In this example, the WeatherServiceProxy class controls the data retrieval
1535 | from the API and caches the data. In the first request, it accesses the real API and retrieves the
1536 | data, and in subsequent requests it uses the cached data. This approach can improve performance and
1537 | reduce network traffic, especially in situations where the same data is needed frequently. The proxy
1538 | design pattern provides an efficient solution in such scenarios. In our case, we will assign the
1539 | data pulled 5 times to the cache after it is pulled for the first time, and we will quickly obtain
1540 | the answers to the remaining 4 requests from the cache. In this way, we will not be loaded with
1541 | unnecessary network traffic.
1542 |
1543 | ```kotlin
1544 | @Composable
1545 | fun ProxyView() {
1546 | val weatherService: WeatherService = WeatherServiceProxy()
1547 |
1548 | val weatherData = remember { mutableStateOf("Loading...") }
1549 |
1550 | LaunchedEffect(Unit) {
1551 | weatherData.value = getWeatherFiveTimes(weatherService)
1552 | }
1553 |
1554 | Scaffold(
1555 | topBar = {
1556 | TopAppBar(title = { Text("Weather App") })
1557 | }
1558 | ) { padding ->
1559 | Box(
1560 | modifier = Modifier
1561 | .fillMaxSize()
1562 | .padding(padding),
1563 | contentAlignment = Alignment.Center
1564 | ) {
1565 | Text(text = weatherData.value, style = MaterialTheme.typography.body1)
1566 | }
1567 | }
1568 | }
1569 |
1570 | suspend fun getWeatherFiveTimes(service: WeatherService): String {
1571 | val results = mutableListOf()
1572 | repeat(5) {
1573 | val data = service.getWeatherData()
1574 | results.add(data)
1575 | }
1576 | return results.joinToString("\n")
1577 | }
1578 | ```
1579 |
1580 | [Return to the beginning of the documentation](#head)
1581 |
1582 | -
1583 | Let's discuss the Chain of Responsibility design pattern in Jetpack Compose in more detail. This pattern
1584 | is useful for managing incoming requests or commands across different composables or screens,
1585 | especially in large and modular Jetpack Compose applications.
1586 |
1587 |
The Chain of Responsibility design pattern has three main components
1588 |
1589 | - **Handler:** An interface that defines how to process the request and pass the request to the next
1590 | handler in the chain.
1591 | - **Concrete Handlers:** Classes that implement the Handler interface. Each processor decides
1592 | whether to process the request or pass it to the next processor in the chain.
1593 | - **Client:** The person or system that initiates the request and sends it to the first handler of
1594 | the chain.
1595 |
1596 |
Working Mechanism
1597 |
1598 | - The client sends the request to the first handler in the chain.
1599 | - Each processor checks the request and decides whether to process it or not.
1600 | - If a handler can process the request, it performs the action and the process ends.
1601 | - If the handler cannot process the request, it forwards it to the next handler in the chain.
1602 | - This process continues until a handler processes the request or the chain ends.
1603 |
1604 |
Advantages of the Chain of Responsibility design pattern
1605 |
1606 | - Sender and receiver become independent, encouraging loose coupling in the system.
1607 | - Easy to add new handlers or change the order of existing ones.
1608 | - Each handler has a single responsibility, making the code easier to maintain.
1609 |
1610 |
Disadvantages of proxy design pattern
1611 |
1612 | - The request may pass through multiple processors, which may impact performance.
1613 | - Can be difficult to debug because the request passes through various handlers.
1614 |
1615 | **Sample Scenario**
1616 |
1617 | How about doing this in an actual application, package, etc. How can we implement it? Let's look at
1618 | it. Consider a Jetpack Compose application that processes different types of user input (gestures, button
1619 | clicks, text input). The application can use the Chain of Responsibility model to process these
1620 | inputs.
1621 |
1622 | The **Handler** interface, which forms the basis of the Chain of Responsibility pattern, defines the
1623 | basic methods that each **Concrete Handlers** class must implement. In Jetpack Compose, this is usually done
1624 | in the form of an abstract class. In our case, **InteractionHandler** will be our **Handler** *
1625 | *abstarct** class. This abstract class will be inherited by **Concrete Handlers**'s. *
1626 | *setNextHandler** will be a method to establish connections between chains. In this way, when an
1627 | incompatible situation occurs, the next chain will run.
1628 |
1629 | ```kotlin
1630 | abstract class InteractionHandler {
1631 | private var nextHandler: InteractionHandler? = null
1632 |
1633 | fun setNextHandler(handler: InteractionHandler) {
1634 | nextHandler = handler
1635 | }
1636 |
1637 | fun handleNext(interactionType: String, onResult: (String) -> Unit) {
1638 | if (nextHandler != null) {
1639 | nextHandler!!.handleInteraction(interactionType, onResult)
1640 | } else {
1641 | onResult("Unrecognized interaction: $interactionType")
1642 | }
1643 | }
1644 |
1645 | abstract fun handleInteraction(interactionType: String, onResult: (String) -> Unit)
1646 | }
1647 | ```
1648 |
1649 | Then we define our **Concrete Handlers** classes. In our case, we are writing 2 different **Concrete
1650 | Handlers** classes, **ButtonInteractionHandler** and **FormInteractionHandler**, as an example. If *
1651 | *ButtonInteractionHandler** from these classes is used, we want to display an **AlertBox** on the
1652 | screen as per the scenario. If the **FormInteractionHandler** class is used, we want to print the
1653 | _Form submitted_ log by submitting. If **interactionType** is not found, we provide relevant
1654 | information by running the **handleUnrecognizedInteraction** method.
1655 |
1656 | ```kotlin
1657 | class ButtonInteractionHandler : InteractionHandler() {
1658 | override fun handleInteraction(interactionType: String, onResult: (String) -> Unit) {
1659 | if (interactionType == "buttonClick") {
1660 | onResult("Button Clicked: Button interaction handled.")
1661 | } else {
1662 | handleNext(interactionType, onResult)
1663 | }
1664 | }
1665 | }
1666 |
1667 | class FormInteractionHandler : InteractionHandler() {
1668 | override fun handleInteraction(interactionType: String, onResult: (String) -> Unit) {
1669 | if (interactionType == "formSubmit") {
1670 | onResult("Form submitted successfully.")
1671 | } else {
1672 | handleNext(interactionType, onResult)
1673 | }
1674 | }
1675 | }
1676 | ```
1677 |
1678 | So, in what scenario can we use this on the UI side? Let's assume we have 3 buttons: **Click Me**, *
1679 | *Submit Form** and **Unknown**. First of all, we create a **ButtonInteractionHandler** and set its *
1680 | *interactionType** to **buttonClick**. The purpose of this button is to display an **AlertDialog**if
1681 | **buttonClick** exists. If **interactionType** is not technically supported in the current handler,
1682 | the next handler will be processed. If **interactionType** is not supported at all, we notify the
1683 | user with **handleUnrecognizedInteraction**.
1684 |
1685 | ```kotlin
1686 | @OptIn(ExperimentalMaterial3Api::class)
1687 | @Composable
1688 | fun ChainOfResponsibilityView() {
1689 | var message by remember { mutableStateOf("") }
1690 |
1691 | // Initialize handlers
1692 | val buttonHandler = ButtonInteractionHandler()
1693 | val formHandler = FormInteractionHandler()
1694 | buttonHandler.setNextHandler(formHandler)
1695 |
1696 | // UI
1697 | Scaffold(
1698 | topBar = {
1699 | TopAppBar(title = { Text("Chain of Responsibility in Compose") })
1700 | },
1701 | content = { padding ->
1702 | Column(
1703 | modifier = Modifier
1704 | .fillMaxSize()
1705 | .padding(padding),
1706 | verticalArrangement = Arrangement.Center,
1707 | horizontalAlignment = Alignment.CenterHorizontally
1708 | ) {
1709 | Button(onClick = {
1710 | buttonHandler.handleInteraction("buttonClick") { result ->
1711 | message = result
1712 | }
1713 | }) {
1714 | Text("Click Me")
1715 | }
1716 |
1717 | Spacer(modifier = Modifier.height(16.dp))
1718 |
1719 | Button(onClick = {
1720 | buttonHandler.handleInteraction("formSubmit") { result ->
1721 | message = result
1722 | }
1723 | }) {
1724 | Text("Submit Form")
1725 | }
1726 |
1727 | Spacer(modifier = Modifier.height(16.dp))
1728 |
1729 | Button(onClick = {
1730 | buttonHandler.handleInteraction("unknown") { result ->
1731 | message = result
1732 | }
1733 | }) {
1734 | Text("Unknown")
1735 | }
1736 |
1737 | Spacer(modifier = Modifier.height(32.dp))
1738 |
1739 | Text(
1740 | text = message,
1741 | style = MaterialTheme.typography.bodyLarge,
1742 | modifier = Modifier.padding(16.dp)
1743 | )
1744 | }
1745 | }
1746 | )
1747 | }
1748 | ```
1749 |
1750 | [Return to the beginning of the documentation](#head)
1751 |
1752 |
1753 | -
1754 | The Interpreter design pattern is a behavioral design pattern that allows us to define a grammar for a language and provide an interpreter that processes expressions in that language.
1755 |
1756 |
The Interpreter design pattern has 4 main components
1757 |
1758 | - **Expression Interface:** This interface declares a method of interpreting a particular context. It is the core of the interpreter pattern.
1759 | - **Concrete Expression Classes:** These classes implement the Expression interface and interpret specific rules in the language.
1760 | - **Context Class(optional):** This class contains general information about the interpreter.
1761 | - **Client:** The client creates the syntax tree representing a particular sentence that defines the grammar of the language. The tree consists of instances of Concrete Expression classes.
1762 |
1763 |
Advantages of the Interpreter design pattern
1764 |
1765 | - Grammar rules and interpreters can be easily changed and new expressions added as needed.
1766 | - It ensures that the code is modular and reusable.
1767 | - Can be optimized for processing complex expressions.
1768 |
1769 |
Disadvantages of the Interpreter design pattern
1770 |
1771 | - Developing interpreters for complex languages can be difficult.
1772 | - For simple expressions the interpreter may be slower than direct code.
1773 |
1774 | **Sample Scenario**
1775 |
1776 | Under normal circumstances, **Interpreter Design Pattern** is used more in _programming languages_, _SQL queries_, _Mathematical expressions_, _Game engines_, but since our current focus is **Jetpack Compose**, **Interpreter Design Pattern** is based on **Jetpack Compose Framework**. We will try to use it. For our scenario, let's consider a mobile application that allows users to define customizable widget structures using a text-based language. Users can dynamically build their interfaces using a simple language that specifies specific widget types, features, and layouts. For example, a user may want to show text by typing something like `"Text: Deatsilence"` or they might want to show an image by typing `"Image: https://picsum.photos/200"`.
1777 |
1778 | First, we define an **Expression Interface** named **WidgetExpression**. We write a method signature called _interpret()_ in **WidgetExpression** that returns a Widget. This interface will be implemented by **Concrete Expression** classes.
1779 |
1780 | ```kotlin
1781 | sealed interface UIComponent {
1782 | @Composable
1783 | fun interpret()
1784 | }
1785 | ```
1786 |
1787 | Afterwards, we create two **Concrete Expression Class** named **ConcreteExpressionText** and **ConcreteExpressionImage** and implement the abstract class named **WidgetExpression**. We _override_ the _interpret_ method in the **Concrete Expression** classes and return **Text or Image** according to the text script received from the user. We can do this for other composables as well, but according to our scenario, we continue with these two specifically.
1788 |
1789 | ```kotlin
1790 | class ConcreteExpressionText(private val text: String) : UIComponent {
1791 | @Composable
1792 | override fun interpret() {
1793 | Text(text = text, style = TextStyle(fontSize = 20.sp))
1794 | }
1795 | }
1796 |
1797 |
1798 | class ConcreteExpressionImage(private val url: String) : UIComponent {
1799 | @Composable
1800 | override fun interpret() {
1801 | AsyncImage(
1802 | model = url,
1803 | contentDescription = null,
1804 | modifier = Modifier.size(100.dp)
1805 | )
1806 | }
1807 | }
1808 | ```
1809 |
1810 | Then, we add a method called **parseScript()** into the **WidgetParser** class to interpret the scripts coming from the user.
1811 |
1812 | ```kotlin
1813 | class WidgetParser {
1814 | fun parseScript(script: String): List {
1815 | val expressions = mutableListOf()
1816 | script.lines().forEach { line ->
1817 | val trimmedLine = line.trim()
1818 | when {
1819 | trimmedLine.startsWith("Text:") -> {
1820 | val text = trimmedLine.substringAfter("Text:").trim()
1821 | expressions.add(ConcreteExpressionText(text))
1822 | }
1823 | trimmedLine.startsWith("Image:") -> {
1824 | val url = trimmedLine.substringAfter("Image:").trim()
1825 | if (url.startsWith("https://")) {
1826 | expressions.add(ConcreteExpressionImage(url))
1827 | }
1828 | }
1829 | }
1830 | }
1831 | return expressions
1832 | }
1833 | }
1834 | ```
1835 |
1836 | Finally, how can we use them on the UI side? Let's look at it. For example, let's interpret some scripts from the user via a **TextField**. Let's show the image or text to the user as a result of the interpretation.
1837 |
1838 | ```kotlin
1839 | @OptIn(ExperimentalMaterial3Api::class)
1840 | @Composable
1841 | fun InterpreterView() {
1842 | var script by remember { mutableStateOf("") }
1843 | val parser = remember { WidgetParser() }
1844 | var expressions by remember { mutableStateOf(parser.parseScript(script)) }
1845 |
1846 | Scaffold(
1847 | topBar = {
1848 | TopAppBar(
1849 | title = { Text("Interpreter Pattern") }
1850 | )
1851 | },
1852 | content = { paddingValues ->
1853 | Column(
1854 | modifier = Modifier
1855 | .fillMaxSize()
1856 | .padding(paddingValues)
1857 | .padding(16.dp),
1858 | verticalArrangement = Arrangement.spacedBy(16.dp)
1859 | ) {
1860 | TextField(
1861 | value = script,
1862 | onValueChange = {
1863 | script = it
1864 | },
1865 | modifier = Modifier.fillMaxWidth(),
1866 | )
1867 | Button(onClick = { expressions = parser.parseScript(script) }) {
1868 | Text("Interpret the Script")
1869 | }
1870 | Spacer(modifier = Modifier.height(16.dp))
1871 | expressions.forEach { expression ->
1872 | expression.interpret()
1873 | Spacer(modifier = Modifier.height(8.dp))
1874 | }
1875 | }
1876 | }
1877 | )
1878 | }
1879 | ```
1880 |
1881 | - When the user sees any text following the **Text:** keyword to display text, the relevant text will be displayed on the screen.
1882 |
1883 | - To display an image, the user must provide a url followed by the **Image:** keyword. It will display the image found in the URL on the screen.
1884 |
1885 | [Return to the beginning of the documentation](#head)
1886 |
1887 |
1888 | -