티스토리 뷰

728x90

일반 이벤트

package net.pilseong.todocompose.ui.screen.task

import android.annotation.SuppressLint
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import net.pilseong.todocompose.R
import net.pilseong.todocompose.ui.viewmodel.SharedViewModel
import net.pilseong.todocompose.util.Action

@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun TaskScreen(
    taskId: Int,
    sharedViewModel: SharedViewModel,
    toListScreen: (Action, Int) -> Unit,
) {
    val context = LocalContext.current
    val taskScreenState = sharedViewModel.taskScreenState

    // list screen 에서 toTaskScreen 이 실행된 경우 taskId 를 받아서 DB 에서 검색한 후 보여 준다.
    // 아래의 getSearchedTask 는 coroutine 에서 실행 된다.
    LaunchedEffect(key1 = taskId) {
        Log.i("PHILIP", "[TaskScreen] getSearchedTask with $taskId")
        sharedViewModel.getSearchedTask(taskId)
    }

    val selectedTask = sharedViewModel.selectedTask
    Log.i("PHILIP", "[TaskScreen] selectedTask $selectedTask")

    // LaunchedEffect 가 없으면 마지막 저장 후 disposal 시 에 taskId 가 -1 인 경우가 실행 되어 모두 비어짐
    // 읽어 온 task 를 메모 내에 반영 하기 위한 구문들
    LaunchedEffect(key1 = selectedTask) {
        if (selectedTask != null || taskId == -1) {
            Log.i("PHILIP", "[TaskScreen] selected task updated ${selectedTask.toString()}")
            sharedViewModel.updateTaskContent(selectedTask)
        }
    }

    // 뒤로 가기 버튼에 대한 가로 채기 및 처리
//    BackHandler(onBackPressed = { toListScreen(Action.NO_ACTION) })
    BackHandler {
        toListScreen(Action.NO_ACTION, taskId)
    }

    val emptyTitleString = stringResource(id = R.string.empty_title_popup)
    val emptyDescriptionString = stringResource(id = R.string.empty_description_popup)

    Scaffold(
        topBar = {
            TaskAppBar(
                taskScreenState = taskScreenState,
                todoTask = selectedTask,
                toListScreen = { action ->
                    // 수정 할 내용을 반영 해야 할 경우 title, description 이 비어 있는지 확인
                    if (action != Action.NO_ACTION) {
                        if (sharedViewModel.title.isEmpty()) {
                            Toast.makeText(
                                context,
                                emptyTitleString,
                                Toast.LENGTH_SHORT
                            ).show()
                        } else if (sharedViewModel.description.isEmpty()) {
                            Toast.makeText(
                                context,
                                emptyDescriptionString,
                                Toast.LENGTH_SHORT
                            ).show()
                        } else {
                            toListScreen(action, taskId)
                        }
                    } else {
                        toListScreen(action, taskId)
                    }
                }
            )
        },
        content = { paddingValues ->
            Column(
                modifier = Modifier
                    .padding(top = paddingValues.calculateTopPadding())
                    .fillMaxSize()
            ) {
                TaskContent(
                    taskScreenState = taskScreenState,
                    title = sharedViewModel.title,
                    description = sharedViewModel.description,
                    priority = sharedViewModel.priority,
                    createdAt = sharedViewModel.createdAt,
                    updatedAt = sharedViewModel.updatedAt,
                    onTitleChange = { title ->
                        sharedViewModel.updateTitle(title)
                    },
                    onDescriptionChange = { description ->
                        sharedViewModel.description = description
                    },
                    onPriorityChange = { priority ->
                        sharedViewModel.priority = priority
                    }
                )
            }
        }
    )
}

//@Composable
//fun BackHandler(
//    backDispatcher: OnBackPressedDispatcher? =
//        LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher,
//    onBackPressed: () -> Unit
//) {
//    val currentOnBackPressed by rememberUpdatedState(newValue = onBackPressed)
//
//    val backCallback = remember {
//        object : OnBackPressedCallback(true) {
//            override fun handleOnBackPressed() {
//                currentOnBackPressed()
//            }
//        }
//    }
//    DisposableEffect(key1 = backDispatcher) {
//        backDispatcher?.addCallback(backCallback)
//
//        onDispose {
//            backCallback.remove()
//        }
//    }
//
//}

 

swipeToDismiss로 구현

 

package net.pilseong.todocompose.ui.screen.task

import android.annotation.SuppressLint
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material3.DismissDirection
import androidx.compose.material3.DismissValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import net.pilseong.todocompose.R
import net.pilseong.todocompose.data.model.TodoTask
import net.pilseong.todocompose.ui.screen.list.ColorBackGround
import net.pilseong.todocompose.ui.viewmodel.SharedViewModel
import net.pilseong.todocompose.util.Action
import net.pilseong.todocompose.util.RequestState
import net.pilseong.todocompose.util.TaskScreenState

@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun TaskScreen(
//    taskId: Int,
    sharedViewModel: SharedViewModel,
    toListScreen: (Action, Int) -> Unit,
) {
    val taskScreenState = sharedViewModel.taskScreenState
    val index = sharedViewModel.index
    Log.i("PHILIP", "[TaskScreen] index is $index")
    // Flow 에 대한 collection 을 처리 하는 파이프 연결 변수들. 이 변수들 은 외부 데이터 베이스 나 외부 API 에 의존 한다.
    // 모든 task 의 상태를 감시 한다. 리스트 는 nav graph 안에서 변동 될 수 있다.
    val allTasks by sharedViewModel.allTasks.collectAsState()
    val searchedTasks by sharedViewModel.searchedTasks.collectAsState()
    val prioritySortState by sharedViewModel.prioritySortState.collectAsState()
    val lowPriorityTasks by sharedViewModel.lowPriorityTasks.collectAsState()
    val highPriorityTasks by sharedViewModel.highPriorityTasks.collectAsState()

    when (allTasks) {
        is RequestState.Success -> {
            MainScreen(
                taskScreenState = taskScreenState,
                tasks = (allTasks as RequestState.Success<List<TodoTask>>).data,
//                taskId = taskId,
                taskIndex = index,
                sharedViewModel = sharedViewModel,
                toListScreen = toListScreen
            )
        }

        else -> {

        }
    }
}


@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun MainScreen(
    taskScreenState: TaskScreenState,
    tasks: List<TodoTask>,
//    taskId: Int,
    taskIndex: Int,
    sharedViewModel: SharedViewModel,
    toListScreen: (Action, Int) -> Unit,
) {

    Log.i("PHILIP", "[MainScreen] size of tasks ${tasks.size}")
    val context = LocalContext.current

    val selectedTask = tasks[taskIndex]
    Log.i("PHILIP", "[TaskScreen] selectedTask $selectedTask")

    // LaunchedEffect 가 없으면 마지막 저장 후 disposal 시 에 taskId 가 -1 인 경우가 실행 되어 모두 비어짐
    // 읽어 온 task 를 메모 내에 반영 하기 위한 구문들
//    LaunchedEffect(key1 = selectedTask) {
    if (selectedTask != null) { //|| taskId == -1) {
        Log.i("PHILIP", "[TaskScreen] selected task updated ${selectedTask.toString()}")
        sharedViewModel.updateTaskContent(selectedTask)
    }
//    }

    // 뒤로 가기 버튼에 대한 가로 채기 및 처리
//    BackHandler(onBackPressed = { toListScreen(Action.NO_ACTION) })
    BackHandler {
        toListScreen(Action.NO_ACTION, taskIndex)
    }

    val emptyTitleString = stringResource(id = R.string.empty_title_popup)
    val emptyDescriptionString = stringResource(id = R.string.empty_description_popup)

    Scaffold(
        topBar = {
            TaskAppBar(
                taskScreenState = taskScreenState,
                todoTask = selectedTask,
                toListScreen = { action ->
                    // 수정 할 내용을 반영 해야 할 경우 title, description 이 비어 있는지 확인
                    if (action != Action.NO_ACTION) {
                        if (sharedViewModel.title.isEmpty()) {
                            Toast.makeText(
                                context,
                                emptyTitleString,
                                Toast.LENGTH_SHORT
                            ).show()
                        } else if (sharedViewModel.description.isEmpty()) {
                            Toast.makeText(
                                context,
                                emptyDescriptionString,
                                Toast.LENGTH_SHORT
                            ).show()
                        } else {
                            toListScreen(action, taskIndex)
                        }
                    } else {
                        toListScreen(action, taskIndex)
                    }
                }
            )
        },
        content = { paddingValues ->
            // 화면 전환의 기준 점 계산 화면의 3분의 1이상 swipe 할 경우 전환
            val threshold = LocalConfiguration.current.screenWidthDp / 3

            val dismissState = rememberDismissState(
                confirmValueChange = {
                    when (it) {
                        DismissValue.Default -> false
                        DismissValue.DismissedToEnd -> {
                            sharedViewModel.decrementIndex()
                            true
                        }

                        DismissValue.DismissedToStart -> {
                            sharedViewModel.incrementIndex()
                            true
                        }
                    }
                },
                positionalThreshold = { threshold.dp.toPx() }
            )

            // index 의 이동이 일어난 경우 실행 된다. 동일한 인덱스 로 이동 하는 경우는 없기 때문에
            // 중복 이벤트 발생에 대한 대처를 할 필요가 없다.
            // index 가 변경 된 상태 변경이 확인 되는 경우에 실행 된다.
            LaunchedEffect(key1 = sharedViewModel.index) {
                if (dismissState.dismissDirection == DismissDirection.StartToEnd) {
                    dismissState.snapTo(DismissValue.DismissedToStart)
                } else {
                    dismissState.snapTo(DismissValue.DismissedToEnd)
                }
                dismissState.reset()
            }
            Column(
                modifier = Modifier
                    .padding(top = paddingValues.calculateTopPadding())
                    .fillMaxSize()
            ) {
                val currentItem by rememberUpdatedState(newValue = selectedTask)

                SwipeToDismiss(
                    state = dismissState,
                    background = {
                        ColorBackGround(
                            dismissState = dismissState,
                            leftToRightColor = MaterialTheme.colorScheme.surface,
                            rightToLeftColor = MaterialTheme.colorScheme.surface,
                            leftIcon = Icons.Default.KeyboardArrowLeft,
                            rightIcon = Icons.Default.KeyboardArrowRight
                        )
                    },
                    dismissContent = {
                        TaskContent(
                            taskScreenState = taskScreenState,
                            title = currentItem.title,
                            description = currentItem.description,
                            priority = currentItem.priority,
                            createdAt = currentItem.createdAt,
                            updatedAt = currentItem.updatedAt,
                            onTitleChange = { title ->
                                sharedViewModel.updateTitle(title)
                            },
                            onDescriptionChange = { description ->
                                sharedViewModel.description = description
                            },
                            onPriorityChange = { priority ->
                                sharedViewModel.priority = priority
                            }
                        )
                    },
//                    directions = setOf(DismissDirection.StartToEnd)
                )

            }
        }
    )
}


//@Composable
//fun BackHandler(
//    backDispatcher: OnBackPressedDispatcher? =
//        LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher,
//    onBackPressed: () -> Unit
//) {
//    val currentOnBackPressed by rememberUpdatedState(newValue = onBackPressed)
//
//    val backCallback = remember {
//        object : OnBackPressedCallback(true) {
//            override fun handleOnBackPressed() {
//                currentOnBackPressed()
//            }
//        }
//    }
//    DisposableEffect(key1 = backDispatcher) {
//        backDispatcher?.addCallback(backCallback)
//
//        onDispose {
//            backCallback.remove()
//        }
//    }
//
//}
@HiltViewModel
class SharedViewModel @Inject constructor(
    private val todoRepository: TodoRepository,
    private val dataStoreRepository: DataStoreRepository
) : ViewModel() {

    var index by mutableStateOf(0)
        private set

    fun updateIndex(index: Int) {
        this.index = index
    }

    // 화면 인덱스 이동 - delay 를 준 것은 swipeToDismiss 에서 swipe animation 구동 시에
    // 전환 된 화면이 화면에 표출 되는 것을 막기 위함
    fun incrementIndex() {
        viewModelScope.launch {
            delay(100)
            index++
            Log.i("PHILIP", "[SharedViewModel] incrementIndex $index")
        }
    }

    fun decrementIndex() {
        viewModelScope.launch {
            delay(300)
            index--
            Log.i("PHILIP", "[SharedViewModel] decrementIndex $index")
        }
    }
728x90
댓글