Accept Friend Requests: A ViewModel Guide

by Admin 42 views
Accept Friend Requests: A ViewModel Guide

Hey guys! Ever wondered how to smoothly handle friend requests in your app? Let's dive into creating a ViewModel for an accept friend request screen. This guide will walk you through the essentials, ensuring your users can easily manage their connections.

Understanding the ViewModel

At its core, a ViewModel is designed to manage and prepare the data for your UI (User Interface). Think of it as the brain that feeds information to your screen, making sure everything is displayed correctly and that user interactions are handled efficiently. In the context of an accept friend request screen, the ViewModel is responsible for fetching the list of pending friend requests, providing methods to accept or reject these requests, and updating the UI accordingly.

The primary goal here is to decouple the UI from the data layer. This means your screen (Activity, Fragment, or Composable) shouldn't be directly dealing with databases, network calls, or complex business logic. Instead, it observes the ViewModel, which handles all the heavy lifting. This separation makes your code more testable, maintainable, and scalable.

To kick things off, your ViewModel needs to perform several key tasks. First, it should fetch the pending friend requests from a data source. This could be a local database, a remote server, or even a combination of both. The ViewModel then transforms this data into a format suitable for the UI, often using data classes or models that represent each friend request. Next, the ViewModel exposes this data to the UI, typically using LiveData, StateFlow, or other observable data holders. These observables allow the UI to react to changes in the data, ensuring that the screen always displays the most up-to-date information.

Additionally, the ViewModel must provide methods for accepting and rejecting friend requests. When the user interacts with the UI (e.g., clicks an "Accept" button), the UI calls the corresponding method in the ViewModel. The ViewModel then performs the necessary actions, such as updating the database or sending a request to the server. After the action is completed, the ViewModel updates the list of pending friend requests and notifies the UI of the changes.

By centralizing all this logic in the ViewModel, you ensure that your UI remains clean and focused on its primary responsibility: displaying information and handling user input. This approach not only simplifies your code but also makes it easier to test and maintain.

Setting Up Your Project

Before we start coding, let's make sure our project is properly set up. This involves adding the necessary dependencies and configuring the basic structure of our app. For this example, we'll assume you're using Android Studio with Kotlin, but the principles apply to other platforms as well.

First, you'll need to add the required dependencies to your build.gradle file. This typically includes dependencies for ViewModel, LiveData (or StateFlow), and any networking or database libraries you plan to use. Here’s a basic example:

dependencies {
 implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1" 
 implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1" 
 implementation "com.squareup.retrofit2:retrofit:2.9.0" // Example for networking
 implementation "androidx.room:room-runtime:2.5.0" // Example for database
 ksp "androidx.room:room-compiler:2.5.0" // KSP for Room
}

Make sure to sync your Gradle file after adding these dependencies to download the necessary libraries. Next, let's set up the basic structure of our app. This usually involves creating the following components:

  • Data Models: These are Kotlin data classes that represent the structure of your data, such as a FriendRequest object with properties like id, name, and profilePicture.
  • Data Sources: These classes handle fetching data from various sources, such as a remote API or a local database. For example, you might have a FriendRequestService that uses Retrofit to fetch friend requests from a server, or a FriendRequestDao that uses Room to interact with a local database.
  • Repositories: These classes act as intermediaries between the ViewModel and the data sources. They encapsulate the logic for retrieving and managing data, providing a clean API for the ViewModel to use. A FriendRequestRepository might fetch friend requests from the FriendRequestService and cache them in a local database.
  • ViewModel: This is where the main logic resides. It fetches data from the repository, transforms it into a format suitable for the UI, and exposes it to the view. It also handles user interactions, such as accepting or rejecting friend requests.
  • UI Components: These are the Activities, Fragments, or Composables that display the data and handle user input. They observe the ViewModel and update the UI accordingly.

By organizing your app into these components, you create a clear separation of concerns, making your code more maintainable and testable. Now that we have our project set up, let's dive into the code and start building our ViewModel.

Implementing the ViewModel

Alright, let's get our hands dirty with some code! We'll start by creating the AcceptFriendRequestViewModel class. This class will be responsible for fetching friend requests, exposing them to the UI, and handling accept/reject actions.

First, let's define the basic structure of our ViewModel:

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class AcceptFriendRequestViewModel(private val friendRequestRepository: FriendRequestRepository) : ViewModel() {

 private val _friendRequests = MutableLiveData<List<FriendRequest>>()
 val friendRequests: LiveData<List<FriendRequest>> = _friendRequests

 init {
 loadFriendRequests()
 }

 private fun loadFriendRequests() {
 viewModelScope.launch {
 val requests = friendRequestRepository.getFriendRequests()
 _friendRequests.value = requests
 }
 }

 fun acceptFriendRequest(requestId: String) {
 viewModelScope.launch {
 friendRequestRepository.acceptFriendRequest(requestId)
 loadFriendRequests() // Refresh the list after accepting
 }
 }

 fun rejectFriendRequest(requestId: String) {
 viewModelScope.launch {
 friendRequestRepository.rejectFriendRequest(requestId)
 loadFriendRequests() // Refresh the list after rejecting
 }
 }
}

In this snippet:

  • We inject a FriendRequestRepository into the ViewModel's constructor. This repository is responsible for fetching and managing friend request data.
  • We use LiveData to hold the list of friend requests. LiveData is an observable data holder that is lifecycle-aware, meaning it automatically manages its subscriptions based on the lifecycle of the observing component.
  • In the init block, we call the loadFriendRequests() function to fetch the initial list of friend requests when the ViewModel is created.
  • The loadFriendRequests() function uses viewModelScope.launch to launch a coroutine in the ViewModel's scope. This ensures that the coroutine is automatically canceled when the ViewModel is destroyed, preventing memory leaks.
  • The acceptFriendRequest() and rejectFriendRequest() functions call the corresponding methods in the repository and then refresh the list of friend requests.

Now, let's look at how to implement the FriendRequestRepository:

class FriendRequestRepository(private val friendRequestService: FriendRequestService, private val friendRequestDao: FriendRequestDao) {

 suspend fun getFriendRequests(): List<FriendRequest> {
 // First, try to get the friend requests from the local database
 val localRequests = friendRequestDao.getAllFriendRequests()

 if (localRequests.isNotEmpty()) {
 return localRequests
 }

 // If the local database is empty, fetch them from the remote API
 val remoteRequests = friendRequestService.getFriendRequests()

 // Save the remote requests to the local database
 friendRequestDao.insertAll(remoteRequests)

 return remoteRequests
 }

 suspend fun acceptFriendRequest(requestId: String) {
 friendRequestService.acceptFriendRequest(requestId)
 friendRequestDao.deleteFriendRequest(requestId)
 }

 suspend fun rejectFriendRequest(requestId: String) {
 friendRequestService.rejectFriendRequest(requestId)
 friendRequestDao.deleteFriendRequest(requestId)
 }
}

In this repository:

  • We inject a FriendRequestService and a FriendRequestDao into the constructor. The FriendRequestService is responsible for fetching data from a remote API, while the FriendRequestDao is responsible for interacting with a local database.
  • The getFriendRequests() function first tries to fetch the friend requests from the local database. If the database is empty, it fetches them from the remote API and saves them to the database.
  • The acceptFriendRequest() and rejectFriendRequest() functions call the corresponding methods in the service and the DAO to update the data both remotely and locally.

Connecting to the UI

With our ViewModel and repository in place, the next step is to connect them to the UI. This involves observing the LiveData exposed by the ViewModel and updating the UI accordingly.

Here's an example of how to do this in an Android Activity or Fragment using Kotlin:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

 private lateinit var viewModel: AcceptFriendRequestViewModel
 private lateinit var recyclerView: RecyclerView
 private lateinit var adapter: FriendRequestAdapter

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

 // Initialize ViewModel (replace with your preferred method of ViewModel creation)
 viewModel = AcceptFriendRequestViewModel(FriendRequestRepository(FriendRequestService(), FriendRequestDao()))

 // Initialize RecyclerView
 recyclerView = findViewById(R.id.recyclerView)
 recyclerView.layoutManager = LinearLayoutManager(this)
 adapter = FriendRequestAdapter(viewModel)
 recyclerView.adapter = adapter

 // Observe the LiveData
 viewModel.friendRequests.observe(this, Observer {
 adapter.submitList(it)
 })
 }
}

In this code:

  • We initialize the ViewModel using a ViewModelProvider. Make sure to replace the example instantiation with your preferred method of creating ViewModels (e.g., using Hilt or Koin).
  • We initialize the RecyclerView and set up the adapter. The FriendRequestAdapter is responsible for displaying the list of friend requests in the RecyclerView.
  • We observe the friendRequests LiveData and call adapter.submitList(it) whenever the list of friend requests changes. This updates the RecyclerView with the new data.

And here’s a basic implementation of the FriendRequestAdapter:

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView

class FriendRequestAdapter(private val viewModel: AcceptFriendRequestViewModel) : ListAdapter<FriendRequest, FriendRequestAdapter.FriendRequestViewHolder>(FriendRequestDiffCallback()) {

 class FriendRequestViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
 val nameTextView: TextView = itemView.findViewById(R.id.nameTextView)
 val acceptButton: Button = itemView.findViewById(R.id.acceptButton)
 val rejectButton: Button = itemView.findViewById(R.id.rejectButton)
 }

 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FriendRequestViewHolder {
 val view = LayoutInflater.from(parent.context).inflate(R.layout.item_friend_request, parent, false)
 return FriendRequestViewHolder(view)
 }

 override fun onBindViewHolder(holder: FriendRequestViewHolder, position: Int) {
 val request = getItem(position)
 holder.nameTextView.text = request.name

 holder.acceptButton.setOnClickListener {
 viewModel.acceptFriendRequest(request.id)
 }

 holder.rejectButton.setOnClickListener {
 viewModel.rejectFriendRequest(request.id)
 }
 }
}

class FriendRequestDiffCallback : DiffUtil.ItemCallback<FriendRequest>() {
 override fun areItemsTheSame(oldItem: FriendRequest, newItem: FriendRequest): Boolean {
 return oldItem.id == newItem.id
 }

 override fun areContentsTheSame(oldItem: FriendRequest, newItem: FriendRequest): Boolean {
 return oldItem == newItem
 }
}

In this adapter:

  • We use a ListAdapter to efficiently update the RecyclerView whenever the list of friend requests changes.
  • We define a FriendRequestViewHolder to hold the views for each item in the RecyclerView.
  • In the onBindViewHolder() method, we bind the data to the views and set up the click listeners for the accept and reject buttons.
  • The FriendRequestDiffCallback is used to efficiently compare the old and new lists of friend requests and update only the items that have changed.

Testing Your ViewModel

Testing is a critical part of software development, and ViewModels are no exception. A well-tested ViewModel ensures that your business logic is correct and that your UI behaves as expected. Let's look at how to test our AcceptFriendRequestViewModel.

First, you'll need to add the necessary testing dependencies to your build.gradle file. This typically includes JUnit, Mockito (or Mockk), and Coroutines test:

dependencies {
 testImplementation "junit:junit:4.13.2"
 testImplementation "org.mockito:mockito-core:3.12.4"
 testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4"
}

Make sure to sync your Gradle file after adding these dependencies. Now, let's create a test class for our ViewModel:

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations

@ExperimentalCoroutinesApi
class AcceptFriendRequestViewModelTest {

 @get:Rule
 val instantTaskExecutorRule = InstantTaskExecutorRule()

 private val testDispatcher = TestCoroutineDispatcher()

 @Mock
 private lateinit var friendRequestRepository: FriendRequestRepository

 @Mock
 private lateinit var observer: Observer<List<FriendRequest>>

 private lateinit var viewModel: AcceptFriendRequestViewModel

 @Before
 fun setup() {
 MockitoAnnotations.openMocks(this)
 Dispatchers.setMain(testDispatcher)
 viewModel = AcceptFriendRequestViewModel(friendRequestRepository)
 viewModel.friendRequests.observeForever(observer)
 }

 @After
 fun tearDown() {
 Dispatchers.resetMain()
 testDispatcher.cleanupTestCoroutines()
 viewModel.friendRequests.removeObserver(observer)
 }

 @Test
 fun `loadFriendRequests should update LiveData with friend requests`() = runBlocking {
 // Arrange
 val friendRequests = listOf(
 FriendRequest("1", "John Doe"),
 FriendRequest("2", "Jane Smith")
 )
 `when`(friendRequestRepository.getFriendRequests()).thenReturn(friendRequests)

 // Act
 viewModel.loadFriendRequests()
 testDispatcher.scheduler.advanceUntilIdle()

 // Assert
 // Verify that the observer's onChanged method was called with the correct list of friend requests
 }

 @Test
 fun `acceptFriendRequest should call repository and reload friend requests`() = runBlocking {
 // Arrange
 val requestId = "1"

 // Act
 viewModel.acceptFriendRequest(requestId)
 testDispatcher.scheduler.advanceUntilIdle()

 // Assert
 // Verify that the repository's acceptFriendRequest method was called with the correct request ID
 // Verify that loadFriendRequests was called
 }

 @Test
 fun `rejectFriendRequest should call repository and reload friend requests`() = runBlocking {
 // Arrange
 val requestId = "1"

 // Act
 viewModel.rejectFriendRequest(requestId)
 testDispatcher.scheduler.advanceUntilIdle()

 // Assert
 // Verify that the repository's rejectFriendRequest method was called with the correct request ID
 // Verify that loadFriendRequests was called
 }
}

In this test class:

  • We use JUnit for running the tests and Mockito for creating mock objects.
  • The @get:Rule annotation is used to apply JUnit rules that run before each test. In this case, we use the InstantTaskExecutorRule to run all background tasks synchronously, making the tests more predictable.
  • The @Before annotation is used to set up the test environment before each test. In this method, we initialize the mock objects, create the ViewModel, and observe the friendRequests LiveData.
  • The @After annotation is used to clean up the test environment after each test. In this method, we remove the observer and reset the coroutine dispatcher.
  • The @Test annotation is used to define the test methods. Each test method should test a specific scenario.
  • In the test methods, we use Mockito.when`` to define the behavior of the mock objects. For example, in the loadFriendRequests test, we define that the friendRequestRepository.getFriendRequests() method should return a list of friend requests.
  • We use viewModel.loadFriendRequests() to trigger the ViewModel's loadFriendRequests method. After the call, use testDispatcher.scheduler.advanceUntilIdle() to make sure all coroutines are completed.

Conclusion

And there you have it! Building a ViewModel for an accept friend request screen involves setting up your project, implementing the ViewModel and repository, connecting them to the UI, and writing tests. By following these steps, you can create a robust and maintainable solution for managing friend requests in your app. Remember, this is just a starting point. Feel free to extend and customize this example to fit your specific needs. Happy coding, and may your friend requests always be accepted!