diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f84666b..53d4876 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(projects.data) implementation(projects.domain) implementation(projects.feature.login) + implementation(projects.feature.main) // Firebase implementation(platform(libs.google.firebase.bom)) diff --git a/app/src/main/java/com/yapp/twix/di/FeatureModules.kt b/app/src/main/java/com/yapp/twix/di/FeatureModules.kt index 2636772..7365a49 100644 --- a/app/src/main/java/com/yapp/twix/di/FeatureModules.kt +++ b/app/src/main/java/com/yapp/twix/di/FeatureModules.kt @@ -1,9 +1,13 @@ package com.yapp.twix.di +import com.twix.home.di.homeModule import com.twix.login.di.loginModule +import com.twix.main.di.mainModule import org.koin.core.module.Module val featureModules: List = listOf( loginModule, + mainModule, + homeModule, ) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 289fe7d..8460246 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -51,5 +51,9 @@ gradlePlugin { id = "twix.data" implementationClass = "com.twix.convention.DataConventionPlugin" } + register("kermit") { + id = "twix.kermit" + implementationClass = "com.twix.convention.KermitConventionPlugin" + } } } diff --git a/build-logic/convention/src/main/kotlin/com/twix/convention/FeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/com/twix/convention/FeatureConventionPlugin.kt index fb110ff..8bf351b 100644 --- a/build-logic/convention/src/main/kotlin/com/twix/convention/FeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/com/twix/convention/FeatureConventionPlugin.kt @@ -8,6 +8,7 @@ class FeatureConventionPlugin : BuildLogicConventionPlugin({ apply() apply() apply() + apply< KermitConventionPlugin>() dependencies { implementation(project(":core:analytics")) diff --git a/build-logic/convention/src/main/kotlin/com/twix/convention/KermitConventionPlugin.kt b/build-logic/convention/src/main/kotlin/com/twix/convention/KermitConventionPlugin.kt new file mode 100644 index 0000000..454c3bc --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/twix/convention/KermitConventionPlugin.kt @@ -0,0 +1,12 @@ +package com.twix.convention + +import com.twix.convention.extension.implementation +import com.twix.convention.extension.library +import com.twix.convention.extension.libs +import org.gradle.kotlin.dsl.dependencies + +class KermitConventionPlugin: BuildLogicConventionPlugin({ + dependencies { + implementation(libs.library("kermit")) + } +}) \ No newline at end of file diff --git a/core/design-system/src/main/res/drawable/ic_alert.xml b/core/design-system/src/main/res/drawable/ic_alert.xml new file mode 100644 index 0000000..945fe48 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_alert.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/design-system/src/main/res/drawable/ic_couple_selected.xml b/core/design-system/src/main/res/drawable/ic_couple_selected.xml new file mode 100644 index 0000000..1cfef09 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_couple_selected.xml @@ -0,0 +1,11 @@ + + + diff --git a/core/design-system/src/main/res/drawable/ic_couple_unselected.xml b/core/design-system/src/main/res/drawable/ic_couple_unselected.xml new file mode 100644 index 0000000..131fa13 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_couple_unselected.xml @@ -0,0 +1,11 @@ + + + diff --git a/core/design-system/src/main/res/drawable/ic_drop_down_arrow.xml b/core/design-system/src/main/res/drawable/ic_drop_down_arrow.xml new file mode 100644 index 0000000..8caeb18 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_drop_down_arrow.xml @@ -0,0 +1,13 @@ + + + diff --git a/core/design-system/src/main/res/drawable/ic_empty_face.xml b/core/design-system/src/main/res/drawable/ic_empty_face.xml new file mode 100644 index 0000000..a67495f --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_empty_face.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + diff --git a/core/design-system/src/main/res/drawable/ic_empty_goal_arrow.xml b/core/design-system/src/main/res/drawable/ic_empty_goal_arrow.xml new file mode 100644 index 0000000..865123a --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_empty_goal_arrow.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/design-system/src/main/res/drawable/ic_home_selected.xml b/core/design-system/src/main/res/drawable/ic_home_selected.xml new file mode 100644 index 0000000..b247a23 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_home_selected.xml @@ -0,0 +1,11 @@ + + + diff --git a/core/design-system/src/main/res/drawable/ic_home_unselected.xml b/core/design-system/src/main/res/drawable/ic_home_unselected.xml new file mode 100644 index 0000000..6979d2f --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_home_unselected.xml @@ -0,0 +1,11 @@ + + + diff --git a/core/design-system/src/main/res/drawable/ic_plus.xml b/core/design-system/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..1785a79 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/design-system/src/main/res/drawable/ic_refresh.xml b/core/design-system/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 0000000..1bf4d68 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,13 @@ + + + diff --git a/core/design-system/src/main/res/drawable/ic_setting.xml b/core/design-system/src/main/res/drawable/ic_setting.xml new file mode 100644 index 0000000..cd02394 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_setting.xml @@ -0,0 +1,16 @@ + + + + diff --git a/core/design-system/src/main/res/drawable/ic_stats_selected.xml b/core/design-system/src/main/res/drawable/ic_stats_selected.xml new file mode 100644 index 0000000..7726a1d --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_stats_selected.xml @@ -0,0 +1,15 @@ + + + + diff --git a/core/design-system/src/main/res/drawable/ic_stats_unselected.xml b/core/design-system/src/main/res/drawable/ic_stats_unselected.xml new file mode 100644 index 0000000..3befa41 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_stats_unselected.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml new file mode 100644 index 0000000..da500dd --- /dev/null +++ b/core/design-system/src/main/res/values/strings.xml @@ -0,0 +1,21 @@ + + + + + 통계 + 커플페이지 + 오늘 + + + + + + + + + + + 오늘 우리 목표 + 첫 목표를 세워볼까요? + + \ No newline at end of file diff --git a/core/navigation/src/main/java/com/twix/navigation/AppNavHost.kt b/core/navigation/src/main/java/com/twix/navigation/AppNavHost.kt index 6a20fb4..6502732 100644 --- a/core/navigation/src/main/java/com/twix/navigation/AppNavHost.kt +++ b/core/navigation/src/main/java/com/twix/navigation/AppNavHost.kt @@ -23,7 +23,7 @@ fun AppNavHost() { } val start = contributors - .firstOrNull { it.graphRoute == NavRoutes.LoginGraph } + .firstOrNull { it.graphRoute == NavRoutes.MainGraph } ?.graphRoute ?: error("해당 Graph를 찾을 수 없습니다.") val duration = 300 diff --git a/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt b/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt index 5a10871..625bc78 100644 --- a/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt +++ b/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt @@ -10,15 +10,17 @@ package com.twix.navigation sealed class NavRoutes( val route: String, ) { + /** + * LoginGraph + * */ object LoginGraph : NavRoutes("login_graph") - object Login : NavRoutes("login") + object LoginRoute : NavRoutes("login") - object HomeGraph : NavRoutes("home_graph") + /** + * MainGraph + * */ + object MainGraph : NavRoutes("main_graph") - object Home : NavRoutes("home") - - object HomeDetail : NavRoutes("home_detail/{id}") { - fun createRoute(id: String) = "home_detail/$id" - } + object MainRoute : NavRoutes("main") } diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 2f73a37..73482c8 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.twix.android.library) alias(libs.plugins.twix.android.compose) + alias(libs.plugins.twix.kermit) } android { diff --git a/core/ui/src/main/java/com/twix/ui/base/BaseViewModel.kt b/core/ui/src/main/java/com/twix/ui/base/BaseViewModel.kt new file mode 100644 index 0000000..3bc9fd5 --- /dev/null +++ b/core/ui/src/main/java/com/twix/ui/base/BaseViewModel.kt @@ -0,0 +1,116 @@ +package com.twix.ui.base + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import co.touchlab.kermit.Logger +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException + +abstract class BaseViewModel( + initialState: S, +) : ViewModel() { + protected open val logger: Logger = + Logger.withTag(this::class.simpleName ?: "BaseViewModel") + + // State + private val stateHolder = StateHolder(initialState) + val uiState: StateFlow = stateHolder.state + protected val currentState: S get() = stateHolder.current + + // SideEffect + private val sideEffectHolder = SideEffectHolder() + val sideEffect: Flow = sideEffectHolder.flow + + // Intent + private val intentChannel = Channel(Channel.BUFFERED) + + init { + // Intent 순차 처리 + viewModelScope.launch { + intentChannel.receiveAsFlow().collect { intent -> + try { + handleIntent(intent) + } catch (t: Throwable) { + if (t is CancellationException) throw t + handleError(t) + } + } + } + } + + /** + * UI에서 Intent를 발생시키는 메서드 + * */ + fun dispatch(intent: I) { + val result = intentChannel.trySend(intent) + if (result.isFailure) { + logger.w { "이벤트 유실: $intent, 원인 = ${result.exceptionOrNull()}" } + } + } + + /** + * State를 변경하는 메서드 + * */ + protected fun reduce(reducer: S.() -> S) { + stateHolder.reduce(reducer) + } + + /** + * SideEffect를 발생시키는 메서드 + * */ + protected suspend fun emitSideEffect(effect: SE) { + sideEffectHolder.emit(effect) + } + + /** + * Intent를 처리하는 메서드 + * */ + protected abstract suspend fun handleIntent(intent: I) + + /** + * 서버 통신 메서드 호출 및 응답을 처리하는 헬퍼 메서드 + * */ + protected fun launchResult( + onStart: (() -> Unit)? = null, // 비동기 시작 전 처리해야 할 로직 ex) 로딩 + onFinally: (() -> Unit)? = null, // 비동기 종료 후 리소스 정리 + onSuccess: (D) -> Unit, // 비동기 메서드 호출이 성공했을 때 처리해야 할 로직 + onError: ((Throwable) -> Unit)? = null, // 비동기 메서드 호출에 실패했을 때 처리해야 할 로직 + block: suspend () -> Result, // 비동기 메서드 ex) 서버 통신 메서드 + ): Job = + viewModelScope.launch { + try { + onStart?.invoke() + + val result = block.invoke() + result.fold( + onSuccess = { data -> onSuccess(data) }, + onFailure = { t -> + if (t is CancellationException) throw t + + handleError(t) + onError?.invoke(t) + }, + ) + } finally { + onFinally?.invoke() + } + } + + /** + * 에러 핸들링 메서드 + * */ + protected open fun handleError(t: Throwable) { + logger.e { "에러 발생: ${t.stackTraceToString()}" } + } + + // 리소스 정리 + override fun onCleared() { + intentChannel.close() + super.onCleared() + } +} diff --git a/core/ui/src/main/java/com/twix/ui/base/Intent.kt b/core/ui/src/main/java/com/twix/ui/base/Intent.kt new file mode 100644 index 0000000..a8d758d --- /dev/null +++ b/core/ui/src/main/java/com/twix/ui/base/Intent.kt @@ -0,0 +1,3 @@ +package com.twix.ui.base + +interface Intent diff --git a/core/ui/src/main/java/com/twix/ui/base/NoSideEffect.kt b/core/ui/src/main/java/com/twix/ui/base/NoSideEffect.kt new file mode 100644 index 0000000..2165c2e --- /dev/null +++ b/core/ui/src/main/java/com/twix/ui/base/NoSideEffect.kt @@ -0,0 +1,3 @@ +package com.twix.ui.base + +data object NoSideEffect : SideEffect diff --git a/core/ui/src/main/java/com/twix/ui/extension/NoRippleClickable.kt b/core/ui/src/main/java/com/twix/ui/extension/NoRippleClickable.kt new file mode 100644 index 0000000..a6186e5 --- /dev/null +++ b/core/ui/src/main/java/com/twix/ui/extension/NoRippleClickable.kt @@ -0,0 +1,15 @@ +package com.twix.ui.extension + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier + +@Composable +fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier = + clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = onClick, + ) diff --git a/core/ui/src/main/java/com/twix/ui/extension/WeekStartSunday.kt b/core/ui/src/main/java/com/twix/ui/extension/WeekStartSunday.kt new file mode 100644 index 0000000..d6a5f61 --- /dev/null +++ b/core/ui/src/main/java/com/twix/ui/extension/WeekStartSunday.kt @@ -0,0 +1,10 @@ +package com.twix.ui.extension + +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.temporal.TemporalAdjusters + +/** + * 주 시작을 일요일로 맞추는 메서드 + */ +fun LocalDate.weekStartSunday(): LocalDate = this.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY)) diff --git a/domain/src/main/java/com/twix/domain/model/enums/WeekNavigation.kt b/domain/src/main/java/com/twix/domain/model/enums/WeekNavigation.kt new file mode 100644 index 0000000..d8fb8ea --- /dev/null +++ b/domain/src/main/java/com/twix/domain/model/enums/WeekNavigation.kt @@ -0,0 +1,7 @@ +package com.twix.domain.model.enums + +enum class WeekNavigation { + NEXT, + PREVIOUS, + TODAY, +} diff --git a/feature/login/src/main/java/com/twix/login/navigation/LoginNavGraph.kt b/feature/login/src/main/java/com/twix/login/navigation/LoginNavGraph.kt index 5d0d2e7..2d609eb 100644 --- a/feature/login/src/main/java/com/twix/login/navigation/LoginNavGraph.kt +++ b/feature/login/src/main/java/com/twix/login/navigation/LoginNavGraph.kt @@ -12,14 +12,14 @@ object LoginNavGraph : NavGraphContributor { override val graphRoute: NavRoutes get() = NavRoutes.LoginGraph override val startDestination: String - get() = NavRoutes.Login.route + get() = NavRoutes.LoginRoute.route override fun NavGraphBuilder.registerGraph(navController: NavHostController) { navigation( route = graphRoute.route, startDestination = startDestination, ) { - composable(NavRoutes.Login.route) { + composable(NavRoutes.LoginRoute.route) { LoginScreen() } } diff --git a/feature/main/.gitignore b/feature/main/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/feature/main/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts new file mode 100644 index 0000000..049447f --- /dev/null +++ b/feature/main/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + alias(libs.plugins.twix.feature) +} + +android { + namespace = "com.twix.main" +} diff --git a/feature/main/consumer-rules.pro b/feature/main/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/feature/main/proguard-rules.pro b/feature/main/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/feature/main/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/main/src/main/AndroidManifest.xml b/feature/main/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/feature/main/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/main/src/main/java/com/twix/home/HomeIntent.kt b/feature/main/src/main/java/com/twix/home/HomeIntent.kt new file mode 100644 index 0000000..d41c316 --- /dev/null +++ b/feature/main/src/main/java/com/twix/home/HomeIntent.kt @@ -0,0 +1,20 @@ +package com.twix.home + +import com.twix.ui.base.Intent +import java.time.LocalDate + +sealed interface HomeIntent : Intent { + data class SelectDate( + val date: LocalDate, + ) : HomeIntent + + data object PreviousWeek : HomeIntent + + data object NextWeek : HomeIntent + + data object MoveToToday : HomeIntent + + data class UpdateVisibleDate( + val date: LocalDate, + ) : HomeIntent +} diff --git a/feature/main/src/main/java/com/twix/home/HomeScreen.kt b/feature/main/src/main/java/com/twix/home/HomeScreen.kt new file mode 100644 index 0000000..93a1b42 --- /dev/null +++ b/feature/main/src/main/java/com/twix/home/HomeScreen.kt @@ -0,0 +1,115 @@ +package com.twix.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.twix.designsystem.R +import com.twix.designsystem.theme.GrayColor +import com.twix.home.component.EmptyGoalGuide +import com.twix.home.component.HomeTopBar +import com.twix.home.component.WeeklyCalendar +import com.twix.home.model.HomeUiState +import com.twix.ui.extension.noRippleClickable +import org.koin.androidx.compose.koinViewModel +import java.time.LocalDate + +@Composable +fun HomeRoute(viewModel: HomeViewModel = koinViewModel()) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + HomeScreen( + uiState = uiState, + onSelectDate = { viewModel.dispatch(HomeIntent.SelectDate(it)) }, + onPreviousWeek = { viewModel.dispatch(HomeIntent.PreviousWeek) }, + onNextWeek = { viewModel.dispatch(HomeIntent.NextWeek) }, + onUpdateVisibleDate = { viewModel.dispatch(HomeIntent.UpdateVisibleDate(it)) }, + onMoveToToday = { viewModel.dispatch(HomeIntent.MoveToToday) }, + ) +} + +@Composable +fun HomeScreen( + uiState: HomeUiState, + onSelectDate: (LocalDate) -> Unit, + onPreviousWeek: () -> Unit, + onNextWeek: () -> Unit, + onUpdateVisibleDate: (LocalDate) -> Unit, + onMoveToToday: () -> Unit, +) { + Box( + modifier = + Modifier + .fillMaxSize(), + ) { + Column( + modifier = + Modifier + .fillMaxSize(), + ) { + HomeTopBar( + monthYearText = uiState.monthYear, + onNotificationClick = {}, + onSettingClick = {}, + onMoveToToday = onMoveToToday, + ) + + WeeklyCalendar( + selectedDate = uiState.selectedDate, + referenceDate = uiState.referenceDate, + onSelectDate = onSelectDate, + onPreviousWeek = onPreviousWeek, + onNextWeek = onNextWeek, + onUpdateVisibleDate = onUpdateVisibleDate, + ) + + Spacer(Modifier.height(12.dp)) + + EmptyGoalGuide(modifier = Modifier.weight(1f)) + } + + AddGoalButton( + modifier = + Modifier + .align(Alignment.BottomEnd) + .padding(bottom = 12.dp, end = 16.dp), + onClick = {}, + ) + } +} + +@Composable +private fun AddGoalButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Box( + modifier = + modifier + .size(56.dp) + .background(GrayColor.C500, CircleShape) + .noRippleClickable(onClick = onClick), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(R.drawable.ic_plus), + contentDescription = "add goal", + modifier = + Modifier + .size(40.dp), + ) + } +} diff --git a/feature/main/src/main/java/com/twix/home/HomeSideEffect.kt b/feature/main/src/main/java/com/twix/home/HomeSideEffect.kt new file mode 100644 index 0000000..5b1046d --- /dev/null +++ b/feature/main/src/main/java/com/twix/home/HomeSideEffect.kt @@ -0,0 +1,7 @@ +package com.twix.home + +import com.twix.ui.base.SideEffect + +interface HomeSideEffect : SideEffect { + data object ShowMonthPickerBottomSheet : HomeSideEffect +} diff --git a/feature/main/src/main/java/com/twix/home/HomeViewModel.kt b/feature/main/src/main/java/com/twix/home/HomeViewModel.kt new file mode 100644 index 0000000..4503fed --- /dev/null +++ b/feature/main/src/main/java/com/twix/home/HomeViewModel.kt @@ -0,0 +1,46 @@ +package com.twix.home + +import com.twix.domain.model.enums.WeekNavigation +import com.twix.home.model.HomeUiState +import com.twix.ui.base.BaseViewModel +import java.time.LocalDate + +class HomeViewModel : + BaseViewModel( + HomeUiState(), + ) { + override suspend fun handleIntent(intent: HomeIntent) { + when (intent) { + is HomeIntent.SelectDate -> updateDate(intent.date) + HomeIntent.NextWeek -> shiftWeek(WeekNavigation.NEXT) + HomeIntent.PreviousWeek -> shiftWeek(WeekNavigation.PREVIOUS) + HomeIntent.MoveToToday -> shiftWeek(WeekNavigation.TODAY) + is HomeIntent.UpdateVisibleDate -> updateVisibleDate(intent.date) + } + } + + private fun updateDate(date: LocalDate) { + if (currentState.selectedDate == date) return + + if (date.month != currentState.visibleDate.month) updateVisibleDate(date) + + reduce { copy(selectedDate = date) } + } + + private fun shiftWeek(action: WeekNavigation) { + val newReference = + when (action) { + WeekNavigation.NEXT -> currentState.referenceDate.plusWeeks(1) + WeekNavigation.PREVIOUS -> currentState.referenceDate.minusWeeks(1) + WeekNavigation.TODAY -> LocalDate.now() + } + if (currentState.referenceDate == newReference) return + reduce { copy(referenceDate = newReference) } + } + + private fun updateVisibleDate(date: LocalDate) { + if (currentState.visibleDate.month == date.month) return + + reduce { copy(visibleDate = date) } + } +} diff --git a/feature/main/src/main/java/com/twix/home/component/EmptyGoalGuide.kt b/feature/main/src/main/java/com/twix/home/component/EmptyGoalGuide.kt new file mode 100644 index 0000000..16b116c --- /dev/null +++ b/feature/main/src/main/java/com/twix/home/component/EmptyGoalGuide.kt @@ -0,0 +1,59 @@ +package com.twix.home.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.twix.designsystem.R +import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.theme.GrayColor +import com.twix.domain.model.enums.AppTextStyle + +@Composable +fun EmptyGoalGuide(modifier: Modifier = Modifier) { + Column( + modifier = + modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Image( + painter = painterResource(R.drawable.ic_empty_face), + contentDescription = "empty face", + modifier = + Modifier + .padding(horizontal = 9.dp, vertical = 6.dp) + .size(width = 34.dp, height = 40.dp), + ) + + Spacer(Modifier.height(10.dp)) + + AppText( + text = stringResource(R.string.home_empty_goal_guide), + style = AppTextStyle.T2, + color = GrayColor.C200, + ) + + Spacer(Modifier.height(12.dp)) + + Image( + painter = painterResource(R.drawable.ic_empty_goal_arrow), + contentDescription = "empty goal arrow", + modifier = + Modifier + .offset(x = 32.dp), + ) + } +} diff --git a/feature/main/src/main/java/com/twix/home/component/HomeTopBar.kt b/feature/main/src/main/java/com/twix/home/component/HomeTopBar.kt new file mode 100644 index 0000000..1b162ae --- /dev/null +++ b/feature/main/src/main/java/com/twix/home/component/HomeTopBar.kt @@ -0,0 +1,135 @@ +package com.twix.home.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.twix.designsystem.R +import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.theme.GrayColor +import com.twix.designsystem.theme.TwixTheme +import com.twix.domain.model.enums.AppTextStyle +import com.twix.ui.extension.noRippleClickable + +@Composable +fun HomeTopBar( + monthYearText: String, + onNotificationClick: () -> Unit, + onSettingClick: () -> Unit, + onMoveToToday: () -> Unit, +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 10.dp) + .padding(bottom = 10.dp), + verticalAlignment = Alignment.Bottom, + ) { + Column( + horizontalAlignment = Alignment.Start, + ) { + Spacer(Modifier.height(12.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + AppText( + text = monthYearText, + style = AppTextStyle.T3, + color = GrayColor.C400, + ) + + Image( + painter = painterResource(R.drawable.ic_drop_down_arrow), + contentDescription = "drop down arrow", + modifier = Modifier.size(20.dp), + colorFilter = ColorFilter.tint(GrayColor.C400), + ) + } + + Spacer(Modifier.height(4.dp)) + + Row( + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + AppText( + text = stringResource(R.string.home_today_goal), + style = AppTextStyle.H3, + color = GrayColor.C400, + ) + + Image( + painter = painterResource(R.drawable.ic_refresh), + contentDescription = "refresh", + modifier = Modifier.noRippleClickable(onClick = onMoveToToday), + ) + } + + Spacer(Modifier.height(2.dp)) + } + + Spacer(Modifier.weight(1f)) + + TopBarButton( + iconRes = R.drawable.ic_alert, + contentDescription = "notification", + onClick = onNotificationClick, + ) + + TopBarButton( + iconRes = R.drawable.ic_setting, + contentDescription = "setting", + onClick = onSettingClick, + ) + } +} + +@Composable +private fun TopBarButton( + @DrawableRes iconRes: Int, + contentDescription: String, + onClick: () -> Unit, +) { + Box( + modifier = + Modifier + .size(40.dp) + .noRippleClickable(onClick = onClick), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(id = iconRes), + contentDescription = contentDescription, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TwixTheme { + HomeTopBar( + monthYearText = "1월 22일", + {}, + {}, + {}, + ) + } +} diff --git a/feature/main/src/main/java/com/twix/home/component/WeeklyCalendar.kt b/feature/main/src/main/java/com/twix/home/component/WeeklyCalendar.kt new file mode 100644 index 0000000..7ce948a --- /dev/null +++ b/feature/main/src/main/java/com/twix/home/component/WeeklyCalendar.kt @@ -0,0 +1,146 @@ +package com.twix.home.component + +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.wrapContentSize +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.twix.designsystem.R +import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.theme.GrayColor +import com.twix.domain.model.enums.AppTextStyle +import com.twix.ui.extension.noRippleClickable +import com.twix.ui.extension.weekStartSunday +import java.time.LocalDate +import kotlin.math.abs + +@Composable +fun WeeklyCalendar( + selectedDate: LocalDate, + referenceDate: LocalDate, + onSelectDate: (LocalDate) -> Unit, + onPreviousWeek: () -> Unit, + onNextWeek: () -> Unit, + onUpdateVisibleDate: (LocalDate) -> Unit, +) { + val today = remember { LocalDate.now() } + val weekStart = remember(referenceDate) { referenceDate.weekStartSunday() } + val days = remember(weekStart) { (0..6).map { weekStart.plusDays(it.toLong()) } } + + val dayLabels = + listOf( + stringResource(R.string.word_sunday), + stringResource(R.string.word_monday), + stringResource(R.string.word_tuesday), + stringResource(R.string.word_wednesday), + stringResource(R.string.word_thursday), + stringResource(R.string.word_friday), + stringResource(R.string.word_saturday), + ) + // 스와이프 처리용 누적 드래그 + var dragSumPx by remember { mutableFloatStateOf(0f) } + + LaunchedEffect(days.first()) { + if (dragSumPx > 0) { + onUpdateVisibleDate(days.first()) + } else { + onUpdateVisibleDate(days.last()) + } + } + + Column( + modifier = + Modifier + .fillMaxWidth() + .pointerInput(Unit) { + detectHorizontalDragGestures( + onDragStart = { dragSumPx = 0f }, + onHorizontalDrag = { _, dragAmount -> + dragSumPx += dragAmount + }, + onDragEnd = { + if (abs(dragSumPx) < 120f) return@detectHorizontalDragGestures + + if (dragSumPx > 0f) onPreviousWeek() else onNextWeek() + }, + ) + }.padding(horizontal = 12.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + ) { + days.forEachIndexed { index, date -> + val header = if (date == today) stringResource(R.string.word_today) else dayLabels[index] + + WeekDayCell( + header = header, + dayOfMonth = date.dayOfMonth, + selected = date == selectedDate, + onClick = { onSelectDate(date) }, + modifier = Modifier.weight(1f), + ) + } + } + } +} + +@Composable +private fun WeekDayCell( + header: String, + dayOfMonth: Int, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier + .padding(vertical = 6.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppText( + text = header, + style = AppTextStyle.C1, + color = GrayColor.C300, + modifier = Modifier.padding(bottom = 10.dp), + ) + + Box( + modifier = + Modifier + .size(44.dp) + .then( + if (selected) Modifier.border(1.dp, GrayColor.C500, CircleShape) else Modifier, + ), + contentAlignment = Alignment.Center, + ) { + AppText( + text = dayOfMonth.toString(), + style = AppTextStyle.B1, + color = GrayColor.C400, + modifier = + Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center) + .noRippleClickable { onClick() }, + ) + } + } +} diff --git a/feature/main/src/main/java/com/twix/home/di/HomeModule.kt b/feature/main/src/main/java/com/twix/home/di/HomeModule.kt new file mode 100644 index 0000000..4ddd9c0 --- /dev/null +++ b/feature/main/src/main/java/com/twix/home/di/HomeModule.kt @@ -0,0 +1,10 @@ +package com.twix.home.di + +import com.twix.home.HomeViewModel +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val homeModule = + module { + viewModelOf(::HomeViewModel) + } diff --git a/feature/main/src/main/java/com/twix/home/model/DateItemUiModel.kt b/feature/main/src/main/java/com/twix/home/model/DateItemUiModel.kt new file mode 100644 index 0000000..24bf669 --- /dev/null +++ b/feature/main/src/main/java/com/twix/home/model/DateItemUiModel.kt @@ -0,0 +1,6 @@ +package com.twix.home.model + +data class DateItemUiModel( + val dayOfMonth: Int, + val isSelected: Boolean, +) diff --git a/feature/main/src/main/java/com/twix/home/model/HomeUiState.kt b/feature/main/src/main/java/com/twix/home/model/HomeUiState.kt new file mode 100644 index 0000000..0eab724 --- /dev/null +++ b/feature/main/src/main/java/com/twix/home/model/HomeUiState.kt @@ -0,0 +1,17 @@ +package com.twix.home.model + +import androidx.compose.runtime.Immutable +import com.twix.ui.base.State +import java.time.LocalDate +import java.time.YearMonth + +@Immutable +data class HomeUiState( + val month: YearMonth = YearMonth.now(), + val visibleDate: LocalDate = LocalDate.now(), // 홈 화면 상단에 존재하는 월, 년 텍스트를 위한 상태 변수 + val selectedDate: LocalDate = LocalDate.now(), + val referenceDate: LocalDate = LocalDate.now(), // 7일 달력을 생성하기 위한 레퍼런스 날짜 +) : State { + val monthYear: String + get() = "${visibleDate.month.value}월 ${visibleDate.year}" +} diff --git a/feature/main/src/main/java/com/twix/main/MainIntent.kt b/feature/main/src/main/java/com/twix/main/MainIntent.kt new file mode 100644 index 0000000..bcdb3b9 --- /dev/null +++ b/feature/main/src/main/java/com/twix/main/MainIntent.kt @@ -0,0 +1,10 @@ +package com.twix.main + +import com.twix.main.model.MainTab +import com.twix.ui.base.Intent + +sealed interface MainIntent : Intent { + data class SelectTab( + val tab: MainTab, + ) : MainIntent +} diff --git a/feature/main/src/main/java/com/twix/main/MainScreen.kt b/feature/main/src/main/java/com/twix/main/MainScreen.kt new file mode 100644 index 0000000..d29f456 --- /dev/null +++ b/feature/main/src/main/java/com/twix/main/MainScreen.kt @@ -0,0 +1,63 @@ +package com.twix.main + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.twix.designsystem.theme.CommonColor +import com.twix.home.HomeRoute +import com.twix.main.component.MainBottomBar +import com.twix.main.model.MainTab +import org.koin.androidx.compose.koinViewModel + +@Composable +fun MainRoute(viewModel: MainViewModel = koinViewModel()) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + MainScreen( + selectedTab = uiState.selectedTab, + onTabClick = { tab -> viewModel.dispatch(MainIntent.SelectTab(tab)) }, + content = { tab -> + when (tab) { + MainTab.HOME -> HomeRoute() + MainTab.STATS -> Box(modifier = Modifier.fillMaxSize()) + MainTab.COUPLE -> Box(modifier = Modifier.fillMaxSize()) + } + }, + ) +} + +@Composable +private fun MainScreen( + selectedTab: MainTab, + onTabClick: (MainTab) -> Unit, + content: @Composable (MainTab) -> Unit, +) { + Scaffold( + modifier = + Modifier + .fillMaxSize() + .background(CommonColor.White), + bottomBar = { + MainBottomBar( + selectedTab = selectedTab, + onTabClick = onTabClick, + ) + }, + ) { padding -> + Box( + modifier = + Modifier + .fillMaxSize() + .background(CommonColor.White) + .padding(padding), + ) { + content(selectedTab) + } + } +} diff --git a/feature/main/src/main/java/com/twix/main/MainViewModel.kt b/feature/main/src/main/java/com/twix/main/MainViewModel.kt new file mode 100644 index 0000000..9dd246d --- /dev/null +++ b/feature/main/src/main/java/com/twix/main/MainViewModel.kt @@ -0,0 +1,20 @@ +package com.twix.main + +import com.twix.main.model.MainTab +import com.twix.main.model.MainUiState +import com.twix.ui.base.BaseViewModel +import com.twix.ui.base.NoSideEffect + +class MainViewModel : BaseViewModel(MainUiState()) { + override suspend fun handleIntent(intent: MainIntent) { + when (intent) { + is MainIntent.SelectTab -> selectTab(intent.tab) + } + } + + private fun selectTab(tab: MainTab) { + if (currentState.selectedTab == tab) return + + reduce { copy(selectedTab = tab) } + } +} diff --git a/feature/main/src/main/java/com/twix/main/component/MainBottomBar.kt b/feature/main/src/main/java/com/twix/main/component/MainBottomBar.kt new file mode 100644 index 0000000..ee623d8 --- /dev/null +++ b/feature/main/src/main/java/com/twix/main/component/MainBottomBar.kt @@ -0,0 +1,95 @@ +package com.twix.main.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +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.main.model.MainTab + +@Composable +fun MainBottomBar( + selectedTab: MainTab, + onTabClick: (MainTab) -> Unit, +) { + Column { + HorizontalDivider(thickness = 1.dp, color = GrayColor.C100) + + Row( + modifier = + Modifier + .fillMaxWidth() + .height(58.dp) + .background(CommonColor.White), + verticalAlignment = Alignment.CenterVertically, + ) { + MainTab.entries.forEach { tab -> + MainBottomBarItem( + modifier = Modifier.weight(1f), + tab = tab, + selected = selectedTab == tab, + onClick = { onTabClick(tab) }, + ) + } + } + } +} + +@Composable +private fun MainBottomBarItem( + modifier: Modifier = Modifier, + tab: MainTab, + selected: Boolean, + onClick: () -> Unit, +) { + val haptic = LocalHapticFeedback.current + val interactionSource = remember { MutableInteractionSource() } + val icon = if (selected) tab.selectedIcon else tab.unselectedIcon + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(icon), + contentDescription = null, + modifier = + Modifier + .size(24.dp) + .clickable( + indication = null, + interactionSource = interactionSource, + onClick = { + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + onClick() + }, + ), + ) + + AppText( + text = stringResource(tab.title), + color = GrayColor.C500, + style = AppTextStyle.C2, + ) + } +} diff --git a/feature/main/src/main/java/com/twix/main/di/MainModule.kt b/feature/main/src/main/java/com/twix/main/di/MainModule.kt new file mode 100644 index 0000000..60b3a8f --- /dev/null +++ b/feature/main/src/main/java/com/twix/main/di/MainModule.kt @@ -0,0 +1,15 @@ +package com.twix.main.di + +import com.twix.main.MainViewModel +import com.twix.main.navigation.MainNavGraph +import com.twix.navigation.NavRoutes +import com.twix.navigation.base.NavGraphContributor +import org.koin.core.module.dsl.viewModelOf +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val mainModule = + module { + viewModelOf(::MainViewModel) + single(named(NavRoutes.MainGraph.route)) { MainNavGraph } + } diff --git a/feature/main/src/main/java/com/twix/main/model/MainTab.kt b/feature/main/src/main/java/com/twix/main/model/MainTab.kt new file mode 100644 index 0000000..17da6b4 --- /dev/null +++ b/feature/main/src/main/java/com/twix/main/model/MainTab.kt @@ -0,0 +1,25 @@ +package com.twix.main.model + +import com.twix.designsystem.R + +enum class MainTab( + val selectedIcon: Int, + val unselectedIcon: Int, + val title: Int, +) { + HOME( + selectedIcon = R.drawable.ic_home_selected, + unselectedIcon = R.drawable.ic_home_unselected, + title = R.string.word_home, + ), + STATS( + selectedIcon = R.drawable.ic_stats_selected, + unselectedIcon = R.drawable.ic_stats_unselected, + title = R.string.word_stats, + ), + COUPLE( + selectedIcon = R.drawable.ic_couple_selected, + unselectedIcon = R.drawable.ic_couple_unselected, + title = R.string.word_couple_page, + ), +} diff --git a/feature/main/src/main/java/com/twix/main/model/MainUiState.kt b/feature/main/src/main/java/com/twix/main/model/MainUiState.kt new file mode 100644 index 0000000..2b2222f --- /dev/null +++ b/feature/main/src/main/java/com/twix/main/model/MainUiState.kt @@ -0,0 +1,9 @@ +package com.twix.main.model + +import androidx.compose.runtime.Immutable +import com.twix.ui.base.State + +@Immutable +data class MainUiState( + val selectedTab: MainTab = MainTab.HOME, +) : State diff --git a/feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt b/feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt new file mode 100644 index 0000000..a96a4e1 --- /dev/null +++ b/feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt @@ -0,0 +1,27 @@ +package com.twix.main.navigation + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import com.twix.main.MainRoute +import com.twix.navigation.NavRoutes +import com.twix.navigation.base.NavGraphContributor + +object MainNavGraph : NavGraphContributor { + override val graphRoute: NavRoutes + get() = NavRoutes.MainGraph + override val startDestination: String + get() = NavRoutes.MainRoute.route + + override fun NavGraphBuilder.registerGraph(navController: NavHostController) { + navigation( + route = graphRoute.route, + startDestination = startDestination, + ) { + composable(NavRoutes.MainRoute.route) { + MainRoute() + } + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 56ad484..fcfaa91 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,6 +55,9 @@ androidx-test-ext-junit = "1.3.0" turbine = "1.2.1" jetbrains-kotlin-jvm = "2.1.0" +# Logging +kermit = "2.0.8" + [libraries] # AndroidX androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } @@ -121,6 +124,9 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" } +# Logging +kermit = { group = "co.touchlab", name = "kermit", version.ref = "kermit" } + [bundles] androidx = [ "androidx-core-ktx", @@ -195,3 +201,4 @@ twix-feature = { id = "twix.feature", version = "unspecified" } twix-koin = { id = "twix.koin", version = "unspecified" } twix-java-library = { id = "twix.java.library", version = "unspecified" } twix-data = { id = "twix.data", version = "unspecified" } +twix-kermit = { id = "twix.kermit", version = "unspecified" } diff --git a/settings.gradle.kts b/settings.gradle.kts index e8d75d0..29626c0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,3 +35,4 @@ include(":core:navigation") include(":core:design-system") include(":core:network") include(":core:analytics") +include(":feature:main")