From 69aec0d5d71e20d9d1c1f32deb33679a889bd94c Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 13:50:56 +0900 Subject: [PATCH 01/28] =?UTF-8?q?add:jetpackCompose=E3=81=AB=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2499a35..95643dc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,6 +7,7 @@ plugins { id("org.jlleitschuh.gradle.ktlint") version "12.1.2" id 'com.google.dagger.hilt.android' id 'jacoco' + id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" } android { @@ -39,6 +40,10 @@ android { buildFeatures { viewBinding true buildConfig true + compose true + } + composeOptions { + kotlinCompilerExtensionVersion '1.5.0' } } @@ -76,6 +81,16 @@ dependencies { implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") implementation("com.squareup.retrofit2:retrofit:2.11.0") implementation("com.squareup.moshi:moshi-kotlin:1.14.0") + + // jetpack compose + def composeBom = platform('androidx.compose:compose-bom:2025.01.00') + implementation composeBom + androidTestImplementation composeBom + implementation 'androidx.compose.material3:material3' + implementation 'androidx.compose.foundation:foundation' + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.ui:ui-tooling-preview' + debugImplementation 'androidx.compose.ui:ui-tooling' } kapt { @@ -86,11 +101,6 @@ jacoco { toolVersion = "0.8.8" } -//tasks.withType(Test) { -// useJUnitPlatform() -// finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run -//} - tasks.register("jacocoTestReport", JacocoReport) { dependsOn(tasks.test) @@ -111,4 +121,4 @@ tasks.register("jacocoTestReport", JacocoReport) { "jacoco/testDebugUnitTest.exec", "outputs/code-coverage/connected/*coverage.ec" ])) -} \ No newline at end of file +} From 6d3bfc6aa97176fa3b64598933cd35dd54d35f16 Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 13:51:26 +0900 Subject: [PATCH 02/28] =?UTF-8?q?fix:=20=20Activity=E3=81=AE=E5=90=8D?= =?UTF-8?q?=E5=89=8D=E3=82=92MainActivity=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 2 +- .../android/code_check/{TopActivity.kt => MainActivity.kt} | 2 +- app/src/main/res/layout/activity_top.xml | 2 +- app/src/main/res/layout/fragment_repository_search.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename app/src/main/kotlin/jp/co/yumemi/android/code_check/{TopActivity.kt => MainActivity.kt} (77%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2e1287a..b83f7e1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ android:fullBackupContent="@xml/backup_descriptor" android:name=".CodeCheckApplication"> diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/TopActivity.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/MainActivity.kt similarity index 77% rename from app/src/main/kotlin/jp/co/yumemi/android/code_check/TopActivity.kt rename to app/src/main/kotlin/jp/co/yumemi/android/code_check/MainActivity.kt index 926636a..c00ec29 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/TopActivity.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/MainActivity.kt @@ -7,4 +7,4 @@ import androidx.appcompat.app.AppCompatActivity import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class TopActivity : AppCompatActivity(R.layout.activity_top) +class MainActivity : AppCompatActivity(R.layout.activity_top) diff --git a/app/src/main/res/layout/activity_top.xml b/app/src/main/res/layout/activity_top.xml index bba2b95..9eb3991 100644 --- a/app/src/main/res/layout/activity_top.xml +++ b/app/src/main/res/layout/activity_top.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".TopActivity"> + tools:context=".MainActivity"> + tools:context=".MainActivity"> Date: Mon, 20 Jan 2025 13:51:38 +0900 Subject: [PATCH 03/28] =?UTF-8?q?fix:=20=E3=83=87=E3=83=95=E3=82=A9?= =?UTF-8?q?=E3=83=AB=E3=83=88=E3=81=AE=E3=83=86=E3=83=BC=E3=83=9E=E3=82=92?= =?UTF-8?q?=E5=8F=8D=E6=98=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code_check/core/presenter/theme/Color.kt | 11 +++ .../code_check/core/presenter/theme/Theme.kt | 71 +++++++++++++++++++ .../code_check/core/presenter/theme/Type.kt | 34 +++++++++ 3 files changed, 116 insertions(+) create mode 100644 app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Color.kt create mode 100644 app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Theme.kt create mode 100644 app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Type.kt diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Color.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Color.kt new file mode 100644 index 0000000..2fa9869 --- /dev/null +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Color.kt @@ -0,0 +1,11 @@ +package jp.co.yumemi.android.code_check.core.presenter.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Theme.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Theme.kt new file mode 100644 index 0000000..290ba05 --- /dev/null +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Theme.kt @@ -0,0 +1,71 @@ +package jp.co.yumemi.android.code_check.core.presenter.theme + + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun CodeCheckAppTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Type.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Type.kt new file mode 100644 index 0000000..6859f57 --- /dev/null +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Type.kt @@ -0,0 +1,34 @@ +package jp.co.yumemi.android.code_check.core.presenter.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file From 5ceb594a8040735c8b80fe2206f7fde578b09d9e Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 16:52:30 +0900 Subject: [PATCH 04/28] =?UTF-8?q?add:=20=E6=A4=9C=E7=B4=A2=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 4 + .../yumemi/android/code_check/MainActivity.kt | 21 +- .../code_check/core/presenter/MainScreen.kt | 60 ++++++ .../detail/RepositoryDetailScreen.kt | 11 + .../core/presenter/router/MainRouter.kt | 45 ++++ .../search/RepositorySearchScreen.kt | 197 ++++++++++++++++++ .../code_check/core/presenter/theme/Theme.kt | 34 +-- 7 files changed, 347 insertions(+), 25 deletions(-) create mode 100644 app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt create mode 100644 app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt create mode 100644 app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt create mode 100644 app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt diff --git a/app/build.gradle b/app/build.gradle index 95643dc..be0e0d6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -74,6 +74,7 @@ dependencies { kapt "com.google.dagger:hilt-compiler:2.55" testImplementation "org.mockito:mockito-core:5.2.0" testImplementation 'org.mockito:mockito-inline:5.2.0' + implementation 'androidx.hilt:hilt-navigation-compose:1.2.0' // Retrofit @@ -91,6 +92,9 @@ dependencies { implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-tooling-preview' debugImplementation 'androidx.compose.ui:ui-tooling' + implementation 'androidx.activity:activity-compose:1.10.0' + implementation 'androidx.navigation:navigation-compose:2.8.5' + } kapt { diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/MainActivity.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/MainActivity.kt index c00ec29..c25411a 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/MainActivity.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/MainActivity.kt @@ -3,8 +3,27 @@ */ package jp.co.yumemi.android.code_check +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.ui.Modifier import dagger.hilt.android.AndroidEntryPoint +import jp.co.yumemi.android.code_check.core.presenter.MainScreen +import jp.co.yumemi.android.code_check.core.presenter.theme.CodeCheckAppTheme @AndroidEntryPoint -class MainActivity : AppCompatActivity(R.layout.activity_top) +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + CodeCheckAppTheme { + MainScreen() + } + } + } +} diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt new file mode 100644 index 0000000..6f5e6bb --- /dev/null +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt @@ -0,0 +1,60 @@ +package jp.co.yumemi.android.code_check.core.presenter + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.navigation.compose.rememberNavController +import jp.co.yumemi.android.code_check.R +import jp.co.yumemi.android.code_check.core.presenter.router.MainRouter + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainScreen() { + val context = LocalContext.current + val appName = context.getString(R.string.app_name) + + val navController = rememberNavController() + val topBarTitle by remember { + mutableStateOf(appName) + } + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = topBarTitle, + fontWeight = FontWeight.Bold + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + ) + }, + ) { innerPadding -> + MainRouter( + toDetailScreen = { +// navController.navigate("${BottomNavigationBarRoute.ROUTE_EDITOR.route}/${id}") + }, + toBackScreen = { + navController.popBackStack() + }, + navController = navController, + modifier = Modifier + .padding(innerPadding) + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt new file mode 100644 index 0000000..107b118 --- /dev/null +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt @@ -0,0 +1,11 @@ +package jp.co.yumemi.android.code_check.core.presenter.detail + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable + +@Composable +fun RepositoryDetailScreen( + toBack: () -> Unit +){ + Text(text = "RepositoryDetailScreen") +} \ No newline at end of file diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt new file mode 100644 index 0000000..41f2991 --- /dev/null +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt @@ -0,0 +1,45 @@ +package jp.co.yumemi.android.code_check.core.presenter.router + + +import android.util.Log +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import jp.co.yumemi.android.code_check.core.presenter.detail.RepositoryDetailScreen +import jp.co.yumemi.android.code_check.core.presenter.search.RepositorySearchScreen + +@Composable +fun MainRouter( + toDetailScreen: () -> Unit, + toBackScreen: () -> Unit, + navController: NavHostController, + modifier: Modifier = Modifier +) { + NavHost( + navController = navController, + startDestination = BottomNavigationBarRoute.SEARCH.route, + modifier = modifier.fillMaxSize() + ) { + composable(BottomNavigationBarRoute.SEARCH.route) { + RepositorySearchScreen( + toDetailScreen = toDetailScreen + ) + } + composable(BottomNavigationBarRoute.DETAIL.route) { + RepositoryDetailScreen( + toBack = toBackScreen + ) + } + } +} + +enum class BottomNavigationBarRoute(val route: String,val title:String) { + SEARCH("search","検索"), + DETAIL("detail","詳細"), +} \ No newline at end of file diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt new file mode 100644 index 0000000..ba7ff08 --- /dev/null +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt @@ -0,0 +1,197 @@ +package jp.co.yumemi.android.code_check.core.presenter.search + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.sharp.Search +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import jp.co.yumemi.android.code_check.R +import jp.co.yumemi.android.code_check.core.entity.RepositoryItem +import jp.co.yumemi.android.code_check.core.presenter.theme.CodeCheckAppTheme +import jp.co.yumemi.android.code_check.core.utils.DialogHelper + +@Composable +fun RepositorySearchScreen( + toDetailScreen: () -> Unit, + viewModel: RepositorySearchViewModel = hiltViewModel() +) { + var inputText by remember { mutableStateOf("") } + val repositoryName = remember { mutableStateListOf() } + val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + + LaunchedEffect(Unit) { + viewModel.searchResults.observe(lifecycleOwner) { + repositoryName.clear() + repositoryName.addAll(it) + } + viewModel.errorMessage.observe(lifecycleOwner) { + it?.let { + DialogHelper.showErrorDialog( + context, + context.getString(it), + ) + } + } + } + + Column { + CustomSearchBar( + inputText = inputText, + onValueChange = { inputText = it }, + searchAction = { searchWord -> + repositoryName.clear() + viewModel.searchRepositories(searchWord.trim()) + } + ) + RepositoryListView( + repositoryName = repositoryName, + onTapping = toDetailScreen, + ) + } +} + +@Composable +fun RepositoryListView( + repositoryName: List, + onTapping: () -> Unit = {} +) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .semantics { isTraversalGroup = true } + ) { + items(repositoryName.size) { index -> + Column( + modifier = Modifier + .clickable { onTapping() } + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 16.dp) + ) { + Text( + text = repositoryName[index].name, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(8.dp)) + HorizontalDivider(color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.2f)) + } + } + } +} + +@Composable +fun CustomSearchBar( + inputText: String = "", + onValueChange: (String) -> Unit = {}, + searchAction:(String) -> Unit +) { + val context = LocalContext.current + val keyboardController = LocalSoftwareKeyboardController.current + + // キーボードアクションを定義 + val keyboardActions = KeyboardActions( + onSearch = { + searchAction(inputText) + keyboardController?.hide() + } + ) + + TextField( + value = inputText, + onValueChange = onValueChange, + placeholder = { Text(context.getString(R.string.searchInputText_hint)) }, + leadingIcon = { + Icon( + Icons.Sharp.Search, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .background( + color = MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(40.dp) + ), + colors = TextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.primaryContainer, + unfocusedContainerColor = MaterialTheme.colorScheme.primaryContainer, + focusedTextColor = MaterialTheme.colorScheme.onSurface, + unfocusedTextColor = MaterialTheme.colorScheme.onSurface, + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = MaterialTheme.colorScheme.primaryContainer, + unfocusedIndicatorColor = MaterialTheme.colorScheme.primaryContainer + ), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Search, + ), + keyboardActions = keyboardActions, + maxLines = 1, + singleLine = true + ) +} + +@Composable +@Preview(showBackground = true) +fun CustomSearchBarPreview() { + CodeCheckAppTheme { + CustomSearchBar(inputText = "Example") {} + } +} + +@Composable +@Preview(showBackground = true) +fun RepositoryListViewPreview() { + CodeCheckAppTheme { + RepositoryListView( + repositoryName = listOf( + RepositoryItem( + name = "Jetpack Compose", + ownerIconUrl = "", + language = "Jetpack Compose", + stargazersCount = 1, + forksCount = 1, + openIssuesCount = 1, + watchersCount = 1 + ) + ), + onTapping = {} + ) + } +} diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Theme.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Theme.kt index 290ba05..db9b027 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Theme.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Theme.kt @@ -1,7 +1,6 @@ package jp.co.yumemi.android.code_check.core.presenter.theme -import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -10,32 +9,27 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.core.view.WindowCompat private val DarkColorScheme = darkColorScheme( primary = Purple80, secondary = PurpleGrey80, - tertiary = Pink80 + tertiary = Pink80, + background = Color(0xFF121212), + surface = Color(0xFF1E1E1E), + onPrimary = Color.Black, + onSecondary = Color.Black ) private val LightColorScheme = lightColorScheme( primary = Purple40, secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), + tertiary = Pink40, + background = Color(0xFFFFFFFF), + surface = Color(0xFFF5F5F5), onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ + onSecondary = Color.White ) @Composable @@ -54,14 +48,6 @@ fun CodeCheckAppTheme( darkTheme -> DarkColorScheme else -> LightColorScheme } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme - } - } MaterialTheme( colorScheme = colorScheme, From 21e0dd321518970d0edf4293810d26d49f9baf7f Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 16:57:52 +0900 Subject: [PATCH 05/28] =?UTF-8?q?add:=20=E3=83=AD=E3=83=BC=E3=83=87?= =?UTF-8?q?=E3=82=A3=E3=83=B3=E3=82=B0=E7=94=BB=E9=9D=A2=E3=81=AE=E4=BD=9C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/RepositorySearchScreen.kt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt index ba7ff08..423c381 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt @@ -2,8 +2,10 @@ package jp.co.yumemi.android.code_check.core.presenter.search import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -13,6 +15,7 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.sharp.Search +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -26,6 +29,7 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController @@ -51,11 +55,13 @@ fun RepositorySearchScreen( val repositoryName = remember { mutableStateListOf() } val lifecycleOwner = LocalLifecycleOwner.current val context = LocalContext.current + var isLoading by remember { mutableStateOf(false) } LaunchedEffect(Unit) { viewModel.searchResults.observe(lifecycleOwner) { repositoryName.clear() repositoryName.addAll(it) + isLoading = false } viewModel.errorMessage.observe(lifecycleOwner) { it?.let { @@ -64,9 +70,12 @@ fun RepositorySearchScreen( context.getString(it), ) } + isLoading = false } } + + Column { CustomSearchBar( inputText = inputText, @@ -74,8 +83,12 @@ fun RepositorySearchScreen( searchAction = { searchWord -> repositoryName.clear() viewModel.searchRepositories(searchWord.trim()) + isLoading = true } ) + if(isLoading){ + ProgressCycle() + } RepositoryListView( repositoryName = repositoryName, onTapping = toDetailScreen, @@ -83,6 +96,24 @@ fun RepositorySearchScreen( } } +@Composable +fun ProgressCycle(){ + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(8.dp) + .semantics { isTraversalGroup = true } + + ) { + CircularProgressIndicator() + Text(text = "検索中") + } + +} + @Composable fun RepositoryListView( repositoryName: List, From 15c7546197540f04f3d3d70ddada5b6018880adb Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 17:18:07 +0900 Subject: [PATCH 06/28] =?UTF-8?q?add:=20=E8=A9=B3=E7=B4=B0=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=E3=81=AB=E9=81=B7=E7=A7=BB=E3=81=A7=E3=81=8D=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 2 + .../yumemi/android/code_check/MainActivity.kt | 8 +- ...{RepositoryItem.kt => RepositoryEntity.kt} | 3 +- .../code_check/core/presenter/MainScreen.kt | 23 +-- .../detail/RepositoryDetailFragment.kt | 4 +- .../detail/RepositoryDetailScreen.kt | 10 +- .../core/presenter/router/MainRouter.kt | 28 ++-- .../RepositoryListRecyclerViewAdapter.kt | 18 +-- .../search/RepositorySearchFragment.kt | 8 +- .../search/RepositorySearchScreen.kt | 151 +++++++++--------- .../search/RepositorySearchViewModel.kt | 6 +- .../code_check/core/presenter/theme/Color.kt | 2 +- .../code_check/core/presenter/theme/Theme.kt | 62 +++---- .../code_check/core/presenter/theme/Type.kt | 22 +-- .../github/entity/GitHubServiceEntity.kt | 1 + .../reposiotory/GitHubServiceRepository.kt | 4 +- .../GitHubServiceRepositoryImpl.kt | 7 +- .../github/usecase/GitHubServiceUsecase.kt | 4 +- .../usecase/GitHubServiceUsecaseImpl.kt | 4 +- app/src/main/res/navigation/nav_graph.xml | 2 +- .../github/GitHubServiceUsecaseImplTest.kt | 6 +- 21 files changed, 195 insertions(+), 180 deletions(-) rename app/src/main/kotlin/jp/co/yumemi/android/code_check/core/entity/{RepositoryItem.kt => RepositoryEntity.kt} (88%) diff --git a/.editorconfig b/.editorconfig index c91f60f..8dc7d85 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,2 +1,4 @@ [*.{kt,kts}] ktlint_standard_package-name = disabled +ktlint_function_naming_ignore_when_annotated_with = Composable + diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/MainActivity.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/MainActivity.kt index c25411a..60a049b 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/MainActivity.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/MainActivity.kt @@ -6,12 +6,6 @@ package jp.co.yumemi.android.code_check import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.ui.Modifier import dagger.hilt.android.AndroidEntryPoint import jp.co.yumemi.android.code_check.core.presenter.MainScreen import jp.co.yumemi.android.code_check.core.presenter.theme.CodeCheckAppTheme @@ -21,7 +15,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - CodeCheckAppTheme { + CodeCheckAppTheme { MainScreen() } } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/entity/RepositoryItem.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/entity/RepositoryEntity.kt similarity index 88% rename from app/src/main/kotlin/jp/co/yumemi/android/code_check/core/entity/RepositoryItem.kt rename to app/src/main/kotlin/jp/co/yumemi/android/code_check/core/entity/RepositoryEntity.kt index 1136dce..e172ea5 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/entity/RepositoryItem.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/entity/RepositoryEntity.kt @@ -4,7 +4,8 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class RepositoryItem( +data class RepositoryEntity( + val id: Int, val name: String, val ownerIconUrl: String, val language: String, diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt index 6f5e6bb..b888478 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.navigation.compose.rememberNavController import jp.co.yumemi.android.code_check.R +import jp.co.yumemi.android.code_check.core.presenter.router.BottomNavigationBarRoute import jp.co.yumemi.android.code_check.core.presenter.router.MainRouter @OptIn(ExperimentalMaterial3Api::class) @@ -35,26 +36,28 @@ fun MainScreen() { title = { Text( text = topBarTitle, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, ) }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - titleContentColor = MaterialTheme.colorScheme.primary, - ), + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), ) }, ) { innerPadding -> MainRouter( - toDetailScreen = { -// navController.navigate("${BottomNavigationBarRoute.ROUTE_EDITOR.route}/${id}") + toDetailScreen = { id -> + navController.navigate("${BottomNavigationBarRoute.DETAIL.route}/$id") }, toBackScreen = { navController.popBackStack() }, navController = navController, - modifier = Modifier - .padding(innerPadding) + modifier = + Modifier + .padding(innerPadding), ) } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailFragment.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailFragment.kt index 614ee71..d535f66 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailFragment.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailFragment.kt @@ -10,7 +10,7 @@ import androidx.navigation.fragment.navArgs import coil.load import dagger.hilt.android.AndroidEntryPoint import jp.co.yumemi.android.code_check.R -import jp.co.yumemi.android.code_check.core.entity.RepositoryItem +import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.databinding.FragmentRepositoryDetailBinding @AndroidEntryPoint @@ -32,7 +32,7 @@ class RepositoryDetailFragment : Fragment(R.layout.fragment_repository_detail) { bindViews(item) } - private fun bindViews(item: RepositoryItem) { + private fun bindViews(item: RepositoryEntity) { binding.ownerIconView.load(item.ownerIconUrl) binding.nameView.text = item.name binding.languageView.text = resources.getString(R.string.written_language, item.language) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt index 107b118..01ed0b4 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt @@ -5,7 +5,9 @@ import androidx.compose.runtime.Composable @Composable fun RepositoryDetailScreen( - toBack: () -> Unit -){ - Text(text = "RepositoryDetailScreen") -} \ No newline at end of file + toBack: () -> Unit, + repositoryId: Int, +) { +// Text(text = "RepositoryDetailScreen") + Text(text = "repositoryId: $repositoryId") +} diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt index 41f2991..6ddcdb8 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt @@ -1,7 +1,5 @@ package jp.co.yumemi.android.code_check.core.presenter.router - -import android.util.Log import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -9,37 +7,41 @@ import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import jp.co.yumemi.android.code_check.core.presenter.detail.RepositoryDetailScreen import jp.co.yumemi.android.code_check.core.presenter.search.RepositorySearchScreen @Composable fun MainRouter( - toDetailScreen: () -> Unit, + toDetailScreen: (Int) -> Unit, toBackScreen: () -> Unit, navController: NavHostController, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { NavHost( navController = navController, startDestination = BottomNavigationBarRoute.SEARCH.route, - modifier = modifier.fillMaxSize() + modifier = modifier.fillMaxSize(), ) { composable(BottomNavigationBarRoute.SEARCH.route) { RepositorySearchScreen( - toDetailScreen = toDetailScreen + toDetailScreen = toDetailScreen, ) } - composable(BottomNavigationBarRoute.DETAIL.route) { + composable( + BottomNavigationBarRoute.DETAIL.route + "/{id}", + arguments = listOf(navArgument("id") { type = NavType.IntType }), + ) { backStackEntry -> + val id = backStackEntry.arguments?.getInt("id") RepositoryDetailScreen( - toBack = toBackScreen + toBack = toBackScreen, + repositoryId = id ?: 0, ) } } } -enum class BottomNavigationBarRoute(val route: String,val title:String) { - SEARCH("search","検索"), - DETAIL("detail","詳細"), -} \ No newline at end of file +enum class BottomNavigationBarRoute(val route: String, val title: String) { + SEARCH("search", "検索"), + DETAIL("detail", "詳細"), +} diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositoryListRecyclerViewAdapter.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositoryListRecyclerViewAdapter.kt index f0d1656..4c420c3 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositoryListRecyclerViewAdapter.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositoryListRecyclerViewAdapter.kt @@ -8,23 +8,23 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import jp.co.yumemi.android.code_check.R -import jp.co.yumemi.android.code_check.core.entity.RepositoryItem +import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity /** * DiffUtilの実装 */ private val diffUtilCallback = - object : DiffUtil.ItemCallback() { + object : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: RepositoryItem, - newItem: RepositoryItem, + oldItem: RepositoryEntity, + newItem: RepositoryEntity, ): Boolean { return oldItem.name == newItem.name } override fun areContentsTheSame( - oldItem: RepositoryItem, - newItem: RepositoryItem, + oldItem: RepositoryEntity, + newItem: RepositoryEntity, ): Boolean { return oldItem == newItem } @@ -35,7 +35,7 @@ private val diffUtilCallback = */ class RepositoryListRecyclerViewAdapter( private val itemClickListener: OnItemClickListener, -) : ListAdapter(diffUtilCallback) { +) : ListAdapter(diffUtilCallback) { class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { private val repositoryNameView: TextView? = view.findViewById(R.id.repositoryNameView) @@ -43,7 +43,7 @@ class RepositoryListRecyclerViewAdapter( * ビューにデータをバインド */ fun bind( - item: RepositoryItem, + item: RepositoryEntity, clickListener: OnItemClickListener, ) { repositoryNameView?.text = item.name @@ -52,7 +52,7 @@ class RepositoryListRecyclerViewAdapter( } interface OnItemClickListener { - fun itemClick(repositoryItem: RepositoryItem) + fun itemClick(repositoryEntity: RepositoryEntity) } override fun onCreateViewHolder( diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchFragment.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchFragment.kt index 12a133c..c74716f 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchFragment.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchFragment.kt @@ -10,7 +10,7 @@ import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import jp.co.yumemi.android.code_check.R -import jp.co.yumemi.android.code_check.core.entity.RepositoryItem +import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.core.utils.DialogHelper import jp.co.yumemi.android.code_check.databinding.FragmentRepositorySearchBinding @@ -23,8 +23,8 @@ class RepositorySearchFragment : Fragment(R.layout.fragment_repository_search) { private val adapter by lazy { RepositoryListRecyclerViewAdapter( object : RepositoryListRecyclerViewAdapter.OnItemClickListener { - override fun itemClick(repositoryItem: RepositoryItem) { - onItemClick(repositoryItem) + override fun itemClick(repositoryEntity: RepositoryEntity) { + onItemClick(repositoryEntity) } }, ) @@ -83,7 +83,7 @@ class RepositorySearchFragment : Fragment(R.layout.fragment_repository_search) { * リポジトリ検索結果のクリックイベント * リサイクラービューでアイテムが押された時に動作を行います。 */ - private fun onItemClick(item: RepositoryItem) { + private fun onItemClick(item: RepositoryEntity) { val action = RepositorySearchFragmentDirections .actionRepositoriesFragmentToRepositoryFragment(item = item) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt index 423c381..ee29549 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt @@ -42,25 +42,25 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import jp.co.yumemi.android.code_check.R -import jp.co.yumemi.android.code_check.core.entity.RepositoryItem +import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.core.presenter.theme.CodeCheckAppTheme import jp.co.yumemi.android.code_check.core.utils.DialogHelper @Composable fun RepositorySearchScreen( - toDetailScreen: () -> Unit, - viewModel: RepositorySearchViewModel = hiltViewModel() + toDetailScreen: (Int) -> Unit, + viewModel: RepositorySearchViewModel = hiltViewModel(), ) { var inputText by remember { mutableStateOf("") } - val repositoryName = remember { mutableStateListOf() } + val repositoryList = remember { mutableStateListOf() } val lifecycleOwner = LocalLifecycleOwner.current val context = LocalContext.current var isLoading by remember { mutableStateOf(false) } LaunchedEffect(Unit) { viewModel.searchResults.observe(lifecycleOwner) { - repositoryName.clear() - repositoryName.addAll(it) + repositoryList.clear() + repositoryList.addAll(it) isLoading = false } viewModel.errorMessage.observe(lifecycleOwner) { @@ -74,68 +74,67 @@ fun RepositorySearchScreen( } } - - Column { CustomSearchBar( inputText = inputText, onValueChange = { inputText = it }, searchAction = { searchWord -> - repositoryName.clear() + repositoryList.clear() viewModel.searchRepositories(searchWord.trim()) isLoading = true - } + }, ) - if(isLoading){ + if (isLoading) { ProgressCycle() } RepositoryListView( - repositoryName = repositoryName, + repositoryList = repositoryList, onTapping = toDetailScreen, ) } } @Composable -fun ProgressCycle(){ +fun ProgressCycle() { Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(8.dp) - .semantics { isTraversalGroup = true } - + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(8.dp) + .semantics { isTraversalGroup = true }, ) { CircularProgressIndicator() Text(text = "検索中") } - } @Composable fun RepositoryListView( - repositoryName: List, - onTapping: () -> Unit = {} + repositoryList: List, + onTapping: (Int) -> Unit = {}, ) { LazyColumn( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - .semantics { isTraversalGroup = true } + modifier = + Modifier + .fillMaxWidth() + .padding(8.dp) + .semantics { isTraversalGroup = true }, ) { - items(repositoryName.size) { index -> + items(repositoryList.size) { index -> Column( - modifier = Modifier - .clickable { onTapping() } - .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 16.dp) + modifier = + Modifier + .clickable { onTapping(repositoryList[index].id) } + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 16.dp), ) { Text( - text = repositoryName[index].name, + text = repositoryList[index].name, style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.height(8.dp)) HorizontalDivider(color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.2f)) @@ -148,18 +147,19 @@ fun RepositoryListView( fun CustomSearchBar( inputText: String = "", onValueChange: (String) -> Unit = {}, - searchAction:(String) -> Unit + searchAction: (String) -> Unit, ) { val context = LocalContext.current val keyboardController = LocalSoftwareKeyboardController.current // キーボードアクションを定義 - val keyboardActions = KeyboardActions( - onSearch = { - searchAction(inputText) - keyboardController?.hide() - } - ) + val keyboardActions = + KeyboardActions( + onSearch = { + searchAction(inputText) + keyboardController?.hide() + }, + ) TextField( value = inputText, @@ -169,32 +169,35 @@ fun CustomSearchBar( Icon( Icons.Sharp.Search, contentDescription = null, - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) }, - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - .background( - color = MaterialTheme.colorScheme.surface, - shape = RoundedCornerShape(40.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(8.dp) + .background( + color = MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(40.dp), + ), + colors = + TextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.primaryContainer, + unfocusedContainerColor = MaterialTheme.colorScheme.primaryContainer, + focusedTextColor = MaterialTheme.colorScheme.onSurface, + unfocusedTextColor = MaterialTheme.colorScheme.onSurface, + cursorColor = MaterialTheme.colorScheme.primary, + focusedIndicatorColor = MaterialTheme.colorScheme.primaryContainer, + unfocusedIndicatorColor = MaterialTheme.colorScheme.primaryContainer, + ), + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Search, ), - colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.primaryContainer, - unfocusedContainerColor = MaterialTheme.colorScheme.primaryContainer, - focusedTextColor = MaterialTheme.colorScheme.onSurface, - unfocusedTextColor = MaterialTheme.colorScheme.onSurface, - cursorColor = MaterialTheme.colorScheme.primary, - focusedIndicatorColor = MaterialTheme.colorScheme.primaryContainer, - unfocusedIndicatorColor = MaterialTheme.colorScheme.primaryContainer - ), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Search, - ), keyboardActions = keyboardActions, maxLines = 1, - singleLine = true + singleLine = true, ) } @@ -211,18 +214,20 @@ fun CustomSearchBarPreview() { fun RepositoryListViewPreview() { CodeCheckAppTheme { RepositoryListView( - repositoryName = listOf( - RepositoryItem( - name = "Jetpack Compose", - ownerIconUrl = "", - language = "Jetpack Compose", - stargazersCount = 1, - forksCount = 1, - openIssuesCount = 1, - watchersCount = 1 - ) - ), - onTapping = {} + repositoryList = + listOf( + RepositoryEntity( + name = "Jetpack Compose", + ownerIconUrl = "", + language = "Jetpack Compose", + stargazersCount = 1, + forksCount = 1, + openIssuesCount = 1, + watchersCount = 1, + id = 1, + ), + ), + onTapping = {}, ) } } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchViewModel.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchViewModel.kt index d19b58f..33fef69 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchViewModel.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchViewModel.kt @@ -7,7 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import jp.co.yumemi.android.code_check.R -import jp.co.yumemi.android.code_check.core.entity.RepositoryItem +import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.features.github.reposiotory.NetworkException import jp.co.yumemi.android.code_check.features.github.usecase.GitHubServiceUsecase import jp.co.yumemi.android.code_check.features.github.utils.GitHubError @@ -27,8 +27,8 @@ class RepositorySearchViewModel private val _errorMessage = MutableLiveData() val errorMessage: LiveData get() = _errorMessage - private val _searchResults = MutableLiveData>() - val searchResults: LiveData> get() = _searchResults + private val _searchResults = MutableLiveData>() + val searchResults: LiveData> get() = _searchResults /** * GitHubのレポジトリ検索を行う diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Color.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Color.kt index 2fa9869..889d0f2 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Color.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Color.kt @@ -8,4 +8,4 @@ val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +val Pink40 = Color(0xFF7D5260) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Theme.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Theme.kt index db9b027..36a45d3 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Theme.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Theme.kt @@ -1,6 +1,5 @@ package jp.co.yumemi.android.code_check.core.presenter.theme - import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -12,46 +11,49 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80, - background = Color(0xFF121212), - surface = Color(0xFF1E1E1E), - onPrimary = Color.Black, - onSecondary = Color.Black -) +private val DarkColorScheme = + darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80, + background = Color(0xFF121212), + surface = Color(0xFF1E1E1E), + onPrimary = Color.Black, + onSecondary = Color.Black, + ) -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40, - background = Color(0xFFFFFFFF), - surface = Color(0xFFF5F5F5), - onPrimary = Color.White, - onSecondary = Color.White -) +private val LightColorScheme = + lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40, + background = Color(0xFFFFFFFF), + surface = Color(0xFFF5F5F5), + onPrimary = Color.White, + onSecondary = Color.White, + ) @Composable fun CodeCheckAppTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } + val colorScheme = + when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } - darkTheme -> DarkColorScheme - else -> LightColorScheme - } + darkTheme -> DarkColorScheme + else -> LightColorScheme + } MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = content, ) -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Type.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Type.kt index 6859f57..e59570c 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Type.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Type.kt @@ -7,14 +7,16 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp // Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) +val Typography = + Typography( + bodyLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ), /* Other default text styles to override titleLarge = TextStyle( fontFamily = FontFamily.Default, @@ -30,5 +32,5 @@ val Typography = Typography( lineHeight = 16.sp, letterSpacing = 0.5.sp ) - */ -) \ No newline at end of file + */ + ) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/entity/GitHubServiceEntity.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/entity/GitHubServiceEntity.kt index 09453d6..d8579f7 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/entity/GitHubServiceEntity.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/entity/GitHubServiceEntity.kt @@ -7,6 +7,7 @@ data class RepositoryList( ) data class RepositoryItem( + val id: Int, val name: String, val owner: RepositoryOwner, val language: String?, diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepository.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepository.kt index 0ccedef..8e7e161 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepository.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepository.kt @@ -1,8 +1,8 @@ package jp.co.yumemi.android.code_check.features.github.reposiotory -import jp.co.yumemi.android.code_check.core.entity.RepositoryItem +import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.features.github.utils.NetworkResult interface GitHubServiceRepository { - suspend fun fetchSearchResults(inputText: String): NetworkResult> + suspend fun fetchSearchResults(inputText: String): NetworkResult> } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepositoryImpl.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepositoryImpl.kt index ecec1d2..26b2d54 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepositoryImpl.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepositoryImpl.kt @@ -1,6 +1,6 @@ package jp.co.yumemi.android.code_check.features.github.reposiotory -import jp.co.yumemi.android.code_check.core.entity.RepositoryItem +import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.features.github.api.GitHubServiceApi import jp.co.yumemi.android.code_check.features.github.utils.GitHubError import jp.co.yumemi.android.code_check.features.github.utils.NetworkResult @@ -14,12 +14,12 @@ class GitHubServiceRepositoryImpl constructor( private val gitHubRepositoryApi: GitHubServiceApi, ) : GitHubServiceRepository { - override suspend fun fetchSearchResults(inputText: String): NetworkResult> { + override suspend fun fetchSearchResults(inputText: String): NetworkResult> { return try { val repositoryList = gitHubRepositoryApi.getRepository(inputText) val items = repositoryList.items.map { item -> - RepositoryItem( + RepositoryEntity( name = item.name, ownerIconUrl = item.owner.avatarUrl, language = item.language ?: "none", @@ -27,6 +27,7 @@ class GitHubServiceRepositoryImpl watchersCount = item.watchersCount, forksCount = item.forksCount, openIssuesCount = item.openIssuesCount, + id = item.id, ) } NetworkResult.Success(items) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecase.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecase.kt index 3583218..50301ea 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecase.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecase.kt @@ -1,8 +1,8 @@ package jp.co.yumemi.android.code_check.features.github.usecase -import jp.co.yumemi.android.code_check.core.entity.RepositoryItem +import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.features.github.utils.NetworkResult interface GitHubServiceUsecase { - suspend fun fetchSearchResults(inputText: String): NetworkResult> + suspend fun fetchSearchResults(inputText: String): NetworkResult> } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecaseImpl.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecaseImpl.kt index a2e4ba6..f16b7ca 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecaseImpl.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecaseImpl.kt @@ -1,7 +1,7 @@ package jp.co.yumemi.android.code_check.features.github.usecase import jp.co.yumemi.android.code_check.core.api.NetworkConnectivityService -import jp.co.yumemi.android.code_check.core.entity.RepositoryItem +import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.features.github.reposiotory.GitHubServiceRepository import jp.co.yumemi.android.code_check.features.github.reposiotory.NetworkException import jp.co.yumemi.android.code_check.features.github.utils.NetworkResult @@ -13,7 +13,7 @@ class GitHubServiceUsecaseImpl private val repository: GitHubServiceRepository, private val networkConnectivityService: NetworkConnectivityService, ) : GitHubServiceUsecase { - override suspend fun fetchSearchResults(inputText: String): NetworkResult> { + override suspend fun fetchSearchResults(inputText: String): NetworkResult> { if (!networkConnectivityService.isNetworkAvailable()) { throw NetworkException("オフライン状態です") } diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index ea34d7b..910a239 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -22,7 +22,7 @@ tools:layout="@layout/fragment_repository_detail"> + app:argType="jp.co.yumemi.android.code_check.core.entity.RepositoryEntity" /> diff --git a/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceUsecaseImplTest.kt b/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceUsecaseImplTest.kt index f473f52..1670272 100644 --- a/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceUsecaseImplTest.kt +++ b/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceUsecaseImplTest.kt @@ -1,7 +1,7 @@ package jp.co.yumemi.android.code_check.features.github import jp.co.yumemi.android.code_check.core.api.NetworkConnectivityService -import jp.co.yumemi.android.code_check.core.entity.RepositoryItem +import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.features.github.reposiotory.GitHubServiceRepository import jp.co.yumemi.android.code_check.features.github.reposiotory.NetworkException import jp.co.yumemi.android.code_check.features.github.usecase.GitHubServiceUsecaseImpl @@ -47,7 +47,7 @@ class GitHubServiceUsecaseImplTest { runBlocking { val mockResults = listOf( - RepositoryItem( + RepositoryEntity( name = "repo1", ownerIconUrl = "description1", language = "url1", @@ -56,7 +56,7 @@ class GitHubServiceUsecaseImplTest { openIssuesCount = 30, watchersCount = 70, ), - RepositoryItem( + RepositoryEntity( name = "repo2", ownerIconUrl = "description2", language = "url2", From 3d295ebf50d6900e45d84ce3a701b300b66978a7 Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 17:21:30 +0900 Subject: [PATCH 07/28] =?UTF-8?q?fix:=20=E3=83=AC=E3=83=9D=E3=82=B8?= =?UTF-8?q?=E3=83=88=E3=83=AA=E3=81=AE=E3=83=AA=E3=82=B9=E3=83=88=E3=82=92?= =?UTF-8?q?=E5=8F=96=E5=BE=97=E3=81=99=E3=82=8B=E3=82=82=E3=81=AE=E3=81=AF?= =?UTF-8?q?=E3=81=9D=E3=81=AE=E3=82=88=E3=81=86=E3=81=AA=E5=90=8D=E5=89=8D?= =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/RepositoryDetailViewModel.kt | 70 +++++++++++++++++++ .../features/github/api/GitHubServiceApi.kt | 2 +- .../api/GitHubServiceApiBuilderInterface.kt | 2 +- .../github/api/GitHubServiceApiImpl.kt | 4 +- .../GitHubServiceRepositoryImpl.kt | 2 +- 5 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt new file mode 100644 index 0000000..ab2eafb --- /dev/null +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt @@ -0,0 +1,70 @@ +package jp.co.yumemi.android.code_check.core.presenter.detail + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import jp.co.yumemi.android.code_check.R +import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity +import jp.co.yumemi.android.code_check.features.github.reposiotory.NetworkException +import jp.co.yumemi.android.code_check.features.github.usecase.GitHubServiceUsecase +import jp.co.yumemi.android.code_check.features.github.utils.GitHubError +import jp.co.yumemi.android.code_check.features.github.utils.NetworkResult +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class +RepositoryDetailViewModel@Inject + constructor( + private val networkRepository: GitHubServiceUsecase, + ) : ViewModel() { + private val _errorMessage = MutableLiveData() + val errorMessage: LiveData get() = _errorMessage + + private val _searchResults = MutableLiveData>() + val searchResults: LiveData> get() = _searchResults + + /** + * GitHubのレポジトリ検索を行う + * @param query 検索キーワード + */ + fun searchRepositories(query: String) { + if (query.isBlank()) { + _errorMessage.postValue(R.string.form_is_empty) + return + } + viewModelScope.launch { + try { + val results = networkRepository.fetchSearchResults(query) + if (results is NetworkResult.Error) { + handleError(results.exception) + return@launch + } + if (results is NetworkResult.Success) { + _searchResults.postValue(results.data) + } + } catch (e: NetworkException) { + Log.e("NetworkException", e.message, e) + handleError(GitHubError.NetworkError(e)) + } + } + } + + /** + * エラーが発生した時に、Viewに問題を表示するためのもの + * @param error エラー情報 + */ + private fun handleError(error: GitHubError) { + _errorMessage.value = + when (error) { + is GitHubError.NetworkError -> R.string.network_error + is GitHubError.ApiError -> R.string.api_error + is GitHubError.ParseError -> R.string.parse_error + is GitHubError.RateLimitError -> R.string.rate_limit_error + is GitHubError.AuthenticationError -> R.string.auth_error + } + } + } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApi.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApi.kt index 8a51f27..fd0a220 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApi.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApi.kt @@ -3,5 +3,5 @@ package jp.co.yumemi.android.code_check.features.github.api import jp.co.yumemi.android.code_check.features.github.entity.RepositoryList interface GitHubServiceApi { - suspend fun getRepository(searchWord: String): RepositoryList + suspend fun getRepositoryList(searchWord: String): RepositoryList } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiBuilderInterface.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiBuilderInterface.kt index fcc38eb..59bb1fd 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiBuilderInterface.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiBuilderInterface.kt @@ -7,7 +7,7 @@ import retrofit2.http.Query interface GitHubServiceApiBuilderInterface { @GET("/search/repositories") - suspend fun getRepository( + suspend fun getRepositoryList( @Query("q") searchWord: String, ): Response } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiImpl.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiImpl.kt index e1997e0..a5026e2 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiImpl.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiImpl.kt @@ -40,8 +40,8 @@ class GitHubServiceApiImpl : GitHubServiceApi { .create(GitHubServiceApiBuilderInterface::class.java) } - override suspend fun getRepository(searchWord: String): RepositoryList { - val response = githubService.getRepository(searchWord) + override suspend fun getRepositoryList(searchWord: String): RepositoryList { + val response = githubService.getRepositoryList(searchWord) if (!response.isSuccessful) { throw when (response.code()) { diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepositoryImpl.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepositoryImpl.kt index 26b2d54..913363f 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepositoryImpl.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepositoryImpl.kt @@ -16,7 +16,7 @@ class GitHubServiceRepositoryImpl ) : GitHubServiceRepository { override suspend fun fetchSearchResults(inputText: String): NetworkResult> { return try { - val repositoryList = gitHubRepositoryApi.getRepository(inputText) + val repositoryList = gitHubRepositoryApi.getRepositoryList(inputText) val items = repositoryList.items.map { item -> RepositoryEntity( From 3ca8e44331653949dcc450e0d6c49ac5f3c2d3eb Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 17:50:02 +0900 Subject: [PATCH 08/28] =?UTF-8?q?add:=20=E8=A9=B3=E7=B4=B0=E3=82=92?= =?UTF-8?q?=E6=8A=BC=E3=81=97=E3=81=9F=E6=99=82=E3=81=AB=E3=80=81=E8=A9=B3?= =?UTF-8?q?=E7=B4=B0=E3=81=8C=E8=A1=A8=E7=A4=BA=E3=81=95=E3=82=8C=E3=82=8B?= =?UTF-8?q?=EF=BD=99ouni=20hennkou=20sita.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 2 + .../detail/RepositoryDetailScreen.kt | 214 +++++++++++++++++- .../detail/RepositoryDetailViewModel.kt | 14 +- .../search/RepositorySearchScreen.kt | 22 +- .../core/presenter/widget/ProgressCycle.kt | 32 +++ .../features/github/api/GitHubServiceApi.kt | 3 + .../api/GitHubServiceApiBuilderInterface.kt | 7 + .../github/api/GitHubServiceApiImpl.kt | 15 ++ .../reposiotory/GitHubServiceRepository.kt | 2 + .../GitHubServiceRepositoryImpl.kt | 30 +++ .../github/usecase/GitHubServiceUsecase.kt | 2 + .../usecase/GitHubServiceUsecaseImpl.kt | 7 + 12 files changed, 320 insertions(+), 30 deletions(-) create mode 100644 app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/ProgressCycle.kt diff --git a/app/build.gradle b/app/build.gradle index be0e0d6..d1c2efa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -94,6 +94,8 @@ dependencies { debugImplementation 'androidx.compose.ui:ui-tooling' implementation 'androidx.activity:activity-compose:1.10.0' implementation 'androidx.navigation:navigation-compose:2.8.5' + implementation "io.coil-kt:coil-compose:2.4.0" + implementation "androidx.compose.material:material-icons-extended:1.7.6" } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt index 01ed0b4..7dc58c4 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt @@ -1,13 +1,223 @@ package jp.co.yumemi.android.code_check.core.presenter.detail +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.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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Report +import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text 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.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import coil.compose.rememberAsyncImagePainter +import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity +import jp.co.yumemi.android.code_check.core.presenter.widget.ProgressCycle +import jp.co.yumemi.android.code_check.core.utils.DialogHelper @Composable fun RepositoryDetailScreen( toBack: () -> Unit, repositoryId: Int, + viewModel: RepositoryDetailViewModel = hiltViewModel(), ) { -// Text(text = "RepositoryDetailScreen") - Text(text = "repositoryId: $repositoryId") + val repositoryDetail = remember { mutableStateOf(null) } + val context = LocalContext.current + var isLoading by remember { mutableStateOf(false) } + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(Unit) { + viewModel.searchResults.observe(lifecycleOwner) { + repositoryDetail.value = it + isLoading = false + } + viewModel.errorMessage.observe(lifecycleOwner) { errorMessage -> + errorMessage?.let { + DialogHelper.showErrorDialog(context, context.getString(it)) + } + isLoading = false + } + viewModel.searchRepositories(repositoryId) + } + + RepositoryDetailScaffold( + isLoading = isLoading, + repositoryDetail = repositoryDetail.value, + toBack = toBack + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RepositoryDetailScaffold( + isLoading: Boolean, + repositoryDetail: RepositoryEntity?, + toBack: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(8.dp) + ) { + if (isLoading) { + ProgressCycle() + } else { + repositoryDetail?.let { + RepositoryDetailContent(repository = it) + } + } + } +} + +@Composable +fun RepositoryDetailContent(repository: RepositoryEntity) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + RepositoryOverviewCard(repository = repository) + RepositoryStatsCard(repository = repository) + } +} + +@Composable +fun RepositoryOverviewCard(repository: RepositoryEntity) { + Card( + modifier = Modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(4.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = rememberAsyncImagePainter(repository.ownerIconUrl), + contentDescription = "Owner Icon", + modifier = Modifier.size(80.dp) + ) + Text( + text = repository.name, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + Text( + text = "Language: ${repository.language}", + style = MaterialTheme.typography.bodyMedium + ) + } + } +} + +@Composable +fun RepositoryStatsCard(repository: RepositoryEntity) { + Card( + modifier = Modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(4.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Default.Star, // 適切なアイコンを選択 + contentDescription = "Stars", + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Stars: ${repository.stargazersCount}", fontSize = 18.sp, fontWeight = FontWeight.Medium) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Default.Share, // 適切なアイコンを選択 + contentDescription = "Forks", + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Forks: ${repository.forksCount}", fontSize = 18.sp, fontWeight = FontWeight.Medium) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Default.Report, // 適切なアイコンを選択 + contentDescription = "Open Issues", + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Open Issues: ${repository.openIssuesCount}", fontSize = 18.sp, fontWeight = FontWeight.Medium) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewRepositoryOverviewCard() { + RepositoryOverviewCard( + repository = RepositoryEntity( + id = 1, + name = "Example Repo", + ownerIconUrl = "https://via.placeholder.com/150", + language = "Kotlin", + stargazersCount = 123, + forksCount = 45, + openIssuesCount = 2, + watchersCount = 1 + ) + ) +} + +@Preview(showBackground = true) +@Composable +fun PreviewRepositoryStatsCard() { + RepositoryStatsCard( + repository = RepositoryEntity( + id = 1, + name = "Example Repo", + ownerIconUrl = "https://via.placeholder.com/150", + language = "Kotlin", + stargazersCount = 123, + forksCount = 45, + openIssuesCount = 2, + watchersCount = 1 + ) + ) } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt index ab2eafb..f93a8e0 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt @@ -24,21 +24,21 @@ RepositoryDetailViewModel@Inject private val _errorMessage = MutableLiveData() val errorMessage: LiveData get() = _errorMessage - private val _searchResults = MutableLiveData>() - val searchResults: LiveData> get() = _searchResults + private val _searchResults = MutableLiveData() + val searchResults: LiveData get() = _searchResults /** * GitHubのレポジトリ検索を行う - * @param query 検索キーワード + * @param id 検索キーワード */ - fun searchRepositories(query: String) { - if (query.isBlank()) { - _errorMessage.postValue(R.string.form_is_empty) + fun searchRepositories(id: Int) { + if (id == 0) { + _errorMessage.postValue(R.string.api_error) return } viewModelScope.launch { try { - val results = networkRepository.fetchSearchResults(query) + val results = networkRepository.fetchRepositoryDetail(id) if (results is NetworkResult.Error) { handleError(results.exception) return@launch diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt index ee29549..0db0829 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt @@ -2,10 +2,8 @@ package jp.co.yumemi.android.code_check.core.presenter.search import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -15,7 +13,6 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.sharp.Search -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -29,7 +26,6 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController @@ -44,6 +40,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import jp.co.yumemi.android.code_check.R import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.core.presenter.theme.CodeCheckAppTheme +import jp.co.yumemi.android.code_check.core.presenter.widget.ProgressCycle import jp.co.yumemi.android.code_check.core.utils.DialogHelper @Composable @@ -94,23 +91,6 @@ fun RepositorySearchScreen( } } -@Composable -fun ProgressCycle() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = - Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(8.dp) - .semantics { isTraversalGroup = true }, - ) { - CircularProgressIndicator() - Text(text = "検索中") - } -} - @Composable fun RepositoryListView( repositoryList: List, diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/ProgressCycle.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/ProgressCycle.kt new file mode 100644 index 0000000..5de3eb6 --- /dev/null +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/ProgressCycle.kt @@ -0,0 +1,32 @@ +package jp.co.yumemi.android.code_check.core.presenter.widget + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp + +@Composable +fun ProgressCycle() { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(8.dp) + .semantics { isTraversalGroup = true }, + ) { + CircularProgressIndicator() + Text(text = "検索中") + } +} diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApi.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApi.kt index fd0a220..721b856 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApi.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApi.kt @@ -1,7 +1,10 @@ package jp.co.yumemi.android.code_check.features.github.api +import jp.co.yumemi.android.code_check.features.github.entity.RepositoryItem import jp.co.yumemi.android.code_check.features.github.entity.RepositoryList interface GitHubServiceApi { suspend fun getRepositoryList(searchWord: String): RepositoryList + + suspend fun getRepositoryDetail(id: Int): RepositoryItem } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiBuilderInterface.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiBuilderInterface.kt index 59bb1fd..fd0ddba 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiBuilderInterface.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiBuilderInterface.kt @@ -1,8 +1,10 @@ package jp.co.yumemi.android.code_check.features.github.api +import jp.co.yumemi.android.code_check.features.github.entity.RepositoryItem import jp.co.yumemi.android.code_check.features.github.entity.RepositoryList import retrofit2.Response import retrofit2.http.GET +import retrofit2.http.Path import retrofit2.http.Query interface GitHubServiceApiBuilderInterface { @@ -10,4 +12,9 @@ interface GitHubServiceApiBuilderInterface { suspend fun getRepositoryList( @Query("q") searchWord: String, ): Response + + @GET("/repositories/{id}") + suspend fun getRepositoryDetail( + @Path("id") id: Int, + ): Response } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiImpl.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiImpl.kt index a5026e2..edff3be 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiImpl.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/api/GitHubServiceApiImpl.kt @@ -3,6 +3,7 @@ package jp.co.yumemi.android.code_check.features.github.api import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import jp.co.yumemi.android.code_check.BuildConfig +import jp.co.yumemi.android.code_check.features.github.entity.RepositoryItem import jp.co.yumemi.android.code_check.features.github.entity.RepositoryList import jp.co.yumemi.android.code_check.features.github.reposiotory.NetworkException import okhttp3.OkHttpClient @@ -53,4 +54,18 @@ class GitHubServiceApiImpl : GitHubServiceApi { } return response.body() ?: throw NetworkException("レスポンスが空でした") } + + override suspend fun getRepositoryDetail(id: Int): RepositoryItem { + val response = githubService.getRepositoryDetail(id) + + if (!response.isSuccessful) { + throw when (response.code()) { + 404 -> NetworkException("リポジトリが見つかりませんでした") + 403 -> NetworkException("APIレート制限に達しました") + 500 -> NetworkException("サーバーエラーが発生しました") + else -> NetworkException("エラーが発生しました: ${response.code()}") + } + } + return response.body() ?: throw NetworkException("レスポンスが空でした") + } } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepository.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepository.kt index 8e7e161..9641df3 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepository.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepository.kt @@ -5,4 +5,6 @@ import jp.co.yumemi.android.code_check.features.github.utils.NetworkResult interface GitHubServiceRepository { suspend fun fetchSearchResults(inputText: String): NetworkResult> + + suspend fun fetchRepositoryDetail(id: Int): NetworkResult } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepositoryImpl.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepositoryImpl.kt index 913363f..db846f4 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepositoryImpl.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/reposiotory/GitHubServiceRepositoryImpl.kt @@ -45,6 +45,36 @@ class GitHubServiceRepositoryImpl NetworkResult.Error(GitHubError.NetworkError(e)) } } + + override suspend fun fetchRepositoryDetail(id: Int): NetworkResult { + return try { + val repositoryDetail = gitHubRepositoryApi.getRepositoryDetail(id) + val repositoryEntity = + RepositoryEntity( + name = repositoryDetail.name, + ownerIconUrl = repositoryDetail.owner.avatarUrl, + language = repositoryDetail.language ?: "none", + stargazersCount = repositoryDetail.stargazersCount, + watchersCount = repositoryDetail.watchersCount, + forksCount = repositoryDetail.forksCount, + openIssuesCount = repositoryDetail.openIssuesCount, + id = repositoryDetail.id, + ) + NetworkResult.Success(repositoryEntity) + } catch (e: HttpException) { + val error = + when (e.code()) { + 429 -> GitHubError.RateLimitError + 401 -> GitHubError.AuthenticationError + else -> GitHubError.ApiError(e.code(), e.message()) + } + NetworkResult.Error(error) + } catch (e: JSONException) { + NetworkResult.Error(GitHubError.ParseError(e)) + } catch (e: IOException) { + NetworkResult.Error(GitHubError.NetworkError(e)) + } + } } class NetworkException(message: String, cause: Throwable? = null) : Exception(message, cause) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecase.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecase.kt index 50301ea..ad87471 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecase.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecase.kt @@ -5,4 +5,6 @@ import jp.co.yumemi.android.code_check.features.github.utils.NetworkResult interface GitHubServiceUsecase { suspend fun fetchSearchResults(inputText: String): NetworkResult> + + suspend fun fetchRepositoryDetail(id: Int): NetworkResult } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecaseImpl.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecaseImpl.kt index f16b7ca..1e388cf 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecaseImpl.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/features/github/usecase/GitHubServiceUsecaseImpl.kt @@ -19,4 +19,11 @@ class GitHubServiceUsecaseImpl } return repository.fetchSearchResults(inputText) } + + override suspend fun fetchRepositoryDetail(id: Int): NetworkResult { + if (!networkConnectivityService.isNetworkAvailable()) { + throw NetworkException("オフライン状態です") + } + return repository.fetchRepositoryDetail(id) + } } From e01f44ca6828a03ade5d4e456c65a9ee42746336 Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 18:08:45 +0900 Subject: [PATCH 09/28] =?UTF-8?q?add:=20=E8=A9=B3=E7=B4=B0=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=E3=81=AB=E8=A1=8C=E3=81=8F=E3=81=A8=E3=81=8D=E3=81=AB?= =?UTF-8?q?=E3=80=81AppBar=E3=81=AB=E6=88=BB=E3=82=8B=E3=83=9C=E3=82=BF?= =?UTF-8?q?=E3=83=B3=E3=82=92=E7=94=A8=E6=84=8F=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code_check/core/presenter/MainScreen.kt | 34 ++++++++++++++++++- .../core/presenter/router/MainRouter.kt | 14 ++++++-- .../core/presenter/widget/EmptyCompose.kt | 8 +++++ app/src/main/res/values/strings.xml | 2 ++ 4 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/EmptyCompose.kt diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt index b888478..547414a 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt @@ -1,7 +1,12 @@ package jp.co.yumemi.android.code_check.core.presenter import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -11,13 +16,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight +import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import jp.co.yumemi.android.code_check.R import jp.co.yumemi.android.code_check.core.presenter.router.BottomNavigationBarRoute import jp.co.yumemi.android.code_check.core.presenter.router.MainRouter +import jp.co.yumemi.android.code_check.core.presenter.widget.EmptyCompose @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -26,10 +34,29 @@ fun MainScreen() { val appName = context.getString(R.string.app_name) val navController = rememberNavController() - val topBarTitle by remember { + val navBackStackEntry by navController.currentBackStackEntryAsState() + var topBarTitle by remember { mutableStateOf(appName) } + val navigationIcon: @Composable () -> Unit = + if (navBackStackEntry?.destination?.route != BottomNavigationBarRoute.SEARCH.route) { + { + IconButton(onClick = { + navController.popBackStack() + }) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = "Back" + ) + } + } + } else { + { + EmptyCompose() + } + } + Scaffold( topBar = { TopAppBar( @@ -44,6 +71,8 @@ fun MainScreen() { containerColor = MaterialTheme.colorScheme.primaryContainer, titleContentColor = MaterialTheme.colorScheme.primary, ), + navigationIcon = navigationIcon + ) }, ) { innerPadding -> @@ -54,6 +83,9 @@ fun MainScreen() { toBackScreen = { navController.popBackStack() }, + changeTopBarTitle = { + topBarTitle = it + }, navController = navController, modifier = Modifier diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt index 6ddcdb8..0238693 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt @@ -3,11 +3,13 @@ package jp.co.yumemi.android.code_check.core.presenter.router import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument +import jp.co.yumemi.android.code_check.R import jp.co.yumemi.android.code_check.core.presenter.detail.RepositoryDetailScreen import jp.co.yumemi.android.code_check.core.presenter.search.RepositorySearchScreen @@ -15,9 +17,13 @@ import jp.co.yumemi.android.code_check.core.presenter.search.RepositorySearchScr fun MainRouter( toDetailScreen: (Int) -> Unit, toBackScreen: () -> Unit, + changeTopBarTitle: (String) -> Unit, navController: NavHostController, modifier: Modifier = Modifier, ) { + + val context = LocalContext.current + NavHost( navController = navController, startDestination = BottomNavigationBarRoute.SEARCH.route, @@ -27,6 +33,7 @@ fun MainRouter( RepositorySearchScreen( toDetailScreen = toDetailScreen, ) + changeTopBarTitle(context.getString(R.string.app_name)) } composable( BottomNavigationBarRoute.DETAIL.route + "/{id}", @@ -37,11 +44,12 @@ fun MainRouter( toBack = toBackScreen, repositoryId = id ?: 0, ) + changeTopBarTitle(context.getString(R.string.detail)) } } } -enum class BottomNavigationBarRoute(val route: String, val title: String) { - SEARCH("search", "検索"), - DETAIL("detail", "詳細"), +enum class BottomNavigationBarRoute(val route: String, val title: Int) { + SEARCH("search", R.string.search), + DETAIL("detail", R.string.detail), } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/EmptyCompose.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/EmptyCompose.kt new file mode 100644 index 0000000..b4dc0b9 --- /dev/null +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/EmptyCompose.kt @@ -0,0 +1,8 @@ +package jp.co.yumemi.android.code_check.core.presenter.widget + +import androidx.compose.runtime.Composable + +@Composable +fun EmptyCompose(){ + +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a665d57..3b6a633 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,4 +12,6 @@ セッションの時間切れ APIのエラー フォームが空欄になっています + 詳細 + 検索 \ No newline at end of file From 5f6e98c5765f44492aed54995f57364f4ee42229 Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 18:21:12 +0900 Subject: [PATCH 10/28] =?UTF-8?q?add:=20=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=95=E3=81=9B=E3=82=8B=E6=99=82?= =?UTF-8?q?=E3=81=AB=E3=80=81=E3=82=B9=E3=83=8A=E3=83=83=E3=82=AF=E3=83=90?= =?UTF-8?q?=E3=83=BC=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code_check/core/presenter/MainScreen.kt | 32 +++++++++++++++++++ .../detail/RepositoryDetailScreen.kt | 15 ++++++++- .../core/presenter/router/MainRouter.kt | 3 ++ .../search/RepositorySearchScreen.kt | 5 +-- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt index 547414a..7104f6b 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt @@ -9,6 +9,9 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults @@ -16,6 +19,7 @@ import androidx.compose.runtime.Composable 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.Modifier import androidx.compose.ui.platform.LocalContext @@ -26,6 +30,7 @@ import jp.co.yumemi.android.code_check.R import jp.co.yumemi.android.code_check.core.presenter.router.BottomNavigationBarRoute import jp.co.yumemi.android.code_check.core.presenter.router.MainRouter import jp.co.yumemi.android.code_check.core.presenter.widget.EmptyCompose +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -39,6 +44,10 @@ fun MainScreen() { mutableStateOf(appName) } + val hostState = remember { SnackbarHostState() } + var isErrorMessage by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + val navigationIcon: @Composable () -> Unit = if (navBackStackEntry?.destination?.route != BottomNavigationBarRoute.SEARCH.route) { { @@ -75,6 +84,7 @@ fun MainScreen() { ) }, + snackbarHost = {CustomSnackbarHost(hostState = hostState, isErrorMessage = isErrorMessage)} ) { innerPadding -> MainRouter( toDetailScreen = { id -> @@ -86,6 +96,12 @@ fun MainScreen() { changeTopBarTitle = { topBarTitle = it }, + showSnackbar = { message, isError -> + scope.launch { + isErrorMessage = isError + hostState.showSnackbar(message) + } + }, navController = navController, modifier = Modifier @@ -93,3 +109,19 @@ fun MainScreen() { ) } } + +@Composable +fun CustomSnackbarHost( + hostState: SnackbarHostState, + isErrorMessage: Boolean +) { + SnackbarHost( + hostState = hostState + ) { snackbarData -> + Snackbar( + snackbarData = snackbarData, + containerColor = if (isErrorMessage) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primaryContainer, + contentColor = if (isErrorMessage) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.onPrimaryContainer + ) + } +} diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt index 7dc58c4..3c70e0a 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt @@ -6,6 +6,7 @@ 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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -48,6 +49,7 @@ fun RepositoryDetailScreen( toBack: () -> Unit, repositoryId: Int, viewModel: RepositoryDetailViewModel = hiltViewModel(), + showSnackBar: (String, Boolean) -> Unit ) { val repositoryDetail = remember { mutableStateOf(null) } val context = LocalContext.current @@ -61,13 +63,24 @@ fun RepositoryDetailScreen( } viewModel.errorMessage.observe(lifecycleOwner) { errorMessage -> errorMessage?.let { - DialogHelper.showErrorDialog(context, context.getString(it)) + showSnackBar(context.getString(it), true) } isLoading = false } viewModel.searchRepositories(repositoryId) } + if(repositoryDetail.value == null && !isLoading){ + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth().fillMaxHeight() + ){ + Text(text = "データの取得に失敗しました。") + + } + } + RepositoryDetailScaffold( isLoading = isLoading, repositoryDetail = repositoryDetail.value, diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt index 0238693..d3fdbb0 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt @@ -18,6 +18,7 @@ fun MainRouter( toDetailScreen: (Int) -> Unit, toBackScreen: () -> Unit, changeTopBarTitle: (String) -> Unit, + showSnackbar: (String, Boolean) -> Unit, navController: NavHostController, modifier: Modifier = Modifier, ) { @@ -32,6 +33,7 @@ fun MainRouter( composable(BottomNavigationBarRoute.SEARCH.route) { RepositorySearchScreen( toDetailScreen = toDetailScreen, + showSnackBar = showSnackbar ) changeTopBarTitle(context.getString(R.string.app_name)) } @@ -43,6 +45,7 @@ fun MainRouter( RepositoryDetailScreen( toBack = toBackScreen, repositoryId = id ?: 0, + showSnackBar = showSnackbar ) changeTopBarTitle(context.getString(R.string.detail)) } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt index 0db0829..515c413 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt @@ -47,6 +47,7 @@ import jp.co.yumemi.android.code_check.core.utils.DialogHelper fun RepositorySearchScreen( toDetailScreen: (Int) -> Unit, viewModel: RepositorySearchViewModel = hiltViewModel(), + showSnackBar:(String, Boolean) -> Unit ) { var inputText by remember { mutableStateOf("") } val repositoryList = remember { mutableStateListOf() } @@ -62,9 +63,9 @@ fun RepositorySearchScreen( } viewModel.errorMessage.observe(lifecycleOwner) { it?.let { - DialogHelper.showErrorDialog( - context, + showSnackBar( context.getString(it), + true ) } isLoading = false From acd828f4227a8c606501612a3c658b30ce576f18 Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 18:24:20 +0900 Subject: [PATCH 11/28] =?UTF-8?q?ref:=20=E3=83=AA=E3=83=B3=E3=82=BF?= =?UTF-8?q?=E3=83=BC=E3=81=AE=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code_check/core/presenter/MainScreen.kt | 13 +-- .../detail/RepositoryDetailScreen.kt | 107 +++++++++--------- .../core/presenter/router/MainRouter.kt | 5 +- .../search/RepositorySearchScreen.kt | 5 +- .../core/presenter/widget/EmptyCompose.kt | 5 +- 5 files changed, 67 insertions(+), 68 deletions(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt index 7104f6b..6e9794d 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/MainScreen.kt @@ -56,7 +56,7 @@ fun MainScreen() { }) { Icon( imageVector = Icons.AutoMirrored.Outlined.ArrowBack, - contentDescription = "Back" + contentDescription = "Back", ) } } @@ -80,11 +80,10 @@ fun MainScreen() { containerColor = MaterialTheme.colorScheme.primaryContainer, titleContentColor = MaterialTheme.colorScheme.primary, ), - navigationIcon = navigationIcon - + navigationIcon = navigationIcon, ) }, - snackbarHost = {CustomSnackbarHost(hostState = hostState, isErrorMessage = isErrorMessage)} + snackbarHost = { CustomSnackbarHost(hostState = hostState, isErrorMessage = isErrorMessage) }, ) { innerPadding -> MainRouter( toDetailScreen = { id -> @@ -113,15 +112,15 @@ fun MainScreen() { @Composable fun CustomSnackbarHost( hostState: SnackbarHostState, - isErrorMessage: Boolean + isErrorMessage: Boolean, ) { SnackbarHost( - hostState = hostState + hostState = hostState, ) { snackbarData -> Snackbar( snackbarData = snackbarData, containerColor = if (isErrorMessage) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primaryContainer, - contentColor = if (isErrorMessage) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.onPrimaryContainer + contentColor = if (isErrorMessage) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.onPrimaryContainer, ) } } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt index 3c70e0a..bfdc837 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt @@ -42,14 +42,13 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import coil.compose.rememberAsyncImagePainter import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.core.presenter.widget.ProgressCycle -import jp.co.yumemi.android.code_check.core.utils.DialogHelper @Composable fun RepositoryDetailScreen( toBack: () -> Unit, repositoryId: Int, viewModel: RepositoryDetailViewModel = hiltViewModel(), - showSnackBar: (String, Boolean) -> Unit + showSnackBar: (String, Boolean) -> Unit, ) { val repositoryDetail = remember { mutableStateOf(null) } val context = LocalContext.current @@ -70,21 +69,20 @@ fun RepositoryDetailScreen( viewModel.searchRepositories(repositoryId) } - if(repositoryDetail.value == null && !isLoading){ + if (repositoryDetail.value == null && !isLoading) { Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth().fillMaxHeight() - ){ + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + ) { Text(text = "データの取得に失敗しました。") - } } RepositoryDetailScaffold( isLoading = isLoading, repositoryDetail = repositoryDetail.value, - toBack = toBack + toBack = toBack, ) } @@ -93,12 +91,13 @@ fun RepositoryDetailScreen( fun RepositoryDetailScaffold( isLoading: Boolean, repositoryDetail: RepositoryEntity?, - toBack: () -> Unit + toBack: () -> Unit, ) { Box( - modifier = Modifier - .fillMaxSize() - .padding(8.dp) + modifier = + Modifier + .fillMaxSize() + .padding(8.dp), ) { if (isLoading) { ProgressCycle() @@ -113,11 +112,12 @@ fun RepositoryDetailScaffold( @Composable fun RepositoryDetailContent(repository: RepositoryEntity) { Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(16.dp) + modifier = + Modifier + .fillMaxSize() + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { RepositoryOverviewCard(repository = repository) RepositoryStatsCard(repository = repository) @@ -128,28 +128,29 @@ fun RepositoryDetailContent(repository: RepositoryEntity) { fun RepositoryOverviewCard(repository: RepositoryEntity) { Card( modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.cardElevation(4.dp) + elevation = CardDefaults.cardElevation(4.dp), ) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Image( painter = rememberAsyncImagePainter(repository.ownerIconUrl), contentDescription = "Owner Icon", - modifier = Modifier.size(80.dp) + modifier = Modifier.size(80.dp), ) Text( text = repository.name, style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, ) Text( text = "Language: ${repository.language}", - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyMedium, ) } } @@ -159,18 +160,18 @@ fun RepositoryOverviewCard(repository: RepositoryEntity) { fun RepositoryStatsCard(repository: RepositoryEntity) { Card( modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.cardElevation(4.dp) + elevation = CardDefaults.cardElevation(4.dp), ) { Column( modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + verticalArrangement = Arrangement.spacedBy(12.dp), ) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( - imageVector = Icons.Default.Star, // 適切なアイコンを選択 + imageVector = Icons.Default.Star, contentDescription = "Stars", modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.width(8.dp)) Text("Stars: ${repository.stargazersCount}", fontSize = 18.sp, fontWeight = FontWeight.Medium) @@ -178,10 +179,10 @@ fun RepositoryStatsCard(repository: RepositoryEntity) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( - imageVector = Icons.Default.Share, // 適切なアイコンを選択 + imageVector = Icons.Default.Share, contentDescription = "Forks", modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.width(8.dp)) Text("Forks: ${repository.forksCount}", fontSize = 18.sp, fontWeight = FontWeight.Medium) @@ -189,10 +190,10 @@ fun RepositoryStatsCard(repository: RepositoryEntity) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( - imageVector = Icons.Default.Report, // 適切なアイコンを選択 + imageVector = Icons.Default.Report, contentDescription = "Open Issues", modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary + tint = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.width(8.dp)) Text("Open Issues: ${repository.openIssuesCount}", fontSize = 18.sp, fontWeight = FontWeight.Medium) @@ -205,16 +206,17 @@ fun RepositoryStatsCard(repository: RepositoryEntity) { @Composable fun PreviewRepositoryOverviewCard() { RepositoryOverviewCard( - repository = RepositoryEntity( - id = 1, - name = "Example Repo", - ownerIconUrl = "https://via.placeholder.com/150", - language = "Kotlin", - stargazersCount = 123, - forksCount = 45, - openIssuesCount = 2, - watchersCount = 1 - ) + repository = + RepositoryEntity( + id = 1, + name = "Example Repo", + ownerIconUrl = "https://via.placeholder.com/150", + language = "Kotlin", + stargazersCount = 123, + forksCount = 45, + openIssuesCount = 2, + watchersCount = 1, + ), ) } @@ -222,15 +224,16 @@ fun PreviewRepositoryOverviewCard() { @Composable fun PreviewRepositoryStatsCard() { RepositoryStatsCard( - repository = RepositoryEntity( - id = 1, - name = "Example Repo", - ownerIconUrl = "https://via.placeholder.com/150", - language = "Kotlin", - stargazersCount = 123, - forksCount = 45, - openIssuesCount = 2, - watchersCount = 1 - ) + repository = + RepositoryEntity( + id = 1, + name = "Example Repo", + ownerIconUrl = "https://via.placeholder.com/150", + language = "Kotlin", + stargazersCount = 123, + forksCount = 45, + openIssuesCount = 2, + watchersCount = 1, + ), ) } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt index d3fdbb0..aa6a49d 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/router/MainRouter.kt @@ -22,7 +22,6 @@ fun MainRouter( navController: NavHostController, modifier: Modifier = Modifier, ) { - val context = LocalContext.current NavHost( @@ -33,7 +32,7 @@ fun MainRouter( composable(BottomNavigationBarRoute.SEARCH.route) { RepositorySearchScreen( toDetailScreen = toDetailScreen, - showSnackBar = showSnackbar + showSnackBar = showSnackbar, ) changeTopBarTitle(context.getString(R.string.app_name)) } @@ -45,7 +44,7 @@ fun MainRouter( RepositoryDetailScreen( toBack = toBackScreen, repositoryId = id ?: 0, - showSnackBar = showSnackbar + showSnackBar = showSnackbar, ) changeTopBarTitle(context.getString(R.string.detail)) } diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt index 515c413..1b01faf 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt @@ -41,13 +41,12 @@ import jp.co.yumemi.android.code_check.R import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.core.presenter.theme.CodeCheckAppTheme import jp.co.yumemi.android.code_check.core.presenter.widget.ProgressCycle -import jp.co.yumemi.android.code_check.core.utils.DialogHelper @Composable fun RepositorySearchScreen( toDetailScreen: (Int) -> Unit, viewModel: RepositorySearchViewModel = hiltViewModel(), - showSnackBar:(String, Boolean) -> Unit + showSnackBar: (String, Boolean) -> Unit, ) { var inputText by remember { mutableStateOf("") } val repositoryList = remember { mutableStateListOf() } @@ -65,7 +64,7 @@ fun RepositorySearchScreen( it?.let { showSnackBar( context.getString(it), - true + true, ) } isLoading = false diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/EmptyCompose.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/EmptyCompose.kt index b4dc0b9..7546c96 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/EmptyCompose.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/EmptyCompose.kt @@ -3,6 +3,5 @@ package jp.co.yumemi.android.code_check.core.presenter.widget import androidx.compose.runtime.Composable @Composable -fun EmptyCompose(){ - -} \ No newline at end of file +fun EmptyCompose() { +} From 9baaee1ab837631b7d9b2897deaa0e9ec74e858a Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 18:25:24 +0900 Subject: [PATCH 12/28] =?UTF-8?q?ref:=20material3=E3=81=AE=E8=AD=A6?= =?UTF-8?q?=E5=91=8A=E8=A7=A3=E9=99=A4=E3=82=92=E6=B6=88=E5=8E=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code_check/core/presenter/detail/RepositoryDetailScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt index bfdc837..854e4f3 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt @@ -86,7 +86,6 @@ fun RepositoryDetailScreen( ) } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun RepositoryDetailScaffold( isLoading: Boolean, From 5c3c9e2e79ed0d0125b31550694674683b71a53a Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 18:46:01 +0900 Subject: [PATCH 13/28] =?UTF-8?q?add:=20=E8=A9=B3=E7=B4=B0=E5=88=86?= =?UTF-8?q?=E3=81=AEUnitTest=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=81=9F?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/code_check/ExampleUnitTest.kt | 16 -- .../features/github/GitHubMockData.kt | 132 +++++++++++++ .../github/GitHubServiceRepositoryImplTest.kt | 186 ++++++++++-------- .../github/GitHubServiceUsecaseImplTest.kt | 128 ++++++++---- 4 files changed, 322 insertions(+), 140 deletions(-) delete mode 100644 app/src/test/kotlin/jp/co/yumemi/android/code_check/ExampleUnitTest.kt create mode 100644 app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubMockData.kt diff --git a/app/src/test/kotlin/jp/co/yumemi/android/code_check/ExampleUnitTest.kt b/app/src/test/kotlin/jp/co/yumemi/android/code_check/ExampleUnitTest.kt deleted file mode 100644 index b435cbc..0000000 --- a/app/src/test/kotlin/jp/co/yumemi/android/code_check/ExampleUnitTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package jp.co.yumemi.android.code_check - -import org.junit.Assert.assertEquals -import org.junit.Test - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubMockData.kt b/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubMockData.kt new file mode 100644 index 0000000..823bc6b --- /dev/null +++ b/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubMockData.kt @@ -0,0 +1,132 @@ +package jp.co.yumemi.android.code_check.features.github + +import jp.co.yumemi.android.code_check.core.api.NetworkConnectivityService +import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity +import jp.co.yumemi.android.code_check.features.github.entity.RepositoryItem +import jp.co.yumemi.android.code_check.features.github.entity.RepositoryList +import jp.co.yumemi.android.code_check.features.github.entity.RepositoryOwner +import jp.co.yumemi.android.code_check.features.github.utils.GitHubError +import jp.co.yumemi.android.code_check.features.github.utils.NetworkResult +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` +import java.io.IOException + +object GitHubMockData { + + // 正常なリポジトリ検索結果 + fun getMockRepositoryList(): RepositoryList { + return RepositoryList( + items = listOf( + RepositoryItem( + name = "repo1", + owner = RepositoryOwner( + avatarUrl = "url1" + ), + language = "Kotlin", + stargazersCount = 100, + forksCount = 50, + openIssuesCount = 30, + watchersCount = 70, + id = 1 + ), + RepositoryItem( + name = "repo2", + owner = RepositoryOwner( + avatarUrl = "url2" + ), + language = "Java", + stargazersCount = 200, + forksCount = 80, + openIssuesCount = 20, + watchersCount = 60, + id = 2 + ) + ) + ) + } + + fun getMockRepositoryEntityList(): List { + return listOf( + RepositoryEntity( + name = "repo1", + ownerIconUrl = "url1", + language = "Kotlin", + stargazersCount = 100, + forksCount = 50, + openIssuesCount = 30, + watchersCount = 70, + id = 1 + ), + RepositoryEntity( + name = "repo2", + ownerIconUrl = "url2", + language = "Java", + stargazersCount = 200, + forksCount = 80, + openIssuesCount = 20, + watchersCount = 60, + id = 2 + ) + ) + } + + // 正常なリポジトリ詳細 + fun getMockRepositoryItem(): RepositoryItem { + return RepositoryItem( + name = "repo1", + language = "Kotlin", + stargazersCount = 100, + forksCount = 50, + openIssuesCount = 30, + watchersCount = 70, + id = 1, + owner = RepositoryOwner( + avatarUrl = "url1" + ) + ) + } + + fun getMockRepositoryEntity(): RepositoryEntity { + return RepositoryEntity( + name = "repo1", + ownerIconUrl = "url1", + language = "Kotlin", + stargazersCount = 100, + forksCount = 50, + openIssuesCount = 30, + watchersCount = 70, + id = 1 + ) + } + + + + // ネットワークエラー + fun getMockNetworkError(): NetworkResult.Error { + return NetworkResult.Error(GitHubError.NetworkError(IOException("Network issue"))) + } + + // APIエラー (例えば404) + fun getMockApiError404(): NetworkResult.Error { + return NetworkResult.Error(GitHubError.ApiError(404, "Not Found")) + } + + // APIエラー (例えば500) + fun getMockApiError500(): NetworkResult.Error { + return NetworkResult.Error(GitHubError.ApiError(500, "Internal Server Error")) + } + + // オフライン時のネットワーク接続サービス + fun getMockOfflineNetworkService(): NetworkConnectivityService { + val networkService = mock(NetworkConnectivityService::class.java) + `when`(networkService.isNetworkAvailable()).thenReturn(false) + return networkService + } + + // オンライン時のネットワーク接続サービス + fun getMockOnlineNetworkService(): NetworkConnectivityService { + val networkService = mock(NetworkConnectivityService::class.java) + `when`(networkService.isNetworkAvailable()).thenReturn(true) + return networkService + } +} diff --git a/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceRepositoryImplTest.kt b/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceRepositoryImplTest.kt index d9eb4b7..0dddb6c 100644 --- a/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceRepositoryImplTest.kt +++ b/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceRepositoryImplTest.kt @@ -10,8 +10,8 @@ import jp.co.yumemi.android.code_check.features.github.utils.NetworkResult import kotlinx.coroutines.runBlocking import org.json.JSONException import org.junit.Assert.assertEquals -import org.junit.Before import org.junit.Test +import org.junit.Before import org.mockito.Mockito.mock import org.mockito.Mockito.`when` import retrofit2.HttpException @@ -28,98 +28,116 @@ class GitHubServiceRepositoryImplTest { } @Test - fun `fetchSearchResults returns success with valid data`() = - runBlocking { - // Arrange - val mockResponse = mockRepositoryList() - `when`(api.getRepository("test")).thenReturn(mockResponse) - - // Act - val result = repository.fetchSearchResults("test") - - // Assert - assert(result is NetworkResult.Success) - val success = result as NetworkResult.Success - assertEquals(2, success.data.size) - assertEquals("repo1", success.data[0].name) - assertEquals("owner1", success.data[0].ownerIconUrl) - } + fun `fetchSearchResults returns success with valid data`() = runBlocking { + // Arrange + val mockResponse = GitHubMockData.getMockRepositoryList() + `when`(api.getRepositoryList("test")).thenReturn(mockResponse) + + // Act + val result = repository.fetchSearchResults("test") + + // Assert + assert(result is NetworkResult.Success) + val success = result as NetworkResult.Success + assertEquals(2, success.data.size) + assertEquals("repo1", success.data[0].name) + assertEquals("url1", success.data[0].ownerIconUrl) // 修正: ownerIconUrl -> avatarUrl + } + @Test - fun `fetchSearchResults returns error on HttpException`() = - runBlocking { - // Arrange - val httpException = mock(HttpException::class.java) - `when`(httpException.code()).thenReturn(401) - `when`(api.getRepository("test")).thenThrow(httpException) - - // Act - val result = repository.fetchSearchResults("test") - - // Assert - assert(result is NetworkResult.Error) - val error = result as NetworkResult.Error - assert(error.exception is GitHubError.AuthenticationError) + fun `fetchSearchResults returns error on HttpException`() = runBlocking { + // Arrange + val httpException = mock(HttpException::class.java) + `when`(httpException.code()).thenReturn(401) + `when`(api.getRepositoryList("test")).thenThrow(httpException) + + // Act + val result = repository.fetchSearchResults("test") + + // Assert + assert(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assert(error.exception is GitHubError.AuthenticationError) + } + + @Test + fun `fetchSearchResults returns error on IOException`() = runBlocking { + // Arrange + `when`(api.getRepositoryList("test")).thenAnswer { + throw IOException("Network error") } + // Act + val result = repository.fetchSearchResults("test") + + // Assert + assert(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assert(error.exception is GitHubError.NetworkError) + } + @Test - fun `fetchSearchResults returns error on IOException`() = - runBlocking { - // Arrange - `when`(api.getRepository("test")).thenAnswer { - throw IOException("Network error") - } - - // Act - val result = repository.fetchSearchResults("test") - - // Assert - assert(result is NetworkResult.Error) - val error = result as NetworkResult.Error - assert(error.exception is GitHubError.NetworkError) + fun `fetchSearchResults returns error on JSONException`() = runBlocking { + // Arrange + `when`(api.getRepositoryList("test")).thenAnswer { + throw JSONException("Parsing error") } + // Act + val result = repository.fetchSearchResults("test") + + // Assert + assert(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assert(error.exception is GitHubError.ParseError) + } + + @Test + fun `fetchRepositoryDetail returns success with valid data`() = runBlocking { + // Arrange + val mockDetail = GitHubMockData.getMockRepositoryItem() + `when`(api.getRepositoryDetail(1)).thenReturn(mockDetail) + + // Act + val result = repository.fetchRepositoryDetail(1) + + // Assert + assert(result is NetworkResult.Success) + val success = result as NetworkResult.Success + assertEquals("repo1", success.data.name) + assertEquals("url1", success.data.ownerIconUrl) + } + @Test - fun `fetchSearchResults returns error on JSONException`() = - runBlocking { - // Arrange - `when`(api.getRepository("test")).thenAnswer { - throw JSONException("Parsing error") - } - - // Act - val result = repository.fetchSearchResults("test") - - // Assert - assert(result is NetworkResult.Error) - val error = result as NetworkResult.Error - assert(error.exception is GitHubError.ParseError) + fun `fetchRepositoryDetail returns error on HttpException`() = runBlocking { + // Arrange + val httpException = mock(HttpException::class.java) + `when`(httpException.code()).thenReturn(429) + `when`(api.getRepositoryDetail(1)).thenThrow(httpException) + + // Act + val result = repository.fetchRepositoryDetail(1) + + // Assert + assert(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assert(error.exception is GitHubError.RateLimitError) + } + + @Test + fun `fetchRepositoryDetail returns error on IOException`() = runBlocking { + // Arrange + `when`(api.getRepositoryDetail(1)).thenAnswer { + throw IOException("Network error") } - // Mockデータ生成関数 - private fun mockRepositoryList(): RepositoryList { - return RepositoryList( - items = - listOf( - RepositoryItem( - name = "repo1", - owner = RepositoryOwner("owner1"), - language = "Kotlin", - stargazersCount = 100, - watchersCount = 50, - forksCount = 20, - openIssuesCount = 5, - ), - RepositoryItem( - name = "repo2", - owner = RepositoryOwner("owner2"), - language = "Java", - stargazersCount = 200, - watchersCount = 80, - forksCount = 30, - openIssuesCount = 10, - ), - ), - ) + // Act + val result = repository.fetchRepositoryDetail(1) + + // Assert + assert(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assert(error.exception is GitHubError.NetworkError) } } diff --git a/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceUsecaseImplTest.kt b/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceUsecaseImplTest.kt index 1670272..a0812d0 100644 --- a/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceUsecaseImplTest.kt +++ b/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceUsecaseImplTest.kt @@ -5,15 +5,13 @@ import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.features.github.reposiotory.GitHubServiceRepository import jp.co.yumemi.android.code_check.features.github.reposiotory.NetworkException import jp.co.yumemi.android.code_check.features.github.usecase.GitHubServiceUsecaseImpl +import jp.co.yumemi.android.code_check.features.github.utils.GitHubError import jp.co.yumemi.android.code_check.features.github.utils.NetworkResult import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertThrows +import org.junit.Assert.* import org.junit.Before import org.junit.Test -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations +import org.mockito.Mockito.* class GitHubServiceUsecaseImplTest { private lateinit var repository: GitHubServiceRepository @@ -22,7 +20,6 @@ class GitHubServiceUsecaseImplTest { @Before fun setUp() { - MockitoAnnotations.openMocks(this) repository = mock(GitHubServiceRepository::class.java) networkConnectivityService = mock(NetworkConnectivityService::class.java) usecase = GitHubServiceUsecaseImpl(repository, networkConnectivityService) @@ -32,45 +29,96 @@ class GitHubServiceUsecaseImplTest { fun `fetchSearchResults throws NetworkException when offline`() { `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(false) - val exception = - assertThrows(NetworkException::class.java) { - runBlocking { - usecase.fetchSearchResults("test") - } + val exception = assertThrows(NetworkException::class.java) { + runBlocking { + usecase.fetchSearchResults("test") } + } assertEquals("オフライン状態です", exception.message) } @Test - fun `fetchSearchResults returns results when online`() = - runBlocking { - val mockResults = - listOf( - RepositoryEntity( - name = "repo1", - ownerIconUrl = "description1", - language = "url1", - stargazersCount = 100, - forksCount = 50, - openIssuesCount = 30, - watchersCount = 70, - ), - RepositoryEntity( - name = "repo2", - ownerIconUrl = "description2", - language = "url2", - stargazersCount = 100, - forksCount = 50, - openIssuesCount = 30, - watchersCount = 70, - ), - ) - `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) - `when`(repository.fetchSearchResults("test")).thenReturn(NetworkResult.Success(mockResults)) - - val result = usecase.fetchSearchResults("test") - - assertEquals(NetworkResult.Success(mockResults), result) + fun `fetchSearchResults returns success when online`() = runBlocking { + val mockResults = GitHubMockData.getMockRepositoryEntityList() + + `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) + `when`(repository.fetchSearchResults("test")).thenReturn(NetworkResult.Success(mockResults)) + + val result = usecase.fetchSearchResults("test") + + assertTrue(result is NetworkResult.Success) + val success = result as NetworkResult.Success + assertEquals(2, success.data.size) + assertEquals("repo1", success.data[0].name) + assertEquals("repo2", success.data[1].name) + } + + @Test + fun `fetchSearchResults returns error on API failure`() = runBlocking { + val mockError = GitHubMockData.getMockApiError404() + `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) + `when`(repository.fetchSearchResults("test")).thenReturn(mockError) + + val result = usecase.fetchSearchResults("test") + + assertTrue(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assertTrue(error.exception is GitHubError.ApiError) + assertEquals(404, (error.exception as GitHubError.ApiError).code) + } + + @Test + fun `fetchRepositoryDetail throws NetworkException when offline`() { + `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(false) + + val exception = assertThrows(NetworkException::class.java) { + runBlocking { + usecase.fetchRepositoryDetail(1) + } } + + assertEquals("オフライン状態です", exception.message) + } + + @Test + fun `fetchRepositoryDetail returns success when online`() = runBlocking { + val mockDetail = GitHubMockData.getMockRepositoryEntity() + + `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) + `when`(repository.fetchRepositoryDetail(1)).thenReturn(NetworkResult.Success(mockDetail)) + + val result = usecase.fetchRepositoryDetail(1) + + assertTrue(result is NetworkResult.Success) + val success = result as NetworkResult.Success + assertEquals("repo1", success.data.name) + } + + @Test + fun `fetchRepositoryDetail returns error on API failure`() = runBlocking { + val mockError = GitHubMockData.getMockApiError500() + `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) + `when`(repository.fetchRepositoryDetail(1)).thenReturn(mockError) + + val result = usecase.fetchRepositoryDetail(1) + + assertTrue(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assertTrue(error.exception is GitHubError.ApiError) + assertEquals(500, (error.exception as GitHubError.ApiError).code) + } + + @Test + fun `fetchRepositoryDetail handles network error`() = runBlocking { + val mockError = GitHubMockData.getMockNetworkError() + `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) + `when`(repository.fetchRepositoryDetail(1)).thenReturn(mockError) + + val result = usecase.fetchRepositoryDetail(1) + + assertTrue(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assertTrue(error.exception is GitHubError.NetworkError) + } } From 23d30502784007583c1fd5114e4deb18bd38ec9f Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 18:50:28 +0900 Subject: [PATCH 14/28] =?UTF-8?q?delete:=20fragment=E3=81=AE=E6=99=82?= =?UTF-8?q?=E3=81=AE=E8=A8=98=E8=BF=B0=E3=82=92=E3=81=99=E3=81=B9=E3=81=A6?= =?UTF-8?q?=E6=B6=88=E5=8E=BB=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/RepositoryDetailFragment.kt | 49 -------- .../RepositoryListRecyclerViewAdapter.kt | 74 ------------ .../search/RepositorySearchFragment.kt | 97 ---------------- app/src/main/res/layout/activity_top.xml | 22 ---- .../res/layout/fragment_repository_detail.xml | 108 ------------------ .../res/layout/fragment_repository_search.xml | 60 ---------- app/src/main/res/layout/layout_item.xml | 21 ---- app/src/main/res/navigation/nav_graph.xml | 28 ----- 8 files changed, 459 deletions(-) delete mode 100644 app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailFragment.kt delete mode 100644 app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositoryListRecyclerViewAdapter.kt delete mode 100644 app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchFragment.kt delete mode 100644 app/src/main/res/layout/activity_top.xml delete mode 100644 app/src/main/res/layout/fragment_repository_detail.xml delete mode 100644 app/src/main/res/layout/fragment_repository_search.xml delete mode 100644 app/src/main/res/layout/layout_item.xml delete mode 100644 app/src/main/res/navigation/nav_graph.xml diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailFragment.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailFragment.kt deleted file mode 100644 index d535f66..0000000 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailFragment.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright © 2021 YUMEMI Inc. All rights reserved. - */ -package jp.co.yumemi.android.code_check.core.presenter.detail - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.navArgs -import coil.load -import dagger.hilt.android.AndroidEntryPoint -import jp.co.yumemi.android.code_check.R -import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity -import jp.co.yumemi.android.code_check.databinding.FragmentRepositoryDetailBinding - -@AndroidEntryPoint -class RepositoryDetailFragment : Fragment(R.layout.fragment_repository_detail) { - private val args: RepositoryDetailFragmentArgs by navArgs() - - private var _binding: FragmentRepositoryDetailBinding? = null - private val binding get() = _binding ?: throw IllegalStateException("Binding is null") - - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - - _binding = FragmentRepositoryDetailBinding.bind(view) - - val item = args.item - bindViews(item) - } - - private fun bindViews(item: RepositoryEntity) { - binding.ownerIconView.load(item.ownerIconUrl) - binding.nameView.text = item.name - binding.languageView.text = resources.getString(R.string.written_language, item.language) - binding.starsView.text = resources.getString(R.string.stars_count, item.stargazersCount) - binding.watchersView.text = resources.getString(R.string.watchers_count, item.watchersCount) - binding.forksView.text = resources.getString(R.string.forks_count, item.forksCount) - binding.openIssuesView.text = resources.getString(R.string.open_issues_count, item.openIssuesCount) - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositoryListRecyclerViewAdapter.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositoryListRecyclerViewAdapter.kt deleted file mode 100644 index 4c420c3..0000000 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositoryListRecyclerViewAdapter.kt +++ /dev/null @@ -1,74 +0,0 @@ -package jp.co.yumemi.android.code_check.core.presenter.search - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import jp.co.yumemi.android.code_check.R -import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity - -/** - * DiffUtilの実装 - */ -private val diffUtilCallback = - object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: RepositoryEntity, - newItem: RepositoryEntity, - ): Boolean { - return oldItem.name == newItem.name - } - - override fun areContentsTheSame( - oldItem: RepositoryEntity, - newItem: RepositoryEntity, - ): Boolean { - return oldItem == newItem - } - } - -/** - * RecyclerView Adapter - */ -class RepositoryListRecyclerViewAdapter( - private val itemClickListener: OnItemClickListener, -) : ListAdapter(diffUtilCallback) { - class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - private val repositoryNameView: TextView? = view.findViewById(R.id.repositoryNameView) - - /** - * ビューにデータをバインド - */ - fun bind( - item: RepositoryEntity, - clickListener: OnItemClickListener, - ) { - repositoryNameView?.text = item.name - itemView.setOnClickListener { clickListener.itemClick(item) } - } - } - - interface OnItemClickListener { - fun itemClick(repositoryEntity: RepositoryEntity) - } - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int, - ): ViewHolder { - val view = - LayoutInflater.from(parent.context) - .inflate(R.layout.layout_item, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder( - holder: ViewHolder, - position: Int, - ) { - holder.bind(getItem(position), itemClickListener) - } -} diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchFragment.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchFragment.kt deleted file mode 100644 index c74716f..0000000 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchFragment.kt +++ /dev/null @@ -1,97 +0,0 @@ -package jp.co.yumemi.android.code_check.core.presenter.search - -import android.os.Bundle -import android.view.View -import android.view.inputmethod.EditorInfo -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import dagger.hilt.android.AndroidEntryPoint -import jp.co.yumemi.android.code_check.R -import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity -import jp.co.yumemi.android.code_check.core.utils.DialogHelper -import jp.co.yumemi.android.code_check.databinding.FragmentRepositorySearchBinding - -@AndroidEntryPoint -class RepositorySearchFragment : Fragment(R.layout.fragment_repository_search) { - private var _binding: FragmentRepositorySearchBinding? = null - private val binding get() = _binding ?: throw IllegalStateException("Binding is null") - private val viewModel: RepositorySearchViewModel by viewModels() - - private val adapter by lazy { - RepositoryListRecyclerViewAdapter( - object : RepositoryListRecyclerViewAdapter.OnItemClickListener { - override fun itemClick(repositoryEntity: RepositoryEntity) { - onItemClick(repositoryEntity) - } - }, - ) - } - - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - _binding = FragmentRepositorySearchBinding.bind(view) - - observeViewModel() - setupRecyclerView() - setupSearchInput() - } - - private fun observeViewModel() { - viewModel.searchResults.observe(viewLifecycleOwner) { - adapter.submitList(it) - } - viewModel.errorMessage.observe(viewLifecycleOwner) { - it?.let { - DialogHelper.showErrorDialog( - requireContext(), - requireContext().getString(it), - ) - } - } - } - - private fun setupRecyclerView() { - val layoutManager = LinearLayoutManager(requireContext()) - val dividerItemDecoration = - DividerItemDecoration(requireContext(), layoutManager.orientation) - - binding.recyclerView.apply { - this.layoutManager = layoutManager - addItemDecoration(dividerItemDecoration) - adapter = this@RepositorySearchFragment.adapter - } - } - - private fun setupSearchInput() { - binding.searchInputText.setOnEditorActionListener { editText, action, _ -> - if (action == EditorInfo.IME_ACTION_SEARCH) { - viewModel.searchRepositories(editText.text.toString().trim()) - true - } else { - false - } - } - } - - /** - * リポジトリ検索結果のクリックイベント - * リサイクラービューでアイテムが押された時に動作を行います。 - */ - private fun onItemClick(item: RepositoryEntity) { - val action = - RepositorySearchFragmentDirections - .actionRepositoriesFragmentToRepositoryFragment(item = item) - findNavController().navigate(action) - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} diff --git a/app/src/main/res/layout/activity_top.xml b/app/src/main/res/layout/activity_top.xml deleted file mode 100644 index 9eb3991..0000000 --- a/app/src/main/res/layout/activity_top.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_repository_detail.xml b/app/src/main/res/layout/fragment_repository_detail.xml deleted file mode 100644 index 8e1b54f..0000000 --- a/app/src/main/res/layout/fragment_repository_detail.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_repository_search.xml b/app/src/main/res/layout/fragment_repository_search.xml deleted file mode 100644 index 5d1e079..0000000 --- a/app/src/main/res/layout/fragment_repository_search.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_item.xml b/app/src/main/res/layout/layout_item.xml deleted file mode 100644 index 5ade5cc..0000000 --- a/app/src/main/res/layout/layout_item.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml deleted file mode 100644 index 910a239..0000000 --- a/app/src/main/res/navigation/nav_graph.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - From d67d4a532651ae7935b711b11ab3d560e4f93ca5 Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 18:51:58 +0900 Subject: [PATCH 15/28] =?UTF-8?q?ref:=20=E3=83=AA=E3=83=B3=E3=82=BF?= =?UTF-8?q?=E3=83=BC=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/RepositoryDetailScreen.kt | 1 - .../features/github/GitHubMockData.kt | 69 +++--- .../github/GitHubServiceRepositoryImplTest.kt | 205 +++++++++--------- .../github/GitHubServiceUsecaseImplTest.kt | 137 ++++++------ 4 files changed, 212 insertions(+), 200 deletions(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt index 854e4f3..e6a2303 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt @@ -20,7 +20,6 @@ import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Star import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text diff --git a/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubMockData.kt b/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubMockData.kt index 823bc6b..26e9a85 100644 --- a/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubMockData.kt +++ b/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubMockData.kt @@ -12,36 +12,38 @@ import org.mockito.Mockito.`when` import java.io.IOException object GitHubMockData { - // 正常なリポジトリ検索結果 fun getMockRepositoryList(): RepositoryList { return RepositoryList( - items = listOf( - RepositoryItem( - name = "repo1", - owner = RepositoryOwner( - avatarUrl = "url1" + items = + listOf( + RepositoryItem( + name = "repo1", + owner = + RepositoryOwner( + avatarUrl = "url1", + ), + language = "Kotlin", + stargazersCount = 100, + forksCount = 50, + openIssuesCount = 30, + watchersCount = 70, + id = 1, ), - language = "Kotlin", - stargazersCount = 100, - forksCount = 50, - openIssuesCount = 30, - watchersCount = 70, - id = 1 - ), - RepositoryItem( - name = "repo2", - owner = RepositoryOwner( - avatarUrl = "url2" + RepositoryItem( + name = "repo2", + owner = + RepositoryOwner( + avatarUrl = "url2", + ), + language = "Java", + stargazersCount = 200, + forksCount = 80, + openIssuesCount = 20, + watchersCount = 60, + id = 2, ), - language = "Java", - stargazersCount = 200, - forksCount = 80, - openIssuesCount = 20, - watchersCount = 60, - id = 2 - ) - ) + ), ) } @@ -55,7 +57,7 @@ object GitHubMockData { forksCount = 50, openIssuesCount = 30, watchersCount = 70, - id = 1 + id = 1, ), RepositoryEntity( name = "repo2", @@ -65,8 +67,8 @@ object GitHubMockData { forksCount = 80, openIssuesCount = 20, watchersCount = 60, - id = 2 - ) + id = 2, + ), ) } @@ -80,9 +82,10 @@ object GitHubMockData { openIssuesCount = 30, watchersCount = 70, id = 1, - owner = RepositoryOwner( - avatarUrl = "url1" - ) + owner = + RepositoryOwner( + avatarUrl = "url1", + ), ) } @@ -95,12 +98,10 @@ object GitHubMockData { forksCount = 50, openIssuesCount = 30, watchersCount = 70, - id = 1 + id = 1, ) } - - // ネットワークエラー fun getMockNetworkError(): NetworkResult.Error { return NetworkResult.Error(GitHubError.NetworkError(IOException("Network issue"))) diff --git a/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceRepositoryImplTest.kt b/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceRepositoryImplTest.kt index 0dddb6c..ce9ec3a 100644 --- a/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceRepositoryImplTest.kt +++ b/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceRepositoryImplTest.kt @@ -1,17 +1,14 @@ package jp.co.yumemi.android.code_check.features.github import jp.co.yumemi.android.code_check.features.github.api.GitHubServiceApi -import jp.co.yumemi.android.code_check.features.github.entity.RepositoryItem -import jp.co.yumemi.android.code_check.features.github.entity.RepositoryList -import jp.co.yumemi.android.code_check.features.github.entity.RepositoryOwner import jp.co.yumemi.android.code_check.features.github.reposiotory.GitHubServiceRepositoryImpl import jp.co.yumemi.android.code_check.features.github.utils.GitHubError import jp.co.yumemi.android.code_check.features.github.utils.NetworkResult import kotlinx.coroutines.runBlocking import org.json.JSONException import org.junit.Assert.assertEquals -import org.junit.Test import org.junit.Before +import org.junit.Test import org.mockito.Mockito.mock import org.mockito.Mockito.`when` import retrofit2.HttpException @@ -28,116 +25,122 @@ class GitHubServiceRepositoryImplTest { } @Test - fun `fetchSearchResults returns success with valid data`() = runBlocking { - // Arrange - val mockResponse = GitHubMockData.getMockRepositoryList() - `when`(api.getRepositoryList("test")).thenReturn(mockResponse) - - // Act - val result = repository.fetchSearchResults("test") - - // Assert - assert(result is NetworkResult.Success) - val success = result as NetworkResult.Success - assertEquals(2, success.data.size) - assertEquals("repo1", success.data[0].name) - assertEquals("url1", success.data[0].ownerIconUrl) // 修正: ownerIconUrl -> avatarUrl - } - + fun `fetchSearchResults returns success with valid data`() = + runBlocking { + // Arrange + val mockResponse = GitHubMockData.getMockRepositoryList() + `when`(api.getRepositoryList("test")).thenReturn(mockResponse) + + // Act + val result = repository.fetchSearchResults("test") + + // Assert + assert(result is NetworkResult.Success) + val success = result as NetworkResult.Success + assertEquals(2, success.data.size) + assertEquals("repo1", success.data[0].name) + assertEquals("url1", success.data[0].ownerIconUrl) // 修正: ownerIconUrl -> avatarUrl + } @Test - fun `fetchSearchResults returns error on HttpException`() = runBlocking { - // Arrange - val httpException = mock(HttpException::class.java) - `when`(httpException.code()).thenReturn(401) - `when`(api.getRepositoryList("test")).thenThrow(httpException) - - // Act - val result = repository.fetchSearchResults("test") - - // Assert - assert(result is NetworkResult.Error) - val error = result as NetworkResult.Error - assert(error.exception is GitHubError.AuthenticationError) - } + fun `fetchSearchResults returns error on HttpException`() = + runBlocking { + // Arrange + val httpException = mock(HttpException::class.java) + `when`(httpException.code()).thenReturn(401) + `when`(api.getRepositoryList("test")).thenThrow(httpException) + + // Act + val result = repository.fetchSearchResults("test") + + // Assert + assert(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assert(error.exception is GitHubError.AuthenticationError) + } @Test - fun `fetchSearchResults returns error on IOException`() = runBlocking { - // Arrange - `when`(api.getRepositoryList("test")).thenAnswer { - throw IOException("Network error") + fun `fetchSearchResults returns error on IOException`() = + runBlocking { + // Arrange + `when`(api.getRepositoryList("test")).thenAnswer { + throw IOException("Network error") + } + + // Act + val result = repository.fetchSearchResults("test") + + // Assert + assert(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assert(error.exception is GitHubError.NetworkError) } - // Act - val result = repository.fetchSearchResults("test") - - // Assert - assert(result is NetworkResult.Error) - val error = result as NetworkResult.Error - assert(error.exception is GitHubError.NetworkError) - } - @Test - fun `fetchSearchResults returns error on JSONException`() = runBlocking { - // Arrange - `when`(api.getRepositoryList("test")).thenAnswer { - throw JSONException("Parsing error") + fun `fetchSearchResults returns error on JSONException`() = + runBlocking { + // Arrange + `when`(api.getRepositoryList("test")).thenAnswer { + throw JSONException("Parsing error") + } + + // Act + val result = repository.fetchSearchResults("test") + + // Assert + assert(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assert(error.exception is GitHubError.ParseError) } - // Act - val result = repository.fetchSearchResults("test") - - // Assert - assert(result is NetworkResult.Error) - val error = result as NetworkResult.Error - assert(error.exception is GitHubError.ParseError) - } - @Test - fun `fetchRepositoryDetail returns success with valid data`() = runBlocking { - // Arrange - val mockDetail = GitHubMockData.getMockRepositoryItem() - `when`(api.getRepositoryDetail(1)).thenReturn(mockDetail) - - // Act - val result = repository.fetchRepositoryDetail(1) - - // Assert - assert(result is NetworkResult.Success) - val success = result as NetworkResult.Success - assertEquals("repo1", success.data.name) - assertEquals("url1", success.data.ownerIconUrl) - } + fun `fetchRepositoryDetail returns success with valid data`() = + runBlocking { + // Arrange + val mockDetail = GitHubMockData.getMockRepositoryItem() + `when`(api.getRepositoryDetail(1)).thenReturn(mockDetail) + + // Act + val result = repository.fetchRepositoryDetail(1) + + // Assert + assert(result is NetworkResult.Success) + val success = result as NetworkResult.Success + assertEquals("repo1", success.data.name) + assertEquals("url1", success.data.ownerIconUrl) + } @Test - fun `fetchRepositoryDetail returns error on HttpException`() = runBlocking { - // Arrange - val httpException = mock(HttpException::class.java) - `when`(httpException.code()).thenReturn(429) - `when`(api.getRepositoryDetail(1)).thenThrow(httpException) - - // Act - val result = repository.fetchRepositoryDetail(1) - - // Assert - assert(result is NetworkResult.Error) - val error = result as NetworkResult.Error - assert(error.exception is GitHubError.RateLimitError) - } + fun `fetchRepositoryDetail returns error on HttpException`() = + runBlocking { + // Arrange + val httpException = mock(HttpException::class.java) + `when`(httpException.code()).thenReturn(429) + `when`(api.getRepositoryDetail(1)).thenThrow(httpException) + + // Act + val result = repository.fetchRepositoryDetail(1) + + // Assert + assert(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assert(error.exception is GitHubError.RateLimitError) + } @Test - fun `fetchRepositoryDetail returns error on IOException`() = runBlocking { - // Arrange - `when`(api.getRepositoryDetail(1)).thenAnswer { - throw IOException("Network error") + fun `fetchRepositoryDetail returns error on IOException`() = + runBlocking { + // Arrange + `when`(api.getRepositoryDetail(1)).thenAnswer { + throw IOException("Network error") + } + + // Act + val result = repository.fetchRepositoryDetail(1) + + // Assert + assert(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assert(error.exception is GitHubError.NetworkError) } - - // Act - val result = repository.fetchRepositoryDetail(1) - - // Assert - assert(result is NetworkResult.Error) - val error = result as NetworkResult.Error - assert(error.exception is GitHubError.NetworkError) - } } diff --git a/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceUsecaseImplTest.kt b/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceUsecaseImplTest.kt index a0812d0..4a9ae28 100644 --- a/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceUsecaseImplTest.kt +++ b/app/src/test/kotlin/jp/co/yumemi/android/code_check/features/github/GitHubServiceUsecaseImplTest.kt @@ -1,17 +1,19 @@ package jp.co.yumemi.android.code_check.features.github import jp.co.yumemi.android.code_check.core.api.NetworkConnectivityService -import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.features.github.reposiotory.GitHubServiceRepository import jp.co.yumemi.android.code_check.features.github.reposiotory.NetworkException import jp.co.yumemi.android.code_check.features.github.usecase.GitHubServiceUsecaseImpl import jp.co.yumemi.android.code_check.features.github.utils.GitHubError import jp.co.yumemi.android.code_check.features.github.utils.NetworkResult +import junit.framework.Assert.assertEquals import kotlinx.coroutines.runBlocking -import org.junit.Assert.* +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import org.mockito.Mockito.* +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` class GitHubServiceUsecaseImplTest { private lateinit var repository: GitHubServiceRepository @@ -29,96 +31,103 @@ class GitHubServiceUsecaseImplTest { fun `fetchSearchResults throws NetworkException when offline`() { `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(false) - val exception = assertThrows(NetworkException::class.java) { - runBlocking { - usecase.fetchSearchResults("test") + val exception = + assertThrows(NetworkException::class.java) { + runBlocking { + usecase.fetchSearchResults("test") + } } - } assertEquals("オフライン状態です", exception.message) } @Test - fun `fetchSearchResults returns success when online`() = runBlocking { - val mockResults = GitHubMockData.getMockRepositoryEntityList() + fun `fetchSearchResults returns success when online`() = + runBlocking { + val mockResults = GitHubMockData.getMockRepositoryEntityList() - `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) - `when`(repository.fetchSearchResults("test")).thenReturn(NetworkResult.Success(mockResults)) + `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) + `when`(repository.fetchSearchResults("test")).thenReturn(NetworkResult.Success(mockResults)) - val result = usecase.fetchSearchResults("test") + val result = usecase.fetchSearchResults("test") - assertTrue(result is NetworkResult.Success) - val success = result as NetworkResult.Success - assertEquals(2, success.data.size) - assertEquals("repo1", success.data[0].name) - assertEquals("repo2", success.data[1].name) - } + assertTrue(result is NetworkResult.Success) + val success = result as NetworkResult.Success + assertEquals(2, success.data.size) + assertEquals("repo1", success.data[0].name) + assertEquals("repo2", success.data[1].name) + } @Test - fun `fetchSearchResults returns error on API failure`() = runBlocking { - val mockError = GitHubMockData.getMockApiError404() - `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) - `when`(repository.fetchSearchResults("test")).thenReturn(mockError) - - val result = usecase.fetchSearchResults("test") - - assertTrue(result is NetworkResult.Error) - val error = result as NetworkResult.Error - assertTrue(error.exception is GitHubError.ApiError) - assertEquals(404, (error.exception as GitHubError.ApiError).code) - } + fun `fetchSearchResults returns error on API failure`() = + runBlocking { + val mockError = GitHubMockData.getMockApiError404() + `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) + `when`(repository.fetchSearchResults("test")).thenReturn(mockError) + + val result = usecase.fetchSearchResults("test") + + assertTrue(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assertTrue(error.exception is GitHubError.ApiError) + assertEquals(404, (error.exception as GitHubError.ApiError).code) + } @Test fun `fetchRepositoryDetail throws NetworkException when offline`() { `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(false) - val exception = assertThrows(NetworkException::class.java) { - runBlocking { - usecase.fetchRepositoryDetail(1) + val exception = + assertThrows(NetworkException::class.java) { + runBlocking { + usecase.fetchRepositoryDetail(1) + } } - } assertEquals("オフライン状態です", exception.message) } @Test - fun `fetchRepositoryDetail returns success when online`() = runBlocking { - val mockDetail = GitHubMockData.getMockRepositoryEntity() + fun `fetchRepositoryDetail returns success when online`() = + runBlocking { + val mockDetail = GitHubMockData.getMockRepositoryEntity() - `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) - `when`(repository.fetchRepositoryDetail(1)).thenReturn(NetworkResult.Success(mockDetail)) + `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) + `when`(repository.fetchRepositoryDetail(1)).thenReturn(NetworkResult.Success(mockDetail)) - val result = usecase.fetchRepositoryDetail(1) + val result = usecase.fetchRepositoryDetail(1) - assertTrue(result is NetworkResult.Success) - val success = result as NetworkResult.Success - assertEquals("repo1", success.data.name) - } + assertTrue(result is NetworkResult.Success) + val success = result as NetworkResult.Success + assertEquals("repo1", success.data.name) + } @Test - fun `fetchRepositoryDetail returns error on API failure`() = runBlocking { - val mockError = GitHubMockData.getMockApiError500() - `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) - `when`(repository.fetchRepositoryDetail(1)).thenReturn(mockError) - - val result = usecase.fetchRepositoryDetail(1) - - assertTrue(result is NetworkResult.Error) - val error = result as NetworkResult.Error - assertTrue(error.exception is GitHubError.ApiError) - assertEquals(500, (error.exception as GitHubError.ApiError).code) - } + fun `fetchRepositoryDetail returns error on API failure`() = + runBlocking { + val mockError = GitHubMockData.getMockApiError500() + `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) + `when`(repository.fetchRepositoryDetail(1)).thenReturn(mockError) + + val result = usecase.fetchRepositoryDetail(1) + + assertTrue(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assertTrue(error.exception is GitHubError.ApiError) + assertEquals(500, (error.exception as GitHubError.ApiError).code) + } @Test - fun `fetchRepositoryDetail handles network error`() = runBlocking { - val mockError = GitHubMockData.getMockNetworkError() - `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) - `when`(repository.fetchRepositoryDetail(1)).thenReturn(mockError) + fun `fetchRepositoryDetail handles network error`() = + runBlocking { + val mockError = GitHubMockData.getMockNetworkError() + `when`(networkConnectivityService.isNetworkAvailable()).thenReturn(true) + `when`(repository.fetchRepositoryDetail(1)).thenReturn(mockError) - val result = usecase.fetchRepositoryDetail(1) + val result = usecase.fetchRepositoryDetail(1) - assertTrue(result is NetworkResult.Error) - val error = result as NetworkResult.Error - assertTrue(error.exception is GitHubError.NetworkError) - } + assertTrue(result is NetworkResult.Error) + val error = result as NetworkResult.Error + assertTrue(error.exception is GitHubError.NetworkError) + } } From c30c4790c4551adebd929a8b50a8ca1cf7111991 Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 18:55:17 +0900 Subject: [PATCH 16/28] =?UTF-8?q?ref:=20=E3=83=AA=E3=83=B3=E3=82=BF?= =?UTF-8?q?=E3=83=BC=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/presenter/detail/RepositoryDetailViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt index f93a8e0..e33fb8b 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt @@ -16,8 +16,8 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class -RepositoryDetailViewModel@Inject +class RepositoryDetailViewModel + @Inject constructor( private val networkRepository: GitHubServiceUsecase, ) : ViewModel() { From 3c5024e369e0a33d0f4c76f590013d2e96219876 Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 19:01:46 +0900 Subject: [PATCH 17/28] =?UTF-8?q?fix:=20=E3=82=BF=E3=82=A4=E3=83=9D?= =?UTF-8?q?=E3=82=B0=E3=83=A9=E3=83=95=E3=82=A3=E3=83=BC=E3=81=AE=E5=AE=8C?= =?UTF-8?q?=E5=85=A8=E3=81=AE=E5=AE=9F=E8=A3=85=E3=82=92=E8=A1=8C=E3=81=AA?= =?UTF-8?q?=E3=81=A3=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code_check/core/presenter/theme/Type.kt | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Type.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Type.kt index e59570c..e35dc03 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Type.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/theme/Type.kt @@ -17,20 +17,36 @@ val Typography = lineHeight = 24.sp, letterSpacing = 0.5.sp, ), - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ + titleLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp, + ), + titleMedium = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 18.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp, + ), + bodyMedium = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + ), + labelSmall = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), ) From d026e5499883e684092287b52fbfc2d7461028f8 Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 19:05:18 +0900 Subject: [PATCH 18/28] =?UTF-8?q?fix:=20=E3=82=A2=E3=82=AF=E3=82=BB?= =?UTF-8?q?=E3=82=B7=E3=83=93=E3=83=AA=E3=83=86=E3=82=A3=E3=81=A8=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=82=AB=E3=83=A9=E3=82=A4=E3=82=BC=E3=83=BC=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=81=AE=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/presenter/widget/ProgressCycle.kt | 32 ++++++++++++++----- app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/ProgressCycle.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/ProgressCycle.kt index 5de3eb6..ce11546 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/ProgressCycle.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/ProgressCycle.kt @@ -10,23 +10,39 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.isTraversalGroup import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import jp.co.yumemi.android.code_check.R @Composable -fun ProgressCycle() { +fun ProgressCycle( + message: String = stringResource(R.string.searching), + contentDescription: String = stringResource(R.string.loading_content_description) +) { Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, modifier = - Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(8.dp) - .semantics { isTraversalGroup = true }, + Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(8.dp) + .semantics { + isTraversalGroup = true + this.contentDescription = contentDescription + } ) { - CircularProgressIndicator() - Text(text = "検索中") + CircularProgressIndicator( + modifier = Modifier.semantics { + this.contentDescription = contentDescription + } + ) + Text( + text = message, + modifier = Modifier.padding(top = 8.dp) + ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3b6a633..71387e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,4 +14,6 @@ フォームが空欄になっています 詳細 検索 + 検索中 + データを読み込んでいます \ No newline at end of file From bedab2d43836490409296b86d7b866839ba446fa Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 19:07:26 +0900 Subject: [PATCH 19/28] =?UTF-8?q?fix:=20=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=83=8F=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=81=AE?= =?UTF-8?q?=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/presenter/detail/RepositoryDetailViewModel.kt | 4 ++-- app/src/main/res/values/strings.xml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt index e33fb8b..46b2eb9 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailViewModel.kt @@ -33,7 +33,7 @@ class RepositoryDetailViewModel */ fun searchRepositories(id: Int) { if (id == 0) { - _errorMessage.postValue(R.string.api_error) + _errorMessage.postValue(R.string.invalid_repository_id) return } viewModelScope.launch { @@ -47,7 +47,7 @@ class RepositoryDetailViewModel _searchResults.postValue(results.data) } } catch (e: NetworkException) { - Log.e("NetworkException", e.message, e) + Log.e("RepositoryDetailViewModel", "Failed to fetch repository details for id: $id", e) handleError(GitHubError.NetworkError(e)) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 71387e1..20a882e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,7 @@ セッションの時間切れ APIのエラー フォームが空欄になっています + 正しいIDが返されませんでした 詳細 検索 検索中 From 99b1860be2fe607c83fda38d422270123ba4a0b8 Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 19:14:41 +0900 Subject: [PATCH 20/28] =?UTF-8?q?fix:=20=E3=82=A2=E3=82=AF=E3=82=BB?= =?UTF-8?q?=E3=82=B7=E3=83=93=E3=83=AA=E3=83=86=E3=82=A3=E3=81=AE=E6=94=B9?= =?UTF-8?q?=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/presenter/search/RepositorySearchScreen.kt | 8 ++++++-- app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt index 1b01faf..5dc3168 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.isTraversalGroup import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.ImeAction @@ -148,7 +149,7 @@ fun CustomSearchBar( leadingIcon = { Icon( Icons.Sharp.Search, - contentDescription = null, + contentDescription = context.getString(R.string.search_icon_description), tint = MaterialTheme.colorScheme.primary, ) }, @@ -159,7 +160,10 @@ fun CustomSearchBar( .background( color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(40.dp), - ), + ) + .semantics { + contentDescription = context.getString(R.string.search_bar_description) + }, colors = TextFieldDefaults.colors( focusedContainerColor = MaterialTheme.colorScheme.primaryContainer, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 20a882e..7a589bd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,4 +17,6 @@ 検索 検索中 データを読み込んでいます + 検索アイコン + 検索バー \ No newline at end of file From 1942c47edd26c768c5a0598bedf3d259edb851da Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 19:17:44 +0900 Subject: [PATCH 21/28] =?UTF-8?q?fix:=20=E7=94=BB=E5=83=8F=E8=AA=AD?= =?UTF-8?q?=E3=81=BF=E8=BE=BC=E3=81=BF=E3=81=AE=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/RepositoryDetailScreen.kt | 13 +++++++-- .../core/presenter/widget/ProgressCycle.kt | 27 ++++++++++--------- app/src/main/res/values/strings.xml | 1 + 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt index e6a2303..82a7d3f 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -39,6 +40,7 @@ import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import coil.compose.rememberAsyncImagePainter +import jp.co.yumemi.android.code_check.R import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.core.presenter.widget.ProgressCycle @@ -124,6 +126,8 @@ fun RepositoryDetailContent(repository: RepositoryEntity) { @Composable fun RepositoryOverviewCard(repository: RepositoryEntity) { + val context = LocalContext.current + Card( modifier = Modifier.fillMaxWidth(), elevation = CardDefaults.cardElevation(4.dp), @@ -137,8 +141,13 @@ fun RepositoryOverviewCard(repository: RepositoryEntity) { horizontalAlignment = Alignment.CenterHorizontally, ) { Image( - painter = rememberAsyncImagePainter(repository.ownerIconUrl), - contentDescription = "Owner Icon", + painter = + rememberAsyncImagePainter( + model = repository.ownerIconUrl, + error = painterResource(R.drawable.ic_launcher_foreground), + placeholder = painterResource(R.drawable.ic_launcher_background), + ), + contentDescription = context.getString(R.string.owner_icon_description), modifier = Modifier.size(80.dp), ) Text( diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/ProgressCycle.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/ProgressCycle.kt index ce11546..d0a5bd1 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/ProgressCycle.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/widget/ProgressCycle.kt @@ -20,29 +20,30 @@ import jp.co.yumemi.android.code_check.R @Composable fun ProgressCycle( message: String = stringResource(R.string.searching), - contentDescription: String = stringResource(R.string.loading_content_description) + contentDescription: String = stringResource(R.string.loading_content_description), ) { Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, modifier = - Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(8.dp) - .semantics { - isTraversalGroup = true - this.contentDescription = contentDescription - } + Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(8.dp) + .semantics { + isTraversalGroup = true + this.contentDescription = contentDescription + }, ) { CircularProgressIndicator( - modifier = Modifier.semantics { - this.contentDescription = contentDescription - } + modifier = + Modifier.semantics { + this.contentDescription = contentDescription + }, ) Text( text = message, - modifier = Modifier.padding(top = 8.dp) + modifier = Modifier.padding(top = 8.dp), ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7a589bd..64c0210 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,4 +19,5 @@ データを読み込んでいます 検索アイコン 検索バー + ユーザーアイコン \ No newline at end of file From 0c897744fbf7c930bbeab451f7fd73768c39258c Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 19:18:37 +0900 Subject: [PATCH 22/28] =?UTF-8?q?fix:=20=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=81=AE=E3=83=AA?= =?UTF-8?q?=E3=82=BD=E3=83=BC=E3=82=B9=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code_check/core/presenter/detail/RepositoryDetailScreen.kt | 2 +- app/src/main/res/values/strings.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt index 82a7d3f..7494615 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt @@ -76,7 +76,7 @@ fun RepositoryDetailScreen( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth().fillMaxHeight(), ) { - Text(text = "データの取得に失敗しました。") + Text(text = context.getString(R.string.error_data_fetch_failed)) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64c0210..18c2cf7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,4 +20,5 @@ 検索アイコン 検索バー ユーザーアイコン + データの取得に失敗しました \ No newline at end of file From 39c75af5beeb1efbabbb60de7f935563de58ff01 Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 19:25:21 +0900 Subject: [PATCH 23/28] =?UTF-8?q?fix:=20=E7=8A=B6=E6=85=8B=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E3=81=A8=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=8F=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=81=AE=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/presenter/detail/RepositoryDetailScreen.kt | 13 ++++++++++--- app/src/main/res/values/strings.xml | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt index 7494615..5ae619c 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt @@ -1,5 +1,6 @@ package jp.co.yumemi.android.code_check.core.presenter.detail +import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -56,27 +57,33 @@ fun RepositoryDetailScreen( var isLoading by remember { mutableStateOf(false) } val lifecycleOwner = LocalLifecycleOwner.current + BackHandler(onBack = toBack) + + var error by remember { mutableStateOf(null) } + LaunchedEffect(Unit) { viewModel.searchResults.observe(lifecycleOwner) { repositoryDetail.value = it isLoading = false + error = null } viewModel.errorMessage.observe(lifecycleOwner) { errorMessage -> errorMessage?.let { showSnackBar(context.getString(it), true) + error = context.getString(it) } isLoading = false } viewModel.searchRepositories(repositoryId) } - if (repositoryDetail.value == null && !isLoading) { + if (error != null) { Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth().fillMaxHeight(), ) { - Text(text = context.getString(R.string.error_data_fetch_failed)) + Text(text = error!!) } } @@ -156,7 +163,7 @@ fun RepositoryOverviewCard(repository: RepositoryEntity) { fontWeight = FontWeight.Bold, ) Text( - text = "Language: ${repository.language}", + text = context.getString(R.string.language_format, repository.language), style = MaterialTheme.typography.bodyMedium, ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 18c2cf7..6659f1c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,4 +21,5 @@ 検索バー ユーザーアイコン データの取得に失敗しました + Language: %s \ No newline at end of file From 8513674843e9233142291f04decb0bef8ae7beac Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 19:35:35 +0900 Subject: [PATCH 24/28] =?UTF-8?q?fix:=20=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=83=8F=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=81=A8?= =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E4=BD=93=E9=A8=93=E3=81=AE?= =?UTF-8?q?=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/RepositorySearchScreen.kt | 137 +++++++++++++++++- app/src/main/res/values/strings.xml | 5 + 2 files changed, 139 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt index 5dc3168..a9184b0 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/search/RepositorySearchScreen.kt @@ -2,17 +2,23 @@ package jp.co.yumemi.android.code_check.core.presenter.search import androidx.compose.foundation.background import androidx.compose.foundation.clickable +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.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.sharp.Search +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -25,15 +31,19 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf 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.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.isTraversalGroup import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -42,6 +52,9 @@ import jp.co.yumemi.android.code_check.R import jp.co.yumemi.android.code_check.core.entity.RepositoryEntity import jp.co.yumemi.android.code_check.core.presenter.theme.CodeCheckAppTheme import jp.co.yumemi.android.code_check.core.presenter.widget.ProgressCycle +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @Composable fun RepositorySearchScreen( @@ -54,12 +67,18 @@ fun RepositorySearchScreen( val lifecycleOwner = LocalLifecycleOwner.current val context = LocalContext.current var isLoading by remember { mutableStateOf(false) } + var isError by remember { mutableStateOf(false) } + + // デバウンス処理の追加 + val scope = rememberCoroutineScope() + var searchJob by remember { mutableStateOf(null) } LaunchedEffect(Unit) { viewModel.searchResults.observe(lifecycleOwner) { repositoryList.clear() repositoryList.addAll(it) isLoading = false + isError = false } viewModel.errorMessage.observe(lifecycleOwner) { it?.let { @@ -69,6 +88,7 @@ fun RepositorySearchScreen( ) } isLoading = false + isError = true } } @@ -77,13 +97,28 @@ fun RepositorySearchScreen( inputText = inputText, onValueChange = { inputText = it }, searchAction = { searchWord -> - repositoryList.clear() - viewModel.searchRepositories(searchWord.trim()) - isLoading = true + searchJob?.cancel() + searchJob = + scope.launch { + delay(500) // 500ms遅延 + if (searchWord.isBlank()) return@launch + repositoryList.clear() + viewModel.searchRepositories(searchWord.trim()) + isLoading = true + } }, ) if (isLoading) { ProgressCycle() + } else if (repositoryList.isEmpty() && !isError) { + EmptyState() + } else if (isError) { + ErrorState( + onRetry = { + viewModel.searchRepositories(inputText.trim()) + isLoading = true + }, + ) } RepositoryListView( repositoryList = repositoryList, @@ -185,6 +220,102 @@ fun CustomSearchBar( ) } +@Composable +fun ErrorState( + onRetry: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = Icons.Filled.Error, + contentDescription = stringResource(R.string.error_icon_description), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(48.dp), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.error_data_fetch_failed), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = onRetry, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + ) { + Text(stringResource(R.string.retry)) + } + } +} + +@Composable +fun EmptyState(modifier: Modifier = Modifier) { + Column( + modifier = + modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = stringResource(R.string.empty_state_icon_description), + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(48.dp), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.empty_state_title), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.empty_state_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } +} + +@Preview(showBackground = true) +@Composable +fun EmptyStatePreview() { + CodeCheckAppTheme { + EmptyState() + } +} + +@Preview(showBackground = true) +@Composable +fun ErrorStatePreview() { + CodeCheckAppTheme { + ErrorState(onRetry = {}) + } +} + @Composable @Preview(showBackground = true) fun CustomSearchBarPreview() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6659f1c..fa51ec3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,4 +22,9 @@ ユーザーアイコン データの取得に失敗しました Language: %s + 再度行う + エラーアイコン + 検索アイコン + リポジトリが見つかりません + 検索ワードを入力してリポジトリを探してください \ No newline at end of file From e9a1c79a725bc0ac136940f6325975319d4efc70 Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 19:42:57 +0900 Subject: [PATCH 25/28] =?UTF-8?q?fix:=20=E7=B5=B1=E8=A8=88=E6=83=85?= =?UTF-8?q?=E5=A0=B1=E3=81=AE=E3=83=86=E3=82=AD=E3=82=B9=E3=83=88=E3=82=92?= =?UTF-8?q?=E3=83=AA=E3=82=BD=E3=83=BC=E3=82=B9=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/RepositoryDetailScreen.kt | 20 ++++++++++++++++--- app/src/main/res/values/strings.xml | 3 +++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt index 5ae619c..271e93c 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt @@ -172,6 +172,8 @@ fun RepositoryOverviewCard(repository: RepositoryEntity) { @Composable fun RepositoryStatsCard(repository: RepositoryEntity) { + val context = LocalContext.current + Card( modifier = Modifier.fillMaxWidth(), elevation = CardDefaults.cardElevation(4.dp), @@ -188,7 +190,11 @@ fun RepositoryStatsCard(repository: RepositoryEntity) { tint = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.width(8.dp)) - Text("Stars: ${repository.stargazersCount}", fontSize = 18.sp, fontWeight = FontWeight.Medium) + Text( + text = context.getString(R.string.stars_count_format, repository.stargazersCount), + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + ) } Row(verticalAlignment = Alignment.CenterVertically) { @@ -199,7 +205,11 @@ fun RepositoryStatsCard(repository: RepositoryEntity) { tint = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.width(8.dp)) - Text("Forks: ${repository.forksCount}", fontSize = 18.sp, fontWeight = FontWeight.Medium) + Text( + text = context.getString(R.string.forks_count_format, repository.forksCount), + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + ) } Row(verticalAlignment = Alignment.CenterVertically) { @@ -210,7 +220,11 @@ fun RepositoryStatsCard(repository: RepositoryEntity) { tint = MaterialTheme.colorScheme.primary, ) Spacer(modifier = Modifier.width(8.dp)) - Text("Open Issues: ${repository.openIssuesCount}", fontSize = 18.sp, fontWeight = FontWeight.Medium) + Text( + text = context.getString(R.string.issues_count_format, repository.openIssuesCount), + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + ) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa51ec3..2e44add 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,4 +27,7 @@ 検索アイコン リポジトリが見つかりません 検索ワードを入力してリポジトリを探してください + Stars: %d + Forks: %d + Open Issues: %d \ No newline at end of file From 230ba6c5be557f930d10c1bd95f98ed625dbf372 Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 19:44:06 +0900 Subject: [PATCH 26/28] =?UTF-8?q?fix:=20=E5=88=9D=E6=9C=9F=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=83=87=E3=82=A3=E3=83=B3=E3=82=B0=E7=8A=B6=E6=85=8B?= =?UTF-8?q?=E3=81=AE=E8=A8=AD=E5=AE=9A=E3=81=A8UI=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=81=AE=E5=88=B6=E5=BE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presenter/detail/RepositoryDetailScreen.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt index 271e93c..4bf9107 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt @@ -62,6 +62,7 @@ fun RepositoryDetailScreen( var error by remember { mutableStateOf(null) } LaunchedEffect(Unit) { + isLoading = true viewModel.searchResults.observe(lifecycleOwner) { repositoryDetail.value = it isLoading = false @@ -77,7 +78,7 @@ fun RepositoryDetailScreen( viewModel.searchRepositories(repositoryId) } - if (error != null) { + if (error != null && !isLoading) { Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, @@ -87,11 +88,13 @@ fun RepositoryDetailScreen( } } - RepositoryDetailScaffold( - isLoading = isLoading, - repositoryDetail = repositoryDetail.value, - toBack = toBack, - ) + if (error == null) { + RepositoryDetailScaffold( + isLoading = isLoading, + repositoryDetail = repositoryDetail.value, + toBack = toBack, + ) + } } @Composable From e6cfc324480d0ffeff1f2858f6a92eb16c4779cb Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 19:48:43 +0900 Subject: [PATCH 27/28] =?UTF-8?q?fix:=E6=96=87=E5=AD=97=E5=88=97=E3=83=AA?= =?UTF-8?q?=E3=82=BD=E3=83=BC=E3=82=B9=E3=81=AE=E9=87=8D=E8=A4=87=E3=81=AE?= =?UTF-8?q?=E8=A7=A3=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2e44add..7493715 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,6 @@ Android Engineer CodeCheck GitHub のリポジトリを検索できるよー - Written in %s "%1$d stars" "%1$d watchers" "%1$d forks" From a94b51d38296d8d6a2c46487e1994aa771057007 Mon Sep 17 00:00:00 2001 From: harutiro Date: Mon, 20 Jan 2025 20:25:15 +0900 Subject: [PATCH 28/28] =?UTF-8?q?fix:=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E6=96=87=E5=AD=97=E5=88=97=E3=81=AE=E9=87=8D?= =?UTF-8?q?=E8=A4=87=E3=81=A8=E4=B8=8D=E6=95=B4=E5=90=88=E3=81=AE=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/presenter/detail/RepositoryDetailScreen.kt | 6 +++--- app/src/main/res/values/strings.xml | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt index 4bf9107..260b384 100644 --- a/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt +++ b/app/src/main/kotlin/jp/co/yumemi/android/code_check/core/presenter/detail/RepositoryDetailScreen.kt @@ -194,7 +194,7 @@ fun RepositoryStatsCard(repository: RepositoryEntity) { ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = context.getString(R.string.stars_count_format, repository.stargazersCount), + text = context.getString(R.string.stars_count, repository.stargazersCount), fontSize = 18.sp, fontWeight = FontWeight.Medium, ) @@ -209,7 +209,7 @@ fun RepositoryStatsCard(repository: RepositoryEntity) { ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = context.getString(R.string.forks_count_format, repository.forksCount), + text = context.getString(R.string.forks_count, repository.forksCount), fontSize = 18.sp, fontWeight = FontWeight.Medium, ) @@ -224,7 +224,7 @@ fun RepositoryStatsCard(repository: RepositoryEntity) { ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = context.getString(R.string.issues_count_format, repository.openIssuesCount), + text = context.getString(R.string.open_issues_count, repository.openIssuesCount), fontSize = 18.sp, fontWeight = FontWeight.Medium, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7493715..49b65d0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -26,7 +26,4 @@ 検索アイコン リポジトリが見つかりません 検索ワードを入力してリポジトリを探してください - Stars: %d - Forks: %d - Open Issues: %d \ No newline at end of file