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
FriendRequestobject with properties likeid,name, andprofilePicture. - 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
FriendRequestServicethat uses Retrofit to fetch friend requests from a server, or aFriendRequestDaothat 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
FriendRequestRepositorymight fetch friend requests from theFriendRequestServiceand 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
FriendRequestRepositoryinto the ViewModel's constructor. This repository is responsible for fetching and managing friend request data. - We use
LiveDatato hold the list of friend requests.LiveDatais an observable data holder that is lifecycle-aware, meaning it automatically manages its subscriptions based on the lifecycle of the observing component. - In the
initblock, we call theloadFriendRequests()function to fetch the initial list of friend requests when the ViewModel is created. - The
loadFriendRequests()function usesviewModelScope.launchto 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()andrejectFriendRequest()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
FriendRequestServiceand aFriendRequestDaointo the constructor. TheFriendRequestServiceis responsible for fetching data from a remote API, while theFriendRequestDaois 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()andrejectFriendRequest()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
FriendRequestAdapteris responsible for displaying the list of friend requests in the RecyclerView. - We observe the
friendRequestsLiveData and calladapter.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
ListAdapterto efficiently update the RecyclerView whenever the list of friend requests changes. - We define a
FriendRequestViewHolderto 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
FriendRequestDiffCallbackis 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:Ruleannotation is used to apply JUnit rules that run before each test. In this case, we use theInstantTaskExecutorRuleto run all background tasks synchronously, making the tests more predictable. - The
@Beforeannotation is used to set up the test environment before each test. In this method, we initialize the mock objects, create the ViewModel, and observe thefriendRequestsLiveData. - The
@Afterannotation is used to clean up the test environment after each test. In this method, we remove the observer and reset the coroutine dispatcher. - The
@Testannotation 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 theloadFriendRequeststest, we define that thefriendRequestRepository.getFriendRequests()method should return a list of friend requests. - We use
viewModel.loadFriendRequests()to trigger the ViewModel's loadFriendRequests method. After the call, usetestDispatcher.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!