diff --git a/app/build.gradle b/app/build.gradle index 196b037..60f5069 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,7 @@ plugins { alias(libs.plugins.ksp) alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.androidx.baselineprofile) + alias(libs.plugins.detekt) } def final debugAppSuffix = ".debug" @@ -13,7 +14,7 @@ android { experimentalProperties["android.experimental.r8.dex-startup-optimization"] = true signingConfigs { release { - storeFile file('/Users/robinsingh/Keystore') + storeFile file('/Users/robinsingh/Keystore') storePassword 'q1w2e3r4t5y' } } @@ -140,4 +141,4 @@ dependencies { debugImplementation libs.bundle.core benchmarkImplementation libs.bundle.core.no.op releaseImplementation libs.bundle.core.no.op -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/com/rob729/newsfeed/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/rob729/newsfeed/ExampleInstrumentedTest.kt index 241d8e7..37cc08a 100644 --- a/app/src/androidTest/java/com/rob729/newsfeed/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/rob729/newsfeed/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package com.rob729.newsfeed -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -21,4 +19,4 @@ class ExampleInstrumentedTest { val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals("com.rob729.newsfeed", appContext.packageName) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/database/DataConverter.kt b/app/src/main/java/com/rob729/newsfeed/database/DataConverter.kt index 679874b..c655c15 100644 --- a/app/src/main/java/com/rob729/newsfeed/database/DataConverter.kt +++ b/app/src/main/java/com/rob729/newsfeed/database/DataConverter.kt @@ -23,4 +23,4 @@ class DataConverter { val jsonAdapter: JsonAdapter = moshi.adapter(ArticleDbData::class.java) return jsonAdapter.fromJson(json) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/database/NewsDBDataSource.kt b/app/src/main/java/com/rob729/newsfeed/database/NewsDBDataSource.kt index e265844..ceb95a0 100644 --- a/app/src/main/java/com/rob729/newsfeed/database/NewsDBDataSource.kt +++ b/app/src/main/java/com/rob729/newsfeed/database/NewsDBDataSource.kt @@ -34,4 +34,4 @@ class NewsDBDataSource(private val newsDao: NewsDao) { return newsDao.getNewsSourceDomainFetchTimeInMillis(newsSourceDomain) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/database/NewsDao.kt b/app/src/main/java/com/rob729/newsfeed/database/NewsDao.kt index 383d421..ee9096f 100644 --- a/app/src/main/java/com/rob729/newsfeed/database/NewsDao.kt +++ b/app/src/main/java/com/rob729/newsfeed/database/NewsDao.kt @@ -21,4 +21,4 @@ interface NewsDao { @Query("DELETE FROM news_source_table where news_source_domain = :newsSourceDomain") suspend fun removeSavedNewsArticlesListForNews(newsSourceDomain: String) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/database/NewsDatabase.kt b/app/src/main/java/com/rob729/newsfeed/database/NewsDatabase.kt index b04c9c1..05cc1b5 100644 --- a/app/src/main/java/com/rob729/newsfeed/database/NewsDatabase.kt +++ b/app/src/main/java/com/rob729/newsfeed/database/NewsDatabase.kt @@ -36,4 +36,4 @@ abstract class NewsDatabase : RoomDatabase() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/initalizers/KoinInitializer.kt b/app/src/main/java/com/rob729/newsfeed/initalizers/KoinInitializer.kt index 2d03f70..46f1fec 100644 --- a/app/src/main/java/com/rob729/newsfeed/initalizers/KoinInitializer.kt +++ b/app/src/main/java/com/rob729/newsfeed/initalizers/KoinInitializer.kt @@ -68,4 +68,4 @@ class KoinInitializer : Initializer { override fun dependencies(): MutableList>> { return mutableListOf() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/initalizers/NewsSourceImagesPrefetch.kt b/app/src/main/java/com/rob729/newsfeed/initalizers/NewsSourceImagesPrefetch.kt index 6fc3aaf..d743f51 100644 --- a/app/src/main/java/com/rob729/newsfeed/initalizers/NewsSourceImagesPrefetch.kt +++ b/app/src/main/java/com/rob729/newsfeed/initalizers/NewsSourceImagesPrefetch.kt @@ -19,4 +19,4 @@ class NewsSourceImagesPrefetch : Initializer { WorkManagerInitializer::class.java ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/initalizers/PlutoInitializer.kt b/app/src/main/java/com/rob729/newsfeed/initalizers/PlutoInitializer.kt index 3b21743..102e07c 100644 --- a/app/src/main/java/com/rob729/newsfeed/initalizers/PlutoInitializer.kt +++ b/app/src/main/java/com/rob729/newsfeed/initalizers/PlutoInitializer.kt @@ -24,4 +24,4 @@ class PlutoInitializer : Initializer { override fun dependencies(): MutableList>> { return mutableListOf() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/model/NewsResource.kt b/app/src/main/java/com/rob729/newsfeed/model/NewsResource.kt index 78845c5..1b5efc3 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/NewsResource.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/NewsResource.kt @@ -4,4 +4,4 @@ sealed class NewsResource { data class Success(val data: T) : NewsResource() data object Loading : NewsResource() data class Error(val message: String) : NewsResource() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/model/api/NetworkArticle.kt b/app/src/main/java/com/rob729/newsfeed/model/api/NetworkArticle.kt index ae596c2..7d89071 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/api/NetworkArticle.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/api/NetworkArticle.kt @@ -12,4 +12,4 @@ data class NetworkArticle( @Json(name = "urlToImage") val imageUrl: String?, @Json(name = "description") val description: String?, @Json(name = "publishedAt") val publishedAt: String -) \ No newline at end of file +) diff --git a/app/src/main/java/com/rob729/newsfeed/model/database/ArticleDbData.kt b/app/src/main/java/com/rob729/newsfeed/model/database/ArticleDbData.kt index cd5e0a4..8b1593e 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/database/ArticleDbData.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/database/ArticleDbData.kt @@ -8,4 +8,4 @@ data class ArticleDbData( @ColumnInfo(name = "urlToImage") val imageUrl: String?, @ColumnInfo(name = "description") val description: String?, @ColumnInfo(name = "publishedAt") val publishedAt: String -) \ No newline at end of file +) diff --git a/app/src/main/java/com/rob729/newsfeed/model/database/NewsSourceDbData.kt b/app/src/main/java/com/rob729/newsfeed/model/database/NewsSourceDbData.kt index fcdb18f..967be43 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/database/NewsSourceDbData.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/database/NewsSourceDbData.kt @@ -10,4 +10,4 @@ data class NewsSourceDbData( @ColumnInfo(name = "news_source_domain") val newsSourceDomain: String, @ColumnInfo(name = "news_article") val newsArticle: ArticleDbData, @ColumnInfo(name = "news_source_fetch_time") val newsSourceFetchTimeInMillis: Long, -) \ No newline at end of file +) diff --git a/app/src/main/java/com/rob729/newsfeed/model/mapper/ArticleMapper.kt b/app/src/main/java/com/rob729/newsfeed/model/mapper/ArticleMapper.kt index 6bf92a3..6a1cdc6 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/mapper/ArticleMapper.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/mapper/ArticleMapper.kt @@ -15,7 +15,10 @@ fun mapNetworkArticleToArticleDbData(networkArticle: NetworkArticle): ArticleDbD } fun mapArticleDbDataToNewsArticleUiData(articleDbData: ArticleDbData): NewsArticleUiData? { - if (articleDbData.title.isNullOrBlank() || articleDbData.description.isNullOrBlank() || articleDbData.imageUrl.isNullOrBlank()) + if (articleDbData.title.isNullOrBlank() + || articleDbData.description.isNullOrBlank() + || articleDbData.imageUrl.isNullOrBlank() + ) return null return NewsArticleUiData( articleDbData.title, @@ -27,7 +30,10 @@ fun mapArticleDbDataToNewsArticleUiData(articleDbData: ArticleDbData): NewsArtic } fun mapNetworkArticleToNewsArticleUiData(networkArticle: NetworkArticle): NewsArticleUiData? { - if (networkArticle.title.isNullOrBlank() || networkArticle.description.isNullOrBlank() || networkArticle.imageUrl.isNullOrBlank()) + if (networkArticle.title.isNullOrBlank() + || networkArticle.description.isNullOrBlank() + || networkArticle.imageUrl.isNullOrBlank() + ) return null return NewsArticleUiData( networkArticle.title, @@ -36,4 +42,4 @@ fun mapNetworkArticleToNewsArticleUiData(networkArticle: NetworkArticle): NewsAr networkArticle.url, networkArticle.publishedAt ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/model/state/home/HomeFeedSideEffect.kt b/app/src/main/java/com/rob729/newsfeed/model/state/home/HomeFeedSideEffect.kt index dee37c9..4c105c0 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/state/home/HomeFeedSideEffect.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/state/home/HomeFeedSideEffect.kt @@ -5,4 +5,4 @@ sealed class HomeFeedSideEffect { data class FeedItemClicked(val selectedItemUrl: String) : HomeFeedSideEffect() data object NewsSourceFabClicked : HomeFeedSideEffect() data object ScrollToTopClicked : HomeFeedSideEffect() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/model/state/search/SearchState.kt b/app/src/main/java/com/rob729/newsfeed/model/state/search/SearchState.kt index e1937ca..dce44a4 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/state/search/SearchState.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/state/search/SearchState.kt @@ -5,4 +5,4 @@ import com.rob729.newsfeed.model.state.UiStatus data class SearchState( val uiStatus: UiStatus = UiStatus.EmptyScreen, val searchQuery: String = "", -) \ No newline at end of file +) diff --git a/app/src/main/java/com/rob729/newsfeed/network/NewsApiDataSourceImpl.kt b/app/src/main/java/com/rob729/newsfeed/network/NewsApiDataSourceImpl.kt index 983f6b3..e6b143a 100644 --- a/app/src/main/java/com/rob729/newsfeed/network/NewsApiDataSourceImpl.kt +++ b/app/src/main/java/com/rob729/newsfeed/network/NewsApiDataSourceImpl.kt @@ -11,6 +11,8 @@ import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.TimeZone import kotlinx.datetime.minus import kotlinx.datetime.todayIn +import okio.IOException +import retrofit2.HttpException class NewsApiDataSourceImpl( private val newsApi: NewsApi @@ -55,9 +57,14 @@ class NewsApiDataSourceImpl( "${Constants.ERROR_MESSAGE_PREFIX} ${result.message()}" ) } - } catch (exception: Exception) { - exception.printStackTrace() - NewsResource.Error("${Constants.ERROR_MESSAGE_PREFIX} ${exception.message}") + } catch (httpException: HttpException) { + // Handle HTTP exceptions (non-2xx responses) + httpException.printStackTrace() + NewsResource.Error("${Constants.ERROR_MESSAGE_PREFIX} ${httpException.message}") + } catch (ioException: IOException) { + // Handle network or I/O exceptions + ioException.printStackTrace() + NewsResource.Error("${Constants.ERROR_MESSAGE_PREFIX} ${ioException.message}") } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/ui/NavigationScreens.kt b/app/src/main/java/com/rob729/newsfeed/ui/NavigationScreens.kt index c6dc08c..38daeeb 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/NavigationScreens.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/NavigationScreens.kt @@ -3,4 +3,4 @@ package com.rob729.newsfeed.ui enum class NavigationScreens(val routeName: String) { HOME("home"), SEARCH("search") -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/ui/NewsActivity.kt b/app/src/main/java/com/rob729/newsfeed/ui/NewsActivity.kt index fb1d0ae..cf44728 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/NewsActivity.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/NewsActivity.kt @@ -26,6 +26,7 @@ import com.rob729.newsfeed.ui.screen.HomeScreen import com.rob729.newsfeed.ui.screen.SearchScreen import com.rob729.newsfeed.ui.theme.NewsFeedTheme import com.rob729.newsfeed.utils.Constants +import com.rob729.newsfeed.utils.Constants.ANIMATION_DURATION import com.rob729.newsfeed.utils.NotificationHelper @OptIn(ExperimentalComposeUiApi::class) @@ -62,25 +63,25 @@ class NewsActivity : ComponentActivity() { enterTransition = { slideIntoContainer( towards = AnimatedContentTransitionScope.SlideDirection.Companion.Left, - animationSpec = tween(500) + animationSpec = tween(ANIMATION_DURATION) ) }, exitTransition = { slideOutOfContainer( towards = AnimatedContentTransitionScope.SlideDirection.Companion.Left, - animationSpec = tween(500) + animationSpec = tween(ANIMATION_DURATION) ) }, popEnterTransition = { slideIntoContainer( towards = AnimatedContentTransitionScope.SlideDirection.Companion.Right, - animationSpec = tween(500) + animationSpec = tween(ANIMATION_DURATION) ) }, popExitTransition = { slideOutOfContainer( towards = AnimatedContentTransitionScope.SlideDirection.Companion.Right, - animationSpec = tween(500) + animationSpec = tween(ANIMATION_DURATION) ) }) { HomeScreen(navController, paddingValues = paddingValues) { @@ -92,25 +93,25 @@ class NewsActivity : ComponentActivity() { enterTransition = { slideIntoContainer( towards = AnimatedContentTransitionScope.SlideDirection.Companion.Left, - animationSpec = tween(500) + animationSpec = tween(ANIMATION_DURATION) ) }, exitTransition = { slideOutOfContainer( towards = AnimatedContentTransitionScope.SlideDirection.Companion.Left, - animationSpec = tween(500) + animationSpec = tween(ANIMATION_DURATION) ) }, popEnterTransition = { slideIntoContainer( towards = AnimatedContentTransitionScope.SlideDirection.Companion.Right, - animationSpec = tween(500) + animationSpec = tween(ANIMATION_DURATION) ) }, popExitTransition = { slideOutOfContainer( towards = AnimatedContentTransitionScope.SlideDirection.Companion.Right, - animationSpec = tween(500) + animationSpec = tween(ANIMATION_DURATION) ) }) { SearchScreen(navController) { diff --git a/app/src/main/java/com/rob729/newsfeed/ui/bottomSheet/NewsSourceBottomSheet.kt b/app/src/main/java/com/rob729/newsfeed/ui/bottomSheet/NewsSourceBottomSheet.kt index 45666fc..8c39bb7 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/bottomSheet/NewsSourceBottomSheet.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/bottomSheet/NewsSourceBottomSheet.kt @@ -7,7 +7,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import com.rob729.newsfeed.ui.components.NewsSourceBottomSheetContent @@ -35,4 +34,4 @@ fun NewsSourceBottomSheet( ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/FloatingActionButtons.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/FloatingActionButtons.kt index 4bc2086..beda689 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/FloatingActionButtons.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/FloatingActionButtons.kt @@ -50,4 +50,4 @@ fun ScrollToTopFab(modifier: Modifier, onClick: () -> Unit) { contentDescription = Constants.FAB_TITLE ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingShimmer.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingShimmer.kt index e2eed8c..baba9bc 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingShimmer.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingShimmer.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import com.rob729.newsfeed.utils.Constants.GRADIENT_START_COORDINATE @Composable fun LoadingShimmer() { @@ -33,11 +34,11 @@ fun LoadingShimmer() { ) val brush = Brush.linearGradient( colors = gradient, - start = Offset(200f, 200f), + start = Offset(GRADIENT_START_COORDINATE, GRADIENT_START_COORDINATE), end = Offset( x = translateAnimation.value, y = translateAnimation.value ) ) ShimmerListItem(brush = brush) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingView.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingView.kt index ecf1009..9584950 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingView.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingView.kt @@ -2,14 +2,15 @@ package com.rob729.newsfeed.ui.components import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable +import com.rob729.newsfeed.utils.Constants.SHIMMER_ITEM_COUNT @Composable fun LoadingView() { LazyColumn { - repeat(4) { + repeat(SHIMMER_ITEM_COUNT) { item { LoadingShimmer() } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/NewsFeedItem.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/NewsFeedItem.kt index 3a9d23b..6a7bc0c 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/NewsFeedItem.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/NewsFeedItem.kt @@ -41,6 +41,7 @@ import coil.request.CachePolicy import coil.request.ImageRequest import com.rob729.newsfeed.model.ui.NewsArticleUiData import com.rob729.newsfeed.ui.theme.lexendDecaFontFamily +import com.rob729.newsfeed.utils.Constants.NEWS_FEED_ITEM_IMAGE_CROSS_FADE_DURATION import kotlinx.datetime.Clock import kotlinx.datetime.toInstant @@ -78,7 +79,7 @@ fun NewsFeedItem( model = ImageRequest.Builder(LocalContext.current) .data(newsArticleUiData.imageUrl) .crossfade(true) - .crossfade(200) + .crossfade(NEWS_FEED_ITEM_IMAGE_CROSS_FADE_DURATION) .networkCachePolicy(CachePolicy.ENABLED) .memoryCachePolicy(CachePolicy.ENABLED) .diskCachePolicy(CachePolicy.DISABLED) @@ -166,4 +167,4 @@ fun shareArticle(context: Context, articleUrl: String) { val shareIntent = Intent.createChooser(sendIntent, null) startActivity(context, shareIntent, null) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/NewsSourceBottomSheetContent.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/NewsSourceBottomSheetContent.kt index 2540570..8ca71e9 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/NewsSourceBottomSheetContent.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/NewsSourceBottomSheetContent.kt @@ -18,6 +18,8 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import com.rob729.newsfeed.utils.Constants +private const val VISIBLE_CARDS = 4.25f +private const val ITEM_SPACING = 12 @Composable fun NewsSourceBottomSheetContent( onNewsSourceClicked: (String) -> Unit, @@ -26,9 +28,7 @@ fun NewsSourceBottomSheetContent( val vibrator = LocalContext.current.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator val newsSourceList = Constants.newsSourceUiDataLists val screenWidthDp = LocalConfiguration.current.screenWidthDp - val visibleCards = 4.25f - val itemSpacing = 12 - val itemWidth = (screenWidthDp - (itemSpacing * visibleCards)).div(visibleCards) + val itemWidth = (screenWidthDp - (ITEM_SPACING * VISIBLE_CARDS)).div(VISIBLE_CARDS) Box( modifier = Modifier.navigationBarsPadding(), @@ -38,7 +38,7 @@ fun NewsSourceBottomSheetContent( LazyRow( modifier = Modifier .testTag("news_source_list"), - horizontalArrangement = Arrangement.spacedBy(itemSpacing.dp) + horizontalArrangement = Arrangement.spacedBy(ITEM_SPACING.dp) ) { items(Constants.newsSourceUiDataLists.size, { index: Int -> newsSourceList[index].domain }) { index -> @@ -59,4 +59,4 @@ fun NewsSourceBottomSheetContent( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/NoInternetView.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/NoInternetView.kt index e310ddf..d233b8a 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/NoInternetView.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/NoInternetView.kt @@ -81,4 +81,4 @@ fun NoInternetView(onTryAgainClicked: () -> Unit) { ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/ShimmerListItem.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/ShimmerListItem.kt index cb24024..329a24d 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/ShimmerListItem.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/ShimmerListItem.kt @@ -57,4 +57,4 @@ fun ShimmerListItem(brush: Brush) { } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/Toolbar.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/Toolbar.kt index 370d40e..5b87c27 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/Toolbar.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/Toolbar.kt @@ -51,4 +51,4 @@ fun Toolbar(toolbarElevation: Dp, onSearchClick: () -> Unit) { }) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/ui/screen/HomeScreen.kt b/app/src/main/java/com/rob729/newsfeed/ui/screen/HomeScreen.kt index 28bf92d..997450d 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/screen/HomeScreen.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/screen/HomeScreen.kt @@ -159,7 +159,12 @@ fun HomeScreen( } } - NewsSourceBottomSheet(bottomSheetState, isNewsSourceBottomSheetVisible, viewModel::newsSourceClicked, newsState.selectedNewsSource) { + NewsSourceBottomSheet( + bottomSheetState, + isNewsSourceBottomSheetVisible, + viewModel::newsSourceClicked, + newsState.selectedNewsSource + ) { isNewsSourceBottomSheetVisible = false } @@ -184,4 +189,4 @@ private fun LazyListState.isScrollingUp(): Boolean { } } }.value -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/ui/screen/SearchScreen.kt b/app/src/main/java/com/rob729/newsfeed/ui/screen/SearchScreen.kt index 19045a6..c8e316e 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/screen/SearchScreen.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/screen/SearchScreen.kt @@ -58,8 +58,6 @@ fun SearchScreen( val listState = rememberLazyListState() - var active by rememberSaveable { mutableStateOf(false) } - viewModel.collectSideEffect { when (it) { is SearchSideEffects.SearchQueryChanged -> { @@ -147,4 +145,4 @@ fun SearchScreen( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/ui/theme/Shape.kt b/app/src/main/java/com/rob729/newsfeed/ui/theme/Shape.kt index 1f84f74..2ec81d1 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/theme/Shape.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/theme/Shape.kt @@ -8,4 +8,4 @@ val Shapes = Shapes( small = RoundedCornerShape(4.dp), medium = RoundedCornerShape(4.dp), large = RoundedCornerShape(0.dp) -) \ No newline at end of file +) diff --git a/app/src/main/java/com/rob729/newsfeed/ui/theme/Theme.kt b/app/src/main/java/com/rob729/newsfeed/ui/theme/Theme.kt index 6474046..3076b18 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/theme/Theme.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/theme/Theme.kt @@ -86,4 +86,4 @@ fun NewsFeedTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composab shapes = Shapes, content = content ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/ui/theme/Type.kt b/app/src/main/java/com/rob729/newsfeed/ui/theme/Type.kt index 119696d..a76badc 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/theme/Type.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/theme/Type.kt @@ -15,4 +15,4 @@ val lexendDecaFontFamily = FontFamily( Font(R.font.lexenddeca_semibold, FontWeight.SemiBold), Font(R.font.lexenddeca_bold, FontWeight.Bold), Font(R.font.lexenddeca_light, FontWeight.Light), -) \ No newline at end of file +) diff --git a/app/src/main/java/com/rob729/newsfeed/utils/Constants.kt b/app/src/main/java/com/rob729/newsfeed/utils/Constants.kt index e7da52f..96972e6 100644 --- a/app/src/main/java/com/rob729/newsfeed/utils/Constants.kt +++ b/app/src/main/java/com/rob729/newsfeed/utils/Constants.kt @@ -43,4 +43,10 @@ object Constants { const val ERROR_MESSAGE_PREFIX = "Something went wrong" const val API_RESULT_LANGUAGE = "en" const val SORT_RESULT_FILTER_PUBLISHED_AT = "publishedAt" -} \ No newline at end of file + const val SHIMMER_ITEM_COUNT = 4 + const val ANIMATION_DURATION = 500 + const val MAX_CACHE_DATA_VALID_DURATION_IN_HOURS = 6 + const val SEARCH_QUERY_UPDATE_DEBOUNCE_TIME = 1000L + const val NEWS_FEED_ITEM_IMAGE_CROSS_FADE_DURATION = 200 + const val GRADIENT_START_COORDINATE = 200f +} diff --git a/app/src/main/java/com/rob729/newsfeed/utils/NotificationHelper.kt b/app/src/main/java/com/rob729/newsfeed/utils/NotificationHelper.kt index 5c5cbf3..df03adf 100644 --- a/app/src/main/java/com/rob729/newsfeed/utils/NotificationHelper.kt +++ b/app/src/main/java/com/rob729/newsfeed/utils/NotificationHelper.kt @@ -71,11 +71,11 @@ class NotificationHelper(private val context: Context) { fun scheduleNotification() { val currentDate = Calendar.getInstance() val dueDate = Calendar.getInstance() - dueDate.set(Calendar.HOUR_OF_DAY, 8) - dueDate.set(Calendar.MINUTE, 30) - dueDate.set(Calendar.SECOND, 0) + dueDate.set(Calendar.HOUR_OF_DAY, NOTIFICATION_HOUR) + dueDate.set(Calendar.MINUTE, NOTIFICATION_MINUTE) + dueDate.set(Calendar.SECOND, NOTIFICATION_SECONDS) if (dueDate.before(currentDate)) { - dueDate.add(Calendar.HOUR_OF_DAY, 24) + dueDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_A_DAY) } val timeDiff = dueDate.timeInMillis - currentDate.timeInMillis @@ -102,5 +102,9 @@ class NotificationHelper(private val context: Context) { private const val CHANNEL_ID = "Daily News Reminder" private const val WORK_NAME = "DAILY_NEWS_REMINDER" private const val NOTIFICATION_CHANNEL_DESCRIPTION = "News Reminder Channel Description" + private const val NOTIFICATION_HOUR = 8 + private const val NOTIFICATION_MINUTE = 30 + private const val NOTIFICATION_SECONDS = 0 + private const val HOURS_IN_A_DAY = 24 } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/vm/NewsRepository.kt b/app/src/main/java/com/rob729/newsfeed/vm/NewsRepository.kt index 3c57c72..d588aa7 100644 --- a/app/src/main/java/com/rob729/newsfeed/vm/NewsRepository.kt +++ b/app/src/main/java/com/rob729/newsfeed/vm/NewsRepository.kt @@ -4,6 +4,7 @@ import com.rob729.newsfeed.database.NewsDBDataSource import com.rob729.newsfeed.model.NewsResource import com.rob729.newsfeed.model.api.NetworkNews import com.rob729.newsfeed.network.NewsApiDataSource +import com.rob729.newsfeed.utils.Constants.MAX_CACHE_DATA_VALID_DURATION_IN_HOURS import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.datetime.Clock @@ -44,7 +45,7 @@ class NewsRepository( newsResource } } catch (e: IOException) { - return NewsResource.Error("No Internet") + return NewsResource.Error("No Internet ${e.localizedMessage}") } } @@ -53,13 +54,14 @@ class NewsRepository( try { emit(newsApiDataSource.getNewsSearchResults(query)) } catch (e: IOException) { - emit(NewsResource.Error("No Internet")) + emit(NewsResource.Error("No Internet ${e.localizedMessage}")) } } private fun checkIfNewsSourceDataIsOutdated(newsSourceFetchTimeInMillis: Long?): Boolean { if (newsSourceFetchTimeInMillis == null) return true - return (Clock.System.now() - Instant.fromEpochMilliseconds(newsSourceFetchTimeInMillis)).inWholeHours > 6 + return (Clock.System.now() - Instant.fromEpochMilliseconds(newsSourceFetchTimeInMillis)) + .inWholeHours > MAX_CACHE_DATA_VALID_DURATION_IN_HOURS } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/vm/SearchViewModel.kt b/app/src/main/java/com/rob729/newsfeed/vm/SearchViewModel.kt index b18916e..5675277 100644 --- a/app/src/main/java/com/rob729/newsfeed/vm/SearchViewModel.kt +++ b/app/src/main/java/com/rob729/newsfeed/vm/SearchViewModel.kt @@ -9,6 +9,7 @@ import com.rob729.newsfeed.model.state.UiStatus import com.rob729.newsfeed.model.state.search.SearchSideEffects import com.rob729.newsfeed.model.state.search.SearchState import com.rob729.newsfeed.model.ui.NewsArticleUiData +import com.rob729.newsfeed.utils.Constants.SEARCH_QUERY_UPDATE_DEBOUNCE_TIME import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChangedBy @@ -31,7 +32,8 @@ class SearchViewModel( init { viewModelScope.launch { - container.stateFlow.debounce(1000).distinctUntilChangedBy { it.searchQuery } + container.stateFlow + .debounce(SEARCH_QUERY_UPDATE_DEBOUNCE_TIME).distinctUntilChangedBy { it.searchQuery } .collectLatest { searchNewsResultsForQuery(it.searchQuery) } @@ -80,11 +82,14 @@ class SearchViewModel( (newsResource.data as? NetworkNews)?.let { reduce { state.copy( - uiStatus = UiStatus.Success(it.networkArticles.mapNotNull(::mapNetworkArticleToNewsArticleUiData)) + uiStatus = UiStatus.Success( + it.networkArticles + .mapNotNull(::mapNetworkArticleToNewsArticleUiData) + ) ) } } } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/workManager/NewsReminder.kt b/app/src/main/java/com/rob729/newsfeed/workManager/NewsReminder.kt index 6b75b91..502c0e7 100644 --- a/app/src/main/java/com/rob729/newsfeed/workManager/NewsReminder.kt +++ b/app/src/main/java/com/rob729/newsfeed/workManager/NewsReminder.kt @@ -22,4 +22,4 @@ class NewsReminder(private val context: Context, params: WorkerParameters) : notificationHelper.scheduleNotification() return Result.success() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/rob729/newsfeed/workManager/NewsSourceImagePrefetch.kt b/app/src/main/java/com/rob729/newsfeed/workManager/NewsSourceImagePrefetch.kt index 9a3f95b..cb7018b 100644 --- a/app/src/main/java/com/rob729/newsfeed/workManager/NewsSourceImagePrefetch.kt +++ b/app/src/main/java/com/rob729/newsfeed/workManager/NewsSourceImagePrefetch.kt @@ -20,4 +20,4 @@ class NewsSourceImagePrefetch(private val context: Context, params: WorkerParame } return Result.success() } -} \ No newline at end of file +} diff --git a/app/src/test/java/com/rob729/newsfeed/ExampleUnitTest.kt b/app/src/test/java/com/rob729/newsfeed/ExampleUnitTest.kt index 4f14376..e623829 100644 --- a/app/src/test/java/com/rob729/newsfeed/ExampleUnitTest.kt +++ b/app/src/test/java/com/rob729/newsfeed/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package com.rob729.newsfeed +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * @@ -14,4 +13,4 @@ class ExampleUnitTest { fun addition_isCorrect() { assertEquals(4, 2 + 2) } -} \ No newline at end of file +} diff --git a/baselineprofile/src/main/java/com/rob729/baselineprofile/BaselineProfileGenerator.kt b/baselineprofile/src/main/java/com/rob729/baselineprofile/BaselineProfileGenerator.kt index 894e4a4..6609284 100644 --- a/baselineprofile/src/main/java/com/rob729/baselineprofile/BaselineProfileGenerator.kt +++ b/baselineprofile/src/main/java/com/rob729/baselineprofile/BaselineProfileGenerator.kt @@ -48,4 +48,4 @@ class BaselineProfileGenerator { scrollAndNavigate() } } -} \ No newline at end of file +} diff --git a/baselineprofile/src/main/java/com/rob729/baselineprofile/StartupBenchmarks.kt b/baselineprofile/src/main/java/com/rob729/baselineprofile/StartupBenchmarks.kt index b5da8a1..f141fe7 100644 --- a/baselineprofile/src/main/java/com/rob729/baselineprofile/StartupBenchmarks.kt +++ b/baselineprofile/src/main/java/com/rob729/baselineprofile/StartupBenchmarks.kt @@ -197,4 +197,4 @@ fun MacrobenchmarkScope.scrollAndNavigate() { TestCase.fail("News details screen didn't opened") } } -} \ No newline at end of file +} diff --git a/build.gradle b/build.gradle index 70dea0a..936e25a 100644 --- a/build.gradle +++ b/build.gradle @@ -6,8 +6,20 @@ plugins { alias(libs.plugins.android.test) apply false alias(libs.plugins.androidx.baselineprofile) apply false alias(libs.plugins.kotlin.parcelize) apply false + alias(libs.plugins.detekt) +} + +dependencies { + detektPlugins(libs.detekt.formatting) +} + +detekt { + autoCorrect = true + buildUponDefaultConfig = true + allRules = false + config.setFrom("$projectDir/config/detekt/detekt.yml") } task clean(type: Delete) { delete rootProject.buildDir -} \ No newline at end of file +} diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml new file mode 100644 index 0000000..23305e9 --- /dev/null +++ b/config/detekt/detekt.yml @@ -0,0 +1,786 @@ +build: + maxIssues: 0 + excludeCorrectable: false + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: false + checkExhaustiveness: false + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' + +processors: + active: true + exclude: + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' + +console-reports: + active: true + exclude: +# - 'ProjectStatisticsReport' +# - 'ComplexityReport' +# - 'NotificationReport' +# - 'FindingsReport' +# - 'FileBasedFindingsReport' + # - 'LiteFindingsReport' + +output-reports: + active: true + exclude: + # - 'TxtOutputReport' + # - 'XmlOutputReport' + # - 'HtmlOutputReport' + - 'MdOutputReport' + - 'SarifOutputReport' + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: false + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + UndocumentedPublicFunction: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedProperty: false + +complexity: + active: true + CognitiveComplexMethod: + active: false + threshold: 15 + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: false + ignoredLabels: [] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 105 + LongParameterList: + active: true + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotatedParameter: [] + MethodOverloading: + active: false + threshold: 6 + NamedArguments: + active: false + threshold: 3 + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + threshold: 4 + NestedScopeFunctions: + active: false + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + +coroutines: + active: true + GlobalCoroutineUsage: + active: false + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: false + SuspendFunWithCoroutineScopeReceiver: + active: false + SuspendFunWithFlowReturnType: + active: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: false + ObjectExtendsThrowable: + active: false + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: false + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [] + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '[a-z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + ignoreAnnotated: ['Composable'] + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: false + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: true + mustBeFirst: true + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: false + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: false + threshold: 3 + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryPartOfBinaryExpression: + active: false + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: false + CastToNullableType: + active: false + Deprecation: + active: false + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: false + ignoredSubjectTypes: [] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: false + excludes: ['**/*.kts'] + NullCheckOnMutableProperty: + active: false + NullableToStringCall: + active: false + PropertyUsedBeforeDeclaration: + active: false + UnconditionalJumpStatementInLoop: + active: false + UnnecessaryNotNullCheck: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnsafeCast: + active: true + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AlsoCouldBeApply: + active: false + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: false + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: false + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'to' + allowOperators: false + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: false + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenAnnotation: + active: false + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'Forbidden TODO todo marker in comment, please do the changes.' + value: 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: false + imports: [] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: false + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: + active: false + rules: [] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: true + ignoreLocalVariableDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: false + MaxChainedCallsOnSameLine: + active: false + maxChainedCalls: 5 + MaxLineLength: + active: true + maxLineLength: 120 + excludes: ['**/Constants.kt'] + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + excludeRawStrings: true + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + MultilineRawStringIndentation: + active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + NullableBooleanCheck: + active: false + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: true + max: 2 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: false + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: false + StringShouldBeRawString: + active: false + maxEscapedCharacterCount: 2 + ignoredCharacters: [] + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: false + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAbstractClass: + active: true + UnnecessaryAnnotationUseSiteTarget: + active: false + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: false + UnnecessaryBracesAroundTrailingLambda: + active: false + UnnecessaryFilter: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + allowForUnclearPrecedence: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedParameter: + active: true + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '' + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: false + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: false + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + ignoreLateinitVar: false + WildcardImport: + active: true + excludeImports: + - 'java.util.*' diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 11031eb..c8f8b26 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ coil = "2.5.0" constraintlayoutCompose = "1.0.1" coreKtx = "1.12.0" datastorePreferences = "1.0.0" +detekt = "1.23.3" espressoCore = "3.5.1" junit = "4.13.2" junitVersion = "1.1.5" @@ -63,6 +64,7 @@ coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } compose-bom = "androidx.compose:compose-bom:2023.10.01" compose-ui = { module = "androidx.compose.ui:ui" } converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" } +detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } junit = { module = "junit:junit", version.ref = "junit" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinAndroid" } koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koinAndroid" } @@ -83,4 +85,5 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } -androidx-baselineprofile = { id = "androidx.baselineprofile", version.ref = "baselineProfile" } \ No newline at end of file +androidx-baselineprofile = { id = "androidx.baselineprofile", version.ref = "baselineProfile" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } \ No newline at end of file