Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/src/main/java/com/yapp/twix/di/InitKoin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.yapp.twix.di

import android.content.Context
import com.twix.network.di.networkModule
import com.twix.ui.di.uiModule
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import org.koin.core.module.Module
Expand All @@ -18,6 +19,7 @@ fun initKoin(
addAll(extraModules)
addAll(featureModules)
addAll(networkModule)
add(uiModule)
},
)
}
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/com/yapp/twix/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@ import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.twix.designsystem.theme.TwixTheme
import com.twix.navigation.AppNavHost
import com.twix.ui.toast.ToastHost
import com.twix.ui.toast.ToastManager
import org.koin.android.ext.android.inject

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
val toastManager by inject<ToastManager>()

TwixTheme {
Box(
modifier =
Expand All @@ -23,6 +29,13 @@ class MainActivity : ComponentActivity() {
.fillMaxSize(),
) {
AppNavHost()

ToastHost(
toastManager = toastManager,
modifier =
Modifier
.align(Alignment.BottomCenter),
)
}
}
}
Expand Down
17 changes: 17 additions & 0 deletions core/design-system/src/main/res/drawable/ic_toast_delete.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:strokeWidth="1"
android:pathData="M12,12m-9.5,0a9.5,9.5 0,1 1,19 0a9.5,9.5 0,1 1,-19 0"
android:fillColor="#FF6363"
android:strokeColor="#171717"/>
<path
android:pathData="M8,12H16"
android:strokeWidth="1.6"
android:fillColor="#00000000"
android:strokeColor="#262423"
android:strokeLineCap="round"/>
</vector>
11 changes: 11 additions & 0 deletions core/design-system/src/main/res/drawable/ic_toast_heart.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:strokeWidth="1"
android:pathData="M12.503,6.386C14.208,3.8 16.632,3.058 18.721,3.737C20.958,4.465 22.647,6.779 22.49,9.752C22.371,12.006 20.768,14.341 18.843,16.241C16.902,18.157 14.528,19.738 12.684,20.465L12.5,20.537L12.316,20.465C10.472,19.738 8.098,18.157 6.157,16.241C4.232,14.341 2.629,12.007 2.51,9.752C2.353,6.777 4.057,4.464 6.299,3.737C8.392,3.059 10.817,3.799 12.503,6.386Z"
android:fillColor="#FF6363"
android:strokeColor="#171717"/>
</vector>
18 changes: 18 additions & 0 deletions core/design-system/src/main/res/drawable/ic_toast_success.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:strokeWidth="1"
android:pathData="M12,12m-9.5,0a9.5,9.5 0,1 1,19 0a9.5,9.5 0,1 1,-19 0"
android:fillColor="#1ED45A"
android:strokeColor="#171717"/>
<path
android:pathData="M8.188,12.107L10.883,15.012L15.81,10.004"
android:strokeLineJoin="round"
android:strokeWidth="1.6"
android:fillColor="#00000000"
android:strokeColor="#262423"
android:strokeLineCap="round"/>
</vector>
14 changes: 14 additions & 0 deletions core/design-system/src/main/res/drawable/ic_toast_warning.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:strokeWidth="1"
android:pathData="M12,12m-9.5,0a9.5,9.5 0,1 1,19 0a9.5,9.5 0,1 1,-19 0"
android:fillColor="#FF6363"
android:strokeColor="#171717"/>
<path
android:pathData="M12.002,14.668C12.499,14.668 12.901,15.071 12.901,15.568C12.901,16.065 12.499,16.468 12.002,16.468C11.505,16.468 11.102,16.065 11.102,15.568C11.102,15.071 11.505,14.668 12.002,14.668ZM12.005,7.535C12.447,7.535 12.805,7.893 12.805,8.335V12.46C12.805,12.902 12.447,13.26 12.005,13.26C11.563,13.26 11.205,12.902 11.205,12.46V8.335C11.205,7.893 11.563,7.535 12.005,7.535Z"
android:fillColor="#262423"/>
</vector>
2 changes: 2 additions & 0 deletions core/ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.twix.android.library)
alias(libs.plugins.twix.android.compose)
alias(libs.plugins.twix.kermit)
alias(libs.plugins.twix.koin)
}

android {
Expand All @@ -10,4 +11,5 @@ android {

dependencies {
implementation(projects.core.designSystem)
implementation(projects.domain)
}
9 changes: 9 additions & 0 deletions core/ui/src/main/java/com/twix/ui/di/UIModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.twix.ui.di

import com.twix.ui.toast.ToastManager
import org.koin.dsl.module

val uiModule =
module {
single { ToastManager() }
}
191 changes: 191 additions & 0 deletions core/ui/src/main/java/com/twix/ui/toast/ToastHost.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package com.twix.ui.toast

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.twix.designsystem.R
import com.twix.designsystem.components.text.AppText
import com.twix.designsystem.theme.CommonColor
import com.twix.designsystem.theme.GrayColor
import com.twix.domain.model.enums.AppTextStyle
import com.twix.ui.extension.noRippleClickable
import com.twix.ui.toast.model.ToastAction
import com.twix.ui.toast.model.ToastData
import com.twix.ui.toast.model.ToastType
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch

@Composable
fun ToastHost(
toastManager: ToastManager,
modifier: Modifier = Modifier,
bottomPadding: Dp = 80.dp,
) {
var current by remember { mutableStateOf<ToastData?>(null) }
var visible by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
var dismissJob by remember { mutableStateOf<Job?>(null) }
val animationMs = 200

// 가장 최신 토스트만을 렌더링하기 위해서 collectLatest를 사용
LaunchedEffect(toastManager) {
toastManager.toasts.collectLatest { toast ->
dismissJob?.cancel()
current = toast
visible = true

dismissJob =
scope.launch {
delay(toast.durationMillis)
visible = false
delay(animationMs.toLong())
if (current == toast) current = null
}
}
}

// 이 메서드는 보러가기 버튼을 클릭했을 때 토스트가 바로 사라지도록 처리하기 위해서 추가
fun dismiss() {
dismissJob?.cancel()
visible = false
scope.launch {
delay(animationMs.toLong())
current = null
}
}

Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter,
) {
AnimatedVisibility(
visible = visible && current != null,
enter =
slideInVertically(
initialOffsetY = { it },
animationSpec = tween(animationMs),
) + fadeIn(tween(animationMs)),
exit =
slideOutVertically(
targetOffsetY = { it },
animationSpec = tween(animationMs),
) + fadeOut(tween(animationMs)),
) {
current?.let { toast ->
ToastItem(
data = toast,
bottomPadding = bottomPadding,
onDismiss = ::dismiss,
)
}
}
}
}

@Composable
private fun ToastItem(
data: ToastData,
bottomPadding: Dp,
onDismiss: () -> Unit,
) {
val res =
when (data.type) {
ToastType.SUCCESS -> painterResource(R.drawable.ic_toast_success)
ToastType.DELETE -> painterResource(R.drawable.ic_toast_delete)
ToastType.LIKE -> painterResource(R.drawable.ic_toast_heart)
ToastType.ERROR -> painterResource(R.drawable.ic_toast_warning)
}

Surface(
modifier =
Modifier
.padding(horizontal = 16.dp)
.padding(bottom = bottomPadding)
.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
border = BorderStroke(1.dp, GrayColor.C500),
color = GrayColor.C400,
) {
Row(
modifier =
Modifier
.padding(vertical = 12.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = res,
contentDescription = "toast icon",
modifier = Modifier.size(24.dp),
)

Spacer(Modifier.width(3.5.dp))

AppText(
text = data.message,
style = AppTextStyle.B1,
color = CommonColor.White,
modifier = Modifier.weight(1f),
textAlign = TextAlign.Start,
)

data.action?.let {
ActionButton(it, onDismiss)
}
}
}
}

@Composable
private fun ActionButton(
action: ToastAction,
onDismiss: () -> Unit,
) {
AppText(
text = action.label,
style = AppTextStyle.B1,
color = CommonColor.White,
modifier =
Modifier
.background(GrayColor.C300, RoundedCornerShape(8.dp))
.border(1.dp, GrayColor.C500, RoundedCornerShape(8.dp))
.padding(vertical = 5.5.dp, horizontal = 12.dp)
.noRippleClickable(
onClick = {
action.onClick()
onDismiss()
},
),
)
}
28 changes: 28 additions & 0 deletions core/ui/src/main/java/com/twix/ui/toast/ToastManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.twix.ui.toast

import com.twix.ui.toast.model.ToastData
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow

class ToastManager {
private val _toasts =
MutableSharedFlow<ToastData>(
replay = 0,
extraBufferCapacity = 16, // 최대 16개까지 토스트를 버퍼에 저장
onBufferOverflow = BufferOverflow.DROP_OLDEST, // 버퍼 가득차면 가장 오래된 것을 버림
)
val toasts: SharedFlow<ToastData> = _toasts

/**
* UX상으로 토스트가 절대 유실되어서는 안되는 경우에는 show를 쓰면 됨.
* */
suspend fun show(data: ToastData) {
_toasts.emit(data)
}

/**
* 대부분의 경우에는 tryShow를 쓰면 됨.
* */
fun tryShow(data: ToastData): Boolean = _toasts.tryEmit(data)
}
9 changes: 9 additions & 0 deletions core/ui/src/main/java/com/twix/ui/toast/model/ToastAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.twix.ui.toast.model

import androidx.compose.runtime.Immutable

@Immutable
data class ToastAction(
val label: String,
val onClick: () -> Unit,
)
11 changes: 11 additions & 0 deletions core/ui/src/main/java/com/twix/ui/toast/model/ToastData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.twix.ui.toast.model

import androidx.compose.runtime.Immutable

@Immutable
data class ToastData(
val message: String,
val type: ToastType,
val durationMillis: Long = 2_000L,
val action: ToastAction? = null,
)
11 changes: 11 additions & 0 deletions core/ui/src/main/java/com/twix/ui/toast/model/ToastType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.twix.ui.toast.model

import androidx.compose.runtime.Immutable

@Immutable
enum class ToastType {
SUCCESS,
DELETE,
LIKE,
ERROR,
}
Loading