diff --git a/README.md b/README.md index 8973bd0b..aed4ffeb 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ # 청년을 위한 플랫폼 '청하' -청하(청춘하랑)는 ‘청춘’과 “함께 높이 날다”라는 의미를 가진 순우리말 ‘하랑’을 합쳐, -우리 청년들이 이루고자 하는 목표를 위해 함께 날아가고자 만들어진 서비스예요. -청하에서 여러분이 이루고자 하는 목표를 위한 첫걸음을 시작해 보세요. +청하(청년하랑) 앱은 청년들을 위한 정책 정보를 제공하고, 사용자들 간의 소통을 도와주는 커뮤니티 플랫폼입니다. + +최신 청년 정책 정보 제공: 청년을 위한 다양한 정책과 그 세부 정보를 확인할 수 있습니다. +커뮤니티 기능: 같은 관심사를 가진 사용자들과 소통하고 네트워크를 형성할 수 있는 커뮤니티 기능을 갖추고 있습니다. + +청하 앱을 통해 청년 정책에 대한 정보를 쉽게 얻고, 다른 청년들과 소통하며 유익한 커뮤니티를 형성하세요! ## Environment - Android Studio Jellyfish diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f5925ce8..bd86b5d5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,8 +10,8 @@ android { defaultConfig { applicationId = "com.withpeace.withpeace" targetSdk = 34 - versionCode = 6 - versionName = "1.0.1" + versionCode = 7 + versionName = "1.0.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -33,6 +33,11 @@ android { "proguard-rules.pro", ) } + create("benchmark") { + initWith(buildTypes.getByName("release")) + matchingFallbacks += listOf("release") + isDebuggable = false + } } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4f2039ec..2dbd14cd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,12 +3,10 @@ xmlns:tools="http://schemas.android.com/tools" package="com.withpeace.withpeace"> - - - - - - + + + + + android:theme="@style/Theme.Withpeace.Starting" + android:windowSoftInputMode="adjustResize"> @@ -35,4 +37,5 @@ - + + \ No newline at end of file diff --git a/app/src/main/java/com/withpeace/withpeace/MainActivity.kt b/app/src/main/java/com/withpeace/withpeace/MainActivity.kt index 8b31f4a8..da55c3d3 100644 --- a/app/src/main/java/com/withpeace/withpeace/MainActivity.kt +++ b/app/src/main/java/com/withpeace/withpeace/MainActivity.kt @@ -53,7 +53,6 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) // System Bar에 가려지는 뷰 영역을 개발자가 제어하겠다. WindowCompat.setDecorFitsSystemWindows(window, false) - lifecycleScope.launch { splashScreen = installSplashScreen() splashScreen.setKeepOnScreenCondition { true } @@ -63,9 +62,7 @@ class MainActivity : ComponentActivity() { when (uiState) { MainUiState.Home -> composeStart(HOME_ROUTE) MainUiState.Login -> composeStart(LOGIN_ROUTE) - MainUiState.Update -> { - compulsionUpdate() - } + MainUiState.Update -> {} MainUiState.Error -> finish() MainUiState.Loading -> {} diff --git a/app/src/main/java/com/withpeace/withpeace/MainViewModel.kt b/app/src/main/java/com/withpeace/withpeace/MainViewModel.kt index 680d78f1..c2e06ba1 100644 --- a/app/src/main/java/com/withpeace/withpeace/MainViewModel.kt +++ b/app/src/main/java/com/withpeace/withpeace/MainViewModel.kt @@ -6,7 +6,11 @@ import com.withpeace.withpeace.core.domain.usecase.CheckAppUpdateUseCase import com.withpeace.withpeace.core.domain.usecase.IsLoginUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -15,8 +19,8 @@ class MainViewModel @Inject constructor( private val isLoginUseCase: IsLoginUseCase, private val checkAppUpdateUseCase: CheckAppUpdateUseCase, ) : ViewModel() { - private val _uiState: Channel = Channel() - val uiState = _uiState.receiveAsFlow() + private val _uiState: MutableStateFlow = MutableStateFlow(MainUiState.Loading) + val uiState = _uiState.asStateFlow() init { checkUpdate() @@ -27,18 +31,18 @@ class MainViewModel @Inject constructor( checkAppUpdateUseCase( currentVersion = BuildConfig.VERSION_CODE, onError = { - _uiState.send(MainUiState.Error) + _uiState.update { MainUiState.Error } }, ).collect { shouldUpdate -> if (shouldUpdate) { - _uiState.send(MainUiState.Update) + _uiState.update {MainUiState.Update } return@collect } val isLogin = isLoginUseCase() if (isLogin) { - _uiState.send(MainUiState.Home) + _uiState.update { MainUiState.Home } } else { - _uiState.send(MainUiState.Login) + _uiState.update { MainUiState.Login } } } } diff --git a/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt b/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt index 2e9423a7..d70e8cf3 100644 --- a/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt +++ b/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt @@ -11,7 +11,10 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController @@ -19,6 +22,7 @@ import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme import com.withpeace.withpeace.navigation.WithpeaceNavHost import kotlinx.coroutines.launch +@OptIn(ExperimentalComposeUiApi::class) @Composable fun WithpeaceApp( startDestination: String, @@ -47,7 +51,9 @@ fun WithpeaceApp( ) } }, - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().semantics { + testTagsAsResourceId = true + }, snackbarHost = { SnackbarHost(snackBarHostState) }, containerColor = WithpeaceTheme.colors.SystemWhite, ) { innerPadding -> diff --git a/benchmark/.gitignore b/benchmark/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/benchmark/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts new file mode 100644 index 00000000..2b9312fb --- /dev/null +++ b/benchmark/build.gradle.kts @@ -0,0 +1,53 @@ +plugins { + alias(libs.plugins.android.test) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.withpeace.withpeace.benchmark" + compileSdk = 34 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + defaultConfig { + minSdk = 26 + targetSdk = 34 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR" + } + + buildTypes { + // This benchmark buildType is used for benchmarking, and should function like your + // release build (for example, with minification on). It"s signed with a debug key + // for easy local/CI testing. + create("benchmark") { + isDebuggable = true + signingConfig = getByName("debug").signingConfig + matchingFallbacks += listOf("release") + } + } + + targetProjectPath = ":app" + experimentalProperties["android.experimental.self-instrumenting"] = true +} + +dependencies { + implementation(libs.junit) + implementation(libs.androidx.test.espresso.core) + implementation(libs.androidx.uiautomator) + implementation(libs.androidx.benchmark.macro.junit4) +} + +androidComponents { + beforeVariants(selector().all()) { + it.enable = it.buildType == "benchmark" + } +} \ No newline at end of file diff --git a/benchmark/src/main/AndroidManifest.xml b/benchmark/src/main/AndroidManifest.xml new file mode 100644 index 00000000..227314ee --- /dev/null +++ b/benchmark/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/benchmark/src/main/java/com/withpeace/withpeace/benchmark/ExampleStartupBenchmark.kt b/benchmark/src/main/java/com/withpeace/withpeace/benchmark/ExampleStartupBenchmark.kt new file mode 100644 index 00000000..bf3d23e1 --- /dev/null +++ b/benchmark/src/main/java/com/withpeace/withpeace/benchmark/ExampleStartupBenchmark.kt @@ -0,0 +1,39 @@ +package com.withpeace.withpeace.benchmark + +import androidx.benchmark.macro.CompilationMode +import androidx.benchmark.macro.StartupMode +import androidx.benchmark.macro.StartupTimingMetric +import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * This is an example startup benchmark. + * + * It navigates to the device's home screen, and launches the default activity. + * + * Before running this benchmark: + * 1) switch your app's active build variant in the Studio (affects Studio runs only) + * 2) add `` to your app's manifest, within the `` tag + * + * Run this benchmark from Studio to see startup measurements, and captured system traces + * for investigating your app's performance. + */ +@RunWith(AndroidJUnit4::class) +class ExampleStartupBenchmark { + @get:Rule + val benchmarkRule = MacrobenchmarkRule() + + @Test + fun startup() = benchmarkRule.measureRepeated( + packageName = "com.withpeace.withpeace", + metrics = listOf(StartupTimingMetric()), + iterations = 5, + startupMode = StartupMode.COLD + ) { + pressHome() + startActivityAndWait() + } +} \ No newline at end of file diff --git a/benchmark/src/main/java/com/withpeace/withpeace/benchmark/home/HomeActions.kt b/benchmark/src/main/java/com/withpeace/withpeace/benchmark/home/HomeActions.kt new file mode 100644 index 00000000..f0bdafa5 --- /dev/null +++ b/benchmark/src/main/java/com/withpeace/withpeace/benchmark/home/HomeActions.kt @@ -0,0 +1,23 @@ +package com.withpeace.withpeace.benchmark.home + +import androidx.benchmark.macro.MacrobenchmarkScope +import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.Until + +fun MacrobenchmarkScope.policyContent() { + device.wait(Until.gone(By.res("circular_progress_bar")), 5_000) + + val obj = device.waitAndFindObject(By.res("forYou:topicSelection"), 10_000) + +} + +fun UiDevice.waitAndFindObject(selector: BySelector, timeout: Long): UiObject2 { + if (!wait(Until.hasObject(selector), timeout)) { + throw AssertionError("Element not found on screen in ${timeout}ms (selector=$selector)") + } + + return findObject(selector) +} \ No newline at end of file diff --git a/benchmark/src/main/java/com/withpeace/withpeace/benchmark/home/HomeBenchMark.kt b/benchmark/src/main/java/com/withpeace/withpeace/benchmark/home/HomeBenchMark.kt new file mode 100644 index 00000000..4d9860ea --- /dev/null +++ b/benchmark/src/main/java/com/withpeace/withpeace/benchmark/home/HomeBenchMark.kt @@ -0,0 +1,36 @@ +package com.withpeace.withpeace.benchmark.home + +import androidx.benchmark.macro.FrameTimingMetric +import androidx.benchmark.macro.StartupMode +import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Direction +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class HomeBenchMark { + @get:Rule + val benchmarkRule = MacrobenchmarkRule() + + @Test + fun scrollPolicy() = benchmarkRule.measureRepeated( + packageName = "com.withpeace.withpeace", + metrics = listOf(FrameTimingMetric()), + iterations = 10, + startupMode = StartupMode.HOT, + setupBlock = { + pressHome() + startActivityAndWait() + } + ) { + val contentList = device.waitAndFindObject(By.res("home:policies"), 10000) + contentList.setGestureMargin(device.displayWidth / 5) + + contentList.fling(Direction.DOWN) + + device.waitForIdle() + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index f4b90c26..516d96bf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,7 @@ plugins { alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.firebase.services) apply false alias(libs.plugins.firebase.crashlytics) apply false + alias(libs.plugins.android.test) apply false } apply { diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt index 1f5562a1..637bb229 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -178,7 +179,7 @@ private fun PolicyItems( ) { Spacer(modifier = modifier.height(8.dp)) LazyColumn( - modifier = modifier.fillMaxSize(), + modifier = modifier.fillMaxSize().testTag("home:policies"), contentPadding = PaddingValues(bottom = 16.dp), ) { items( diff --git a/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/PolicyConsentScreen.kt b/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/PolicyConsentScreen.kt index 2c72b9f5..db0d7194 100644 --- a/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/PolicyConsentScreen.kt +++ b/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/PolicyConsentScreen.kt @@ -110,7 +110,7 @@ fun PolicyConsentScreen( Text( modifier = modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center, - text = "청하(청춘하랑)에 어서오세요!\n" + + text = "청하(청년하랑)에 어서오세요!\n" + "약관에 동의하시면 청하와의 여정을\n" + "시작할 수 있어요!", style = WithpeaceTheme.typography.title2.merge( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dc383a05..0ed740bb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,16 @@ [versions] androidGradlePlugin = "8.2.2" -androidDesugarJdkLibs = "1.2.2" androidxCore = "1.12.0" androidxAppCompat = "1.6.1" androidxLifecycle = "2.7.0" -androidxComposeBom = "2023.10.01" +androidxComposeBom = "2024.06.00" androidxComposeCompiler = "1.5.1" -androidxComposeNavigation = "2.7.6" +androidxComposeNavigation = "2.7.7" composeIconExtended = "1.5.4" androidxActivity = "1.8.2" coreSplashscreen = "1.0.1" -hilt = "2.48" -hiltNavigationCompose = "1.1.0" +hilt = "2.51.1" +hiltNavigationCompose = "1.2.0" androidxConstraintlayout = "1.0.1" lifecycleRuntimeKtx = "2.7.0" @@ -54,15 +53,15 @@ androidxGlance = "1.0.0-beta01" glanceExperimentalTools = "0.2.2" junit = "1.1.5" material = "1.12.0" -material3Android = "1.3.0-beta03" -material3 = "1.3.0-beta03" +material3Android = "1.3.0-beta04" +material3 = "1.3.0-beta04" multidex = "2.0.1" google-login = "1.1.0" credential = "1.2.0" -paging = "3.3.0-alpha02" +paging = "3.3.0" coreTesting = "2.2.0" dependencyGraph = "0.8.0" @@ -72,10 +71,11 @@ firebaseBom = "32.7.4" firebaseCrashlytics = "2.9.9" textflow = "1.1.2" +uiautomator = "2.2.0" +benchmarkMacroJunit4 = "1.2.0-beta01" [libraries] android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } -android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" } kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } hilt-gradlePlugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" } @@ -185,6 +185,8 @@ androidx-material3-android = { group = "androidx.compose.material3", name = "mat firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" } firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" } +androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } +androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" } [bundles] @@ -200,5 +202,6 @@ verify-detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } firebase-services = { id = "com.google.gms.google-services", version.ref = "firebasePlugin" } firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlytics" } +android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 8ea150ea..6e8ded44 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,3 +41,4 @@ include(":feature:policydetail") include(":feature:policyconsent") include(":feature:privacypolicy") include(":feature:termsofservice") +include(":benchmark")