I am still new to coding in kotlin for android and I am having trouble with a function for my child Naming Application. This particular function is meant to allow users to swipe left and right on ChildNames, swipe left for dislike ,right for like, the swipes are then meant to be recorded by the data class 'SwipedName' into a room database tablecalled 'SwipedNames'. however currently no data is being saved to the table as well as swiping left moves the user to the previous name rather that to the next name.
package com.examples.childrennaming
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.examples.childrennaming.ui.theme.ChildrenNamingTheme
class SwipeActivity : ComponentActivity() {
private lateinit var database: AppDatabase //Declares a private lateinit variable database of type AppDatabase for late state intialisation
override fun onCreate(savedInstanceState: Bundle?) { //onCreate method is called when the activity is first created to perform intial set up
super.onCreate(savedInstanceState)
val currentUserId = intent.getIntExtra("CURRENT_USER_ID", -1)
database = (application as MyApp).database //Initialises the database property using the application context
val nameDao = database.nameDao() //Retrieves the DAO (Data Access Object) from the database
val repository = NameRepository(nameDao) //Creates an instance of NameRepository with the DAO
val viewModel: NameViewModel by viewModels { NameViewModelFactory(repository) } //Initializes a NameViewModel instance using a NameViewModel factory that provides the repository
val swipedNameDao = database.swipedNameDao() //Retrieves the swipedNameDao from the database
val swipedNameRepository = SwipedNameRepository(swipedNameDao) //Creates an instance of SwipedNameRepository with the swipedNameDao
setContent { //Sets the content view of the activity using Jetpack Compose
ChildrenNamingTheme { //Applies a theme to the Compose UI.
val names by viewModel.allNames.observeAsState(emptyList()) //Observes allNames LiveData from the ViewModel and collects its state, to give an empty list as the initial state.
NamePagerScreen(names, currentUserId, swipedNameRepository) //Calls the composable function NamePagerScreen with the list of names.
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable //Marks this as a composable function
fun NamePagerScreen(names: List<Name>, currentUserId: Int, swipedNameRepository: SwipedNameRepository) { //Defines a composable function NamePagerScreen that takes a list of Name objects, currentUserId and SwipedNameRepository as a parameter.
val pagerState = rememberPagerState( //Creates a rememberPagerState instance to manage the pager state
initialPage = 0,// Sets the initial page to 0, shows first name in the table first
initialPageOffsetFraction = 0f // Sets the initial page offset fraction to 0
) {
names.size //Sets the total number of pages based on the size of the names lis
}
val swipedNameViewModel: SwipedNameViewModel = viewModel( //Creates a SwipedNameViewModel instance using the viewModel() function.
factory = SwipedNameViewModelFactory(swipedNameRepository) //Provides the SwipedNameRepository as a parameter to the factory.
)
Box(modifier = Modifier //Creates a box layout for swipe detection
.fillMaxSize() //Fills the entire available space
.pointerInput(Unit) { //Enables pointer input for the box
detectHorizontalDragGestures { change, dragAmount -> //Detects horizontal drag gestures
val isRightSwipe = dragAmount > 0 //Determines if the swipe is to the right
val swipedName = SwipedName( //Creates a new SwipedName object with the provided parameters
userId = currentUserId,
nameId = names[pagerState.currentPage].nameId,
isRightSwipe = isRightSwipe //Sets the isRightSwipe property based on the swipe direction
)
swipedNameViewModel.insert(swipedName) //Inserts the swipedName into the database using the SwipedNameViewModel
change.consume() //Consumes the change event to prevent further processing
}
}
) {
HorizontalPager( //Creates a horizontal pager layout
state = pagerState //Creates a horizontal pager with the given state.
) { page ->
val name = names[page] //Gets the name value corresponding to the current page.
val background = when (name.gender) { //Sets the background resource based on the gender of the name
"Girl" -> R.drawable.pink //if gender is "Girl" sets background a pink.jpg
"Boy" -> R.drawable.blue //if gender is "Boy" sets background a pink.jpg
else -> R.drawable.default_background //else sets background a default_background.jpg
}
Box(modifier = Modifier.fillMaxSize()) {//box layout for Name and gender text
Image( //Displays background image
painter = painterResource(id = background), //sets background image with the specified painter resource based on gender
contentDescription = null,
contentScale = ContentScale.Crop,//crops scale of box
modifier = Modifier.fillMaxSize()
)
Box( //style of the box set to semi transparent black
modifier = Modifier //box modifier
.align(Alignment.Center)
.background(Color(0x80000000))
.fillMaxWidth()
.padding(16.dp)
) {
Text( //style for name and gender text
text = "${name.childFirstName} - ${name.gender}", //sets name and gender text
style = MaterialTheme.typography.headlineMedium,
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
}
Here is the data class for 'SwipeName'
@Entity(tableName = "swiped_names")
data class SwipedName(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val userId: Int,
val nameId: Int,
val isRightSwipe: Boolean
)
if any more information is needed for clarification, please let me know.
I have since tried moving the detectHorizontalDragGestures to attach it directly to the HorizontalPager, this did yield some results, as it did save the swipes correctly into the table, however, the other data such as the userID is wrong, it also broke the actual swiping between pages feature and is stuck on the initial page.
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.room.Room
import com.examples.childrennaming.ui.theme.ChildrenNamingTheme
import kotlinx.coroutines.Dispatchers
import kotlin.math.abs
class SwipeActivity : ComponentActivity() {
private lateinit var database: AppDatabase //Declares a private lateinit variable database of type AppDatabase for late state intialisation
override fun onCreate(savedInstanceState: Bundle?) { //onCreate method is called when the activity is first created to perform intial set up
super.onCreate(savedInstanceState)
val currentUserId = intent.getIntExtra("CURRENT_USER_ID", -1)
database = (application as MyApp).database //Initialises the database property using the application context
val nameDao = database.nameDao() //Retrieves the DAO (Data Access Object) from the database
val repository = NameRepository(nameDao) //Creates an instance of NameRepository with the DAO
val viewModel: NameViewModel by viewModels { NameViewModelFactory(repository) } //Initializes a NameViewModel instance using a NameViewModel factory that provides the repository
val swipedNameDao = database.swipedNameDao() //Retrieves the swipedNameDao from the database
val swipedNameRepository = SwipedNameRepository(swipedNameDao) //Creates an instance of SwipedNameRepository with the swipedNameDao
setContent { //Sets the content view of the activity using Jetpack Compose
ChildrenNamingTheme { //Applies a theme to the Compose UI.
val names by viewModel.allNames.observeAsState(emptyList()) //Observes allNames LiveData from the ViewModel and collects its state, to give an empty list as the initial state.
NamePagerScreen(names, currentUserId, swipedNameRepository) //Calls the composable function NamePagerScreen with the list of names.
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable //Marks this as a composable function
fun NamePagerScreen(names: List<Name>, currentUserId: Int, swipedNameRepository: SwipedNameRepository) { //Defines a composable function NamePagerScreen that takes a list of Name objects, currentUserId and SwipedNameRepository as a parameter.
val pagerState = rememberPagerState( //Creates a rememberPagerState instance to manage the pager state
initialPage = 0,// Sets the initial page to 0, shows first name in the table first
initialPageOffsetFraction = 0f // Sets the initial page offset fraction to 0
) {
names.size //Sets the total number of pages based on the size of the names lis
}
val swipedNameViewModel: SwipedNameViewModel = viewModel( //Creates a SwipedNameViewModel instance using the viewModel() function.
factory = SwipedNameViewModelFactory(swipedNameRepository) //Provides the SwipedNameRepository as a parameter to the factory.
)
HorizontalPager(state = pagerState) { page ->
val name = names[page]
val background = when (name.gender) {
"Girl" -> R.drawable.pink
"Boy" -> R.drawable.blue
else -> R.drawable.default_background
}
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectHorizontalDragGestures { change, dragAmount ->
val isSignificantSwipe = abs(dragAmount) > 16 // Define a threshold
if (isSignificantSwipe) {
val isRightSwipe = dragAmount > 0
val swipedName = SwipedName(
userId = currentUserId,
nameId = name.nameId,
isRightSwipe = isRightSwipe
)
Log.d("SwipeActivity", "Detected swipe: isRightSwipe=$isRightSwipe, swipedName=$swipedName")
swipedNameViewModel.insert(swipedName)
change.consume() // Consume for significant swipes
}
}
}
) {
Image(
painter = painterResource(id = background),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
Box(
modifier = Modifier
.align(Alignment.Center)
.background(Color(0x80000000))
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "${name.childFirstName} - ${name.gender}",
style = MaterialTheme.typography.headlineMedium,
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
You don't need additional drag
gesture to check if user swiped in any direction. You can use pagerState. settledPage
to check slide events inside Launchedeffect
with snapshotFlow
as
val pagerState = rememberPagerState {
// page count
}
var userScrolled by remember {
mutableStateOf(false)
}
LaunchedEffect(pagerState.isScrollInProgress) {
if (pagerState.isScrollInProgress) {
userScrolled = true
}
}
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.settledPage }.collect {
if (pagerState.targetPage == pagerState.settledPage && userScrolled) {
println("Swiped to ${pagerState.currentPage}")
}
}
}
Or like this if you consider swiping completed even gesture hasn't finished yet. User might half scroll to from page 0 to page 1 and not swipe back to 0 and it would still count as swiped to page 1 with this alternative. First one is more reliable because it expect animation to finish target to be equal to settle page and since both are equal at the beginning you need to check if user ever swiped or reset it if needed.
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }.collect {
if (pagerState.currentPage != pagerState.settledPage) {
println("Swiped to ${pagerState.currentPage}")
}
}
}