Design Patterns in Android App Development

In Android app development, design patterns are reusable solutions to common problems. They help ensure code maintainability, scalability, and flexibility. Here’s an overview of key design patterns used in Android app development, with examples:

1. Model-View-ViewModel (MVVM)

  • Purpose: MVVM separates the UI (View) from the business logic (ViewModel), making the code more modular and easier to test.
  • Components:
    • Model: Represents the data and business logic.
    • View: Displays the UI and interacts with the user.
    • ViewModel: Holds the logic for preparing data for the View and manages UI-related data.
  • Example: In an Android app that fetches a list of users from a REST API:
    • Model: UserRepository makes the API call.
    • ViewModel: UserViewModel holds the user data and state.
    • View: UserActivity observes the UserViewModel and updates the UI.

Example Code (MVVM):

// Model
data class User(val id: Int, val name: String)
interface UserRepository {
    suspend fun getUsers(): List<User>
}

// ViewModel
class UserViewModel(private val repository: UserRepository) : ViewModel() {
    private val _users = MutableLiveData<List<User>>()
    val users: LiveData<List<User>> = _users

    fun fetchUsers() {
        viewModelScope.launch {
            _users.value = repository.getUsers()
        }
    }
}

// View (Activity)
class UserActivity : AppCompatActivity() {
    private lateinit var userViewModel: UserViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val userRepository = UserRepositoryImpl()
        val viewModelFactory = UserViewModelFactory(userRepository)
        userViewModel = ViewModelProvider(this, viewModelFactory).get(UserViewModel::class.java)

        userViewModel.users.observe(this, Observer { users ->
            // Update UI with users
        })

        userViewModel.fetchUsers()
    }
}

2. Singleton

  • Purpose: Ensures a class has only one instance throughout the application.
  • Example: Used for classes like network clients (e.g., Retrofit, OkHttpClient), databases (Room), etc.
  • Example Code:
object RetrofitClient {
    val retrofit: Retrofit by lazy {
        Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .build()
    }
}

3. Factory

  • Purpose: Provides a way to create objects without specifying the exact class of object that will be created. It's useful for dependency injection or when you have complex object creation logic.
  • Example: Used in DI (Dependency Injection) frameworks like Hilt or Dagger.
  • Example Code:
interface Button {
    fun render()
}

class WindowsButton : Button {
    override fun render() {
        println("Rendering Windows button")
    }
}

class MacButton : Button {
    override fun render() {
        println("Rendering Mac button")
    }
}

class ButtonFactory {
    fun createButton(os: String): Button {
        return if (os == "Windows") WindowsButton() else MacButton()
    }
}

4. Observer

  • Purpose: Allows a subject (e.g., ViewModel or data model) to notify all its observers (e.g., UI components) about changes.
  • Example: This is commonly used in LiveData in Android, where the UI observes changes in data, and updates automatically when the data changes.
  • Example Code:
// Model
class UserModel {
    private val _name = MutableLiveData<String>()
    val name: LiveData<String> = _name

    fun setName(name: String) {
        _name.value = name
    }
}

// Observer (Activity or Fragment)
class UserFragment : Fragment() {
    private lateinit var userModel: UserModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        userModel.name.observe(viewLifecycleOwner, Observer { name ->
            // Update UI
            userNameTextView.text = name
        })
        return inflater.inflate(R.layout.fragment_user, container, false)
    }
}

5. Adapter

  • Purpose: Adapts one interface to another, often used in connecting a data source to a UI component, such as RecyclerView.Adapter.
  • Example: Adapter pattern is used in RecyclerView to display lists of data.
  • Example Code:
class UserAdapter(private val users: List<User>) : RecyclerView.Adapter<UserAdapter.UserViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val binding = ListItemUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return UserViewHolder(binding)
    }

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        val user = users[position]
        holder.bind(user)
    }

    override fun getItemCount(): Int = users.size

    inner class UserViewHolder(private val binding: ListItemUserBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(user: User) {
            binding.userName.text = user.name
        }
    }
}

6. Decorator

  • Purpose: Adds behavior to an object dynamically. It’s useful for scenarios where inheritance is not flexible enough.
  • Example: This can be used for adding functionalities like logging, security checks, etc., to existing objects.
  • Example Code:
interface Notifier {
    fun send(message: String)
}

class EmailNotifier : Notifier {
    override fun send(message: String) {
        println("Sending email: $message")
    }
}

class SmsNotifier(private val notifier: Notifier) : Notifier {
    override fun send(message: String) {
        println("Sending SMS: $message")
        notifier.send(message)
    }
}

7. Command

  • Purpose: Encapsulates a request as an object, thereby letting users parameterize clients with queues, requests, and operations.
  • Example: Used in implementing Undo/Redo functionality.
  • Example Code:
interface Command {
    fun execute()
}

class LightOnCommand(private val light: Light) : Command {
    override fun execute() {
        light.turnOn()
    }
}

class LightOffCommand(private val light: Light) : Command {
    override fun execute() {
        light.turnOff()
    }
}

class RemoteControl {
    private var command: Command? = null

    fun setCommand(command: Command) {
        this.command = command
    }

    fun pressButton() {
        command?.execute()
    }
}

class Light {
    fun turnOn() {
        println("Light is ON")
    }

    fun turnOff() {
        println("Light is OFF")
    }
}

8. Strategy

  • Purpose: Allows a family of algorithms to be defined and encapsulated, making them interchangeable. The Strategy pattern lets the algorithm vary independently from clients that use it.
  • Example: Used for switching between different types of sorting algorithms or network request strategies.
  • Example Code:
interface SortStrategy {
    fun sort(list: List<Int>): List<Int>
}

class QuickSort : SortStrategy {
    override fun sort(list: List<Int>): List<Int> {
        // Quick sort logic
        return list.sorted()
    }
}

class MergeSort : SortStrategy {
    override fun sort(list: List<Int>): List<Int> {
        // Merge sort logic
        return list.sorted()
    }
}

class SortContext(private var strategy: SortStrategy) {
    fun setStrategy(strategy: SortStrategy) {
        this.strategy = strategy
    }

    fun executeStrategy(list: List<Int>): List<Int> {
        return strategy.sort(list)
    }
}

Summary

Design patterns like MVVM, Singleton, Factory, Observer, and others can help structure Android applications efficiently. They enhance modularity, reusability, testability, and scalability, ultimately leading to better maintainable codebases. Understanding when and how to apply these patterns is key to building robust Android apps.

Thanks for reading! ๐ŸŽ‰ I'd love to know what you think about the article. Did it resonate with you? ๐Ÿ’ญ Any suggestions for improvement? I’m always open to hearing your feedback to improve my posts! ๐Ÿ‘‡๐Ÿš€. Happy coding! ๐Ÿ’ป✨


0 comments:

Post a Comment