Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ dependencies {
implementation libs.androidx.swiperefreshlayout
implementation libs.androidx.preference.ktx
implementation libs.androidx.security.crypto.ktx
implementation libs.androidx.splashscreen

// Compose
implementation platform(libs.compose.bom)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,41 @@ package com.runnect.runnect.presentation.splash

import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.runnect.runnect.R
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.runnect.runnect.presentation.login.LoginActivity
import com.runnect.runnect.presentation.scheme.SchemeActivity

import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch

@AndroidEntryPoint
class SplashActivity : AppCompatActivity() {
private val handler = Handler(Looper.getMainLooper())
private val viewModel: SplashViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { false }
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
navigateToLoginScreen()
}
enableEdgeToEdge()

private fun navigateToLoginScreen() {
handler.postDelayed({
val intent = Intent(this, LoginActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
startActivity(intent)
finish()
}, DELAY_TIME)
}
setContent {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setContent { } — Compose 진입점

Activity에서 XML setContentView(R.layout.xxx)를 대체하는 Compose 진입점.
이 블록 안에 넘긴 @Composable 함수가 화면 전체를 그린다.

// 기존
setContentView(R.layout.activity_splash)

// Compose
setContent {
    SplashScreen()
}

핵심 개념:

  • setContent 내부에서 Compose 런타임이 초기화되고, @Composable 함수 호출 트리가 UI 트리로 변환됨
  • ComponentActivity.setContentandroidx.activity.compose 라이브러리가 제공 — AppCompatActivity를 상속해도 사용 가능

공식 문서 — setContent

SplashScreen()
}

companion object {
private const val DELAY_TIME = 1000L
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.navigateEvent.collect {
startActivity(Intent(this@SplashActivity, LoginActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
})
finish()
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.runnect.runnect.presentation.splash

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import com.runnect.runnect.R

@Composable

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Composable — Compose 함수 선언

이 어노테이션이 붙은 함수는 UI를 **기술(describe)**하는 단위.
기존 XML inflate + View 참조 방식 대신 함수 호출 트리가 UI 구조가 된다.

// 기존: XML을 inflate하고 View에 데이터 바인딩
val binding = ActivitySplashBinding.inflate(layoutInflater)

// Compose: 함수 = UI
@Composable
fun SplashScreen() { ... }

핵심 개념:

  • @Composable 함수는 일반 함수처럼 호출하지만, Compose 런타임이 호출 트리를 추적해 상태 변화 시 변경된 부분만 다시 그림(recomposition)
  • 반환값이 없음 — 함수가 UI를 직접 '그리는' 것이 아니라 런타임에 UI 트리를 '기술'하는 것
  • @Composable 함수는 반드시 다른 @Composable 스코프에서 호출해야 함

공식 문서 — Composable functions

fun SplashScreen() {
Image(
painter = painterResource(id = R.drawable.splash),
contentDescription = null,
modifier = Modifier.fillMaxSize(),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modifier — Compose의 레이아웃/스타일 체이닝

기존 View XML의 layout_width, layout_height, padding, background 등 속성을 코드로 체이닝하는 방식.

<!-- 기존 XML -->
<ImageView
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
// Compose
Image(
    modifier = Modifier.fillMaxSize()  // match_parent × match_parent
)

핵심 개념:

  • Modifier순서가 중요함fillMaxSize().padding(16.dp)padding(16.dp).fillMaxSize()는 다른 결과
  • 각 Composable의 modifier 파라미터로 전달 — 컴포넌트 자체 크기·위치·동작을 외부에서 제어하는 패턴
  • fillMaxSize() = match_parent / wrapContentSize() = wrap_content / size(100.dp) = 고정 크기

공식 문서 — Modifiers

contentScale = ContentScale.FillBounds,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ContentScale — Compose Image의 스케일링 방식

기존 ImageViewandroid:scaleType 속성에 대응.

ContentScale ImageView scaleType 동작
FillBounds fitXY 비율 무시하고 컨테이너 꽉 채움
Crop centerCrop 비율 유지, 짧은 축 기준으로 크롭
Fit fitCenter 비율 유지, 긴 축 기준으로 맞춤
FillWidth fitStart(유사) 너비 기준으로 채움

여기서 FillBounds를 선택한 이유: splash.webp가 정확히 기기 화면 비율(1440×3200)로 제작된 전체화면 이미지라 비율 왜곡이 발생해도 원본 windowBackground 렌더링 방식과 동일한 결과를 냄.

공식 문서 — ContentScale

)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.runnect.runnect.presentation.splash

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class SplashViewModel @Inject constructor() : ViewModel() {

private val _isReady = MutableStateFlow(false)
val isReady: StateFlow<Boolean> = _isReady.asStateFlow()

private val _navigateEvent = MutableSharedFlow<Unit>(replay = 1)
val navigateEvent: SharedFlow<Unit> = _navigateEvent.asSharedFlow()

init {
viewModelScope.launch {
delay(SPLASH_DELAY)
_isReady.value = true
_navigateEvent.emit(Unit)
}
}

companion object {
const val SPLASH_DELAY = 1000L
}
}
9 changes: 0 additions & 9 deletions app/src/main/res/layout/activity_splash.xml

This file was deleted.

7 changes: 3 additions & 4 deletions app/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@
<item name="editTextStyle">@style/Widget.Runnect.EditText</item>
</style>

<style name="SplashTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="windowNoTitle">true</item>
<item name="windowActionBar">false</item>
<item name="android:windowBackground">@drawable/splash</item>
<style name="SplashTheme" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/M1</item>
<item name="postSplashScreenTheme">@style/Theme.Runnect</item>
</style>

<!-- 신고하기 BottomSheet 테마 -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.runnect.runnect.presentation.splash

import app.cash.turbine.test
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

@OptIn(ExperimentalCoroutinesApi::class)
class SplashViewModelTest {
private val testDispatcher = StandardTestDispatcher()

@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}

@After
fun tearDown() {
Dispatchers.resetMain()
}

@Test
fun `초기 상태에서 isReady는 false다`() {
val viewModel = SplashViewModel()
assertFalse(viewModel.isReady.value)
}

@Test
fun `1초 경과 전에는 isReady가 false다`() = runTest(testDispatcher) {
val viewModel = SplashViewModel()
advanceTimeBy(SplashViewModel.SPLASH_DELAY - 1)
assertFalse(viewModel.isReady.value)
}

@Test
fun `1초 경과 후 isReady가 true가 된다`() = runTest(testDispatcher) {
val viewModel = SplashViewModel()
advanceUntilIdle()
assertTrue(viewModel.isReady.value)
}

@Test
fun `1초 후 navigateEvent가 emit된다`() = runTest(testDispatcher) {
val viewModel = SplashViewModel()
viewModel.navigateEvent.test {
advanceTimeBy(SplashViewModel.SPLASH_DELAY)
awaitItem()
cancelAndIgnoreRemainingEvents()
}
}
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ viewpager2 = "1.1.0"
swiperefreshlayout = "1.2.0"
preference-ktx = "1.2.1"
security-crypto = "1.1.0"
splashscreen = "1.0.1"

# Compose
compose-bom = "2026.03.01"
Expand Down Expand Up @@ -106,6 +107,7 @@ androidx-viewpager2 = { group = "androidx.viewpager2", name = "viewpager2", vers
androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" }
androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preference-ktx" }
androidx-security-crypto-ktx = { group = "androidx.security", name = "security-crypto-ktx", version.ref = "security-crypto" }
androidx-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashscreen" }

# Compose
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
Expand Down
Loading