diff --git a/.github/workflows/testandlint.yaml b/.github/workflows/testandlint.yaml index 880a088..ec75d8b 100644 --- a/.github/workflows/testandlint.yaml +++ b/.github/workflows/testandlint.yaml @@ -26,11 +26,6 @@ jobs: java-version: 11 - name: Unit tests run: bash ./gradlew test --stacktrace - - name: Unit tests results - uses: actions/upload-artifact@v1 - with: - name: unit-tests-results - path: app/build/reports/tests/testDebugUnitTest/index.html lint: name: Lint Check @@ -45,8 +40,8 @@ jobs: java-version: 11 - name: Make gradlew executable run: chmod +x ./gradlew - - name: Lint debug flavor - run: ./gradlew lint --stacktrace + - name: Lint Checks + run: ./gradlew detektAll lintKotlin lint - name: Lint results uses: yutailang0119/action-android-lint@v2 with: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 36cea9f..4bcadc6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,10 +1,13 @@ import com.loodos.buildsrc.getDateTime +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") plugins { - id("com.android.application") - kotlin("android") - id("kotlin-kapt") - id("dagger.hilt.android.plugin") + kotlin("kapt") + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.dagger.hilt) + alias(libs.plugins.kotlinter) } android { diff --git a/app/src/androidTest/java/com/loodos/samplecomposeandroid/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/loodos/samplecomposeandroid/ExampleInstrumentedTest.kt index 17422f8..20da553 100644 --- a/app/src/androidTest/java/com/loodos/samplecomposeandroid/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/loodos/samplecomposeandroid/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package com.loodos.samplecomposeandroid -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.loodos.samplecomposeandroid", appContext.packageName) } -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/com/loodos/samplecomposeandroid/feature/appstate/MainAppStateTest.kt b/app/src/androidTest/java/com/loodos/samplecomposeandroid/feature/appstate/MainAppStateTest.kt index 6f11697..343c4f2 100644 --- a/app/src/androidTest/java/com/loodos/samplecomposeandroid/feature/appstate/MainAppStateTest.kt +++ b/app/src/androidTest/java/com/loodos/samplecomposeandroid/feature/appstate/MainAppStateTest.kt @@ -35,7 +35,6 @@ class MainAppStateTest { // Subject under test. private lateinit var state: MainAppState - @Test fun mainAppState_currentDestination() = runTest { var currentDestination: String? = null @@ -46,7 +45,7 @@ class MainAppStateTest { MainAppState( navController = navController, networkMonitor = networkMonitor, - coroutineScope = backgroundScope + coroutineScope = backgroundScope, ) } @@ -63,7 +62,6 @@ class MainAppStateTest { assertEquals("b", currentDestination) } - @Test fun mainAppState_stateIsOfflineWhenNetworkMonitorIsOffline() = runTest(UnconfinedTestDispatcher()) { @@ -71,7 +69,7 @@ class MainAppStateTest { state = MainAppState( navController = NavHostController(LocalContext.current), networkMonitor = networkMonitor, - coroutineScope = backgroundScope + coroutineScope = backgroundScope, ) } backgroundScope.launch { state.isOffline.collect() } @@ -86,7 +84,7 @@ class MainAppStateTest { state = MainAppState( navController = NavHostController(LocalContext.current), networkMonitor = networkMonitor, - coroutineScope = backgroundScope + coroutineScope = backgroundScope, ) } backgroundScope.launch { state.isOffline.collect() } @@ -109,4 +107,4 @@ private fun rememberTestNavController(): TestNavHostController { } } return navController -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/MainApplication.kt b/app/src/main/java/com/loodos/MainApplication.kt index 8ed4dc8..9a6a087 100644 --- a/app/src/main/java/com/loodos/MainApplication.kt +++ b/app/src/main/java/com/loodos/MainApplication.kt @@ -7,4 +7,4 @@ import dagger.hilt.android.HiltAndroidApp * Created by mertcantoptas on 07.03.2023 */ @HiltAndroidApp -class MainApplication :Application() {} \ No newline at end of file +class MainApplication : Application() diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/arch/BaseViewModel.kt b/app/src/main/java/com/loodos/samplecomposeandroid/arch/BaseViewModel.kt index 7d4c4fc..118b34f 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/arch/BaseViewModel.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/arch/BaseViewModel.kt @@ -21,5 +21,3 @@ abstract class BaseViewModel : ViewModel() { _uiState.update { currentState.reduce() } } } - - diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/di/DataModule.kt b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/di/DataModule.kt index f33929c..9b146e1 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/di/DataModule.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/di/DataModule.kt @@ -15,6 +15,6 @@ import dagger.hilt.components.SingletonComponent interface DataModule { @Binds fun bindsNetworkMonitor( - networkMonitor: ConnectivityManagerNetworkMonitor + networkMonitor: ConnectivityManagerNetworkMonitor, ): NetworkMonitor -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/di/LoginModule.kt b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/di/LoginModule.kt index f282fb4..3f5aa78 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/di/LoginModule.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/di/LoginModule.kt @@ -22,4 +22,4 @@ interface LoginModule { @Binds fun bindAuthRepository(repositoryImpl: AuthenticationRepositoryImpl): AuthenticationRepository -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/di/RemoteDataModule.kt b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/di/RemoteDataModule.kt index 2a095bb..581b220 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/di/RemoteDataModule.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/di/RemoteDataModule.kt @@ -24,6 +24,9 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object RemoteDataModule { + + private const val CHUCKER_MAX_CONTENT_LENGTH = 250_000L + @Provides @Singleton fun provideRetrofit( @@ -52,6 +55,7 @@ object RemoteDataModule { fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor = HttpLoggingInterceptor().apply { setLevel(HttpLoggingInterceptor.Level.BODY) } + @Singleton @Provides fun provideGsonConverterFactory(): GsonConverterFactory { @@ -64,9 +68,8 @@ object RemoteDataModule { @ApplicationContext context: Context, chuckerCollector: ChuckerCollector, ): ChuckerInterceptor { - return ChuckerInterceptor.Builder(context).collector(chuckerCollector) - .maxContentLength(250_000L) + .maxContentLength(CHUCKER_MAX_CONTENT_LENGTH) .redactHeaders("Content-Type", "application/json") .alwaysReadResponseBody(true).build() } @@ -79,13 +82,12 @@ object RemoteDataModule { // Toggles visibility of the push notification showNotification = true, // Allows to customize the retention period of collected data - retentionPeriod = RetentionManager.Period.ONE_HOUR + retentionPeriod = RetentionManager.Period.ONE_HOUR, ) - @Provides @Singleton fun provideLoginService(retrofit: Retrofit): AuthenticationService { return retrofit.create(AuthenticationService::class.java) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/model/login/LoginBody.kt b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/model/login/LoginBody.kt index cb7dc58..f163812 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/model/login/LoginBody.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/model/login/LoginBody.kt @@ -9,5 +9,5 @@ data class LoginBody( @SerializedName("username") val username: String, @SerializedName("password") - val password: String -) \ No newline at end of file + val password: String, +) diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/model/login/LoginResponse.kt b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/model/login/LoginResponse.kt index 8de144b..64b7ccd 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/model/login/LoginResponse.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/model/login/LoginResponse.kt @@ -4,5 +4,5 @@ package com.loodos.samplecomposeandroid.core.data.model.login * Created by mertcantoptas on 13.04.2023 */ data class LoginResponse( - val token: String + val token: String, ) diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/remote/api/AuthenticationService.kt b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/remote/api/AuthenticationService.kt index 8a86a39..3c9df17 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/remote/api/AuthenticationService.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/remote/api/AuthenticationService.kt @@ -2,7 +2,6 @@ package com.loodos.samplecomposeandroid.core.data.remote.api import com.loodos.samplecomposeandroid.core.data.model.login.LoginBody import com.loodos.samplecomposeandroid.core.data.model.login.LoginResponse -import retrofit2.Response import retrofit2.http.Body import retrofit2.http.POST @@ -13,6 +12,6 @@ import retrofit2.http.POST interface AuthenticationService { @POST("auth/login") suspend fun login( - @Body requestBody: LoginBody + @Body requestBody: LoginBody, ): LoginResponse -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/remote/source/AuthenticationRemoteDataSource.kt b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/remote/source/AuthenticationRemoteDataSource.kt index 059e759..5b11a37 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/remote/source/AuthenticationRemoteDataSource.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/remote/source/AuthenticationRemoteDataSource.kt @@ -1,11 +1,10 @@ package com.loodos.samplecomposeandroid.core.data.remote.source import com.loodos.samplecomposeandroid.core.data.model.login.LoginResponse -import kotlinx.coroutines.flow.Flow /** * Created by mertcantoptas on 13.04.2023 */ interface AuthenticationRemoteDataSource { suspend fun login(username: String, password: String): Result -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/repository/AuthenticationRepository.kt b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/repository/AuthenticationRepository.kt index 431af14..993897a 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/repository/AuthenticationRepository.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/repository/AuthenticationRepository.kt @@ -1,11 +1,10 @@ package com.loodos.samplecomposeandroid.core.data.repository import com.loodos.samplecomposeandroid.core.data.model.login.LoginResponse -import kotlinx.coroutines.flow.Flow /** * Created by mertcantoptas on 13.04.2023 */ interface AuthenticationRepository { suspend fun login(username: String, password: String): Result -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/repository/AuthenticationRepositoryImpl.kt b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/repository/AuthenticationRepositoryImpl.kt index be40dfc..896ba33 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/core/data/repository/AuthenticationRepositoryImpl.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/core/data/repository/AuthenticationRepositoryImpl.kt @@ -2,14 +2,13 @@ package com.loodos.samplecomposeandroid.core.data.repository import com.loodos.samplecomposeandroid.core.data.model.login.LoginResponse import com.loodos.samplecomposeandroid.core.data.remote.source.AuthenticationRemoteDataSource -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class AuthenticationRepositoryImpl @Inject constructor( - private val authenticationRemoteDataSource: AuthenticationRemoteDataSource + private val authenticationRemoteDataSource: AuthenticationRemoteDataSource, ) : AuthenticationRepository { override suspend fun login(username: String, password: String): Result { return authenticationRemoteDataSource.login(username, password) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/LoginExceptions.kt b/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/LoginExceptions.kt index 698f5a7..8cd3932 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/LoginExceptions.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/LoginExceptions.kt @@ -7,4 +7,4 @@ package com.loodos.samplecomposeandroid.core.domain class UsernameRequiredException : Exception() class PasswordRequiredException : Exception() class UsernameLengthException : Exception() -class PasswordLengthException : Exception() \ No newline at end of file +class PasswordLengthException : Exception() diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/login/LoginResult.kt b/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/login/LoginResult.kt index 72b9409..9f1e793 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/login/LoginResult.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/login/LoginResult.kt @@ -6,6 +6,6 @@ data class LoginResult( val token: String = "", ) -fun LoginResponse.toModel() : LoginResult { +fun LoginResponse.toModel(): LoginResult { return LoginResult(this.token) } diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/login/LoginUseCase.kt b/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/login/LoginUseCase.kt index 0cdd7da..2047ae9 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/login/LoginUseCase.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/login/LoginUseCase.kt @@ -6,14 +6,14 @@ import kotlinx.coroutines.flow.flow import javax.inject.Inject class LoginUseCase @Inject constructor( - private val authenticationRepository: AuthenticationRepository + private val authenticationRepository: AuthenticationRepository, ) { - operator fun invoke(username: String, password: String) : Flow { + operator fun invoke(username: String, password: String): Flow { return flow { val result = authenticationRepository.login(username, password) - (result.getOrNull() ?: throw Exception("error message")).also { + (result.getOrNull() ?: throw IllegalArgumentException("error message")).also { emit(it.toModel()) } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/login/ValidateAuthUseCase.kt b/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/login/ValidateAuthUseCase.kt index 46ad610..3eb69f2 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/login/ValidateAuthUseCase.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/core/domain/login/ValidateAuthUseCase.kt @@ -14,19 +14,22 @@ import javax.inject.Inject * Created by mertcantoptas on 12.05.2023 */ +private const val MinUsernameLength = 6 +private const val MinPasswordLength = 6 + class ValidateAuthUseCase @Inject constructor() { operator fun invoke(username: String, password: String): Flow> { return flow { if (username.isEmpty()) { throw UsernameRequiredException() } - if (username.length < 6) { + if (username.length < MinUsernameLength) { throw UsernameLengthException() } if (password.isEmpty()) { throw PasswordRequiredException() } - if (password.length < 6) { + if (password.length < MinPasswordLength) { throw PasswordLengthException() } emit(Unit) diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/core/util/ConnectivityManagerNetworkMonitor.kt b/app/src/main/java/com/loodos/samplecomposeandroid/core/util/ConnectivityManagerNetworkMonitor.kt index 76d7595..663f1a3 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/core/util/ConnectivityManagerNetworkMonitor.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/core/util/ConnectivityManagerNetworkMonitor.kt @@ -19,7 +19,7 @@ import javax.inject.Inject */ class ConnectivityManagerNetworkMonitor @Inject constructor( - @ApplicationContext private val context: Context + @ApplicationContext private val context: Context, ) : NetworkMonitor { override val isOnline: Flow = callbackFlow { val connectivityManager = context.getSystemService() @@ -35,7 +35,7 @@ class ConnectivityManagerNetworkMonitor @Inject constructor( override fun onCapabilitiesChanged( network: Network, - networkCapabilities: NetworkCapabilities + networkCapabilities: NetworkCapabilities, ) { channel.trySend(connectivityManager.isCurrentlyConnected()) } @@ -45,7 +45,7 @@ class ConnectivityManagerNetworkMonitor @Inject constructor( NetworkRequest.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .build(), - callback + callback, ) channel.trySend(connectivityManager.isCurrentlyConnected()) diff --git "a/app/src/main/java/com/loodos/samplecomposeandroid/core/util/NetworkMonit\303\266r.kt" b/app/src/main/java/com/loodos/samplecomposeandroid/core/util/NetworkMonitor.kt similarity index 100% rename from "app/src/main/java/com/loodos/samplecomposeandroid/core/util/NetworkMonit\303\266r.kt" rename to app/src/main/java/com/loodos/samplecomposeandroid/core/util/NetworkMonitor.kt diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/feature/appstate/MainApp.kt b/app/src/main/java/com/loodos/samplecomposeandroid/feature/appstate/MainApp.kt index 92f0185..03afccc 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/feature/appstate/MainApp.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/feature/appstate/MainApp.kt @@ -2,7 +2,10 @@ package com.loodos.samplecomposeandroid.feature.appstate -import androidx.compose.material3.* +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -13,16 +16,16 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.loodos.samplecomposeandroid.core.util.NetworkMonitor -import com.loodos.samplecomposeandroid.ui.components.MainAppScaffold import com.loodos.samplecomposeandroid.R +import com.loodos.samplecomposeandroid.core.util.NetworkMonitor import com.loodos.samplecomposeandroid.feature.navigation.MainNavHost +import com.loodos.samplecomposeandroid.ui.components.MainAppScaffold /** * Created by mertcantoptas on 10.03.2023 */ @OptIn( - ExperimentalComposeUiApi::class + ExperimentalComposeUiApi::class, ) @Composable fun MainApp( @@ -37,10 +40,12 @@ fun MainApp( val notConnectedMessage = stringResource(R.string.not_network_connected) LaunchedEffect(isOffline) { - if (isOffline) snackbarHostState.showSnackbar( - message = notConnectedMessage, - duration = SnackbarDuration.Indefinite - ) + if (isOffline) { + snackbarHostState.showSnackbar( + message = notConnectedMessage, + duration = SnackbarDuration.Indefinite, + ) + } } MainAppScaffold( @@ -50,7 +55,7 @@ fun MainApp( backgroundColor = MaterialTheme.colorScheme.background, snackbarHost = { SnackbarHost(snackbarHostState) }, - ) { + ) { MainNavHost( navController = appState.navController, ) diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/feature/appstate/MainAppState.kt b/app/src/main/java/com/loodos/samplecomposeandroid/feature/appstate/MainAppState.kt index 3f2c1b4..9cd118a 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/feature/appstate/MainAppState.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/feature/appstate/MainAppState.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.stateIn fun rememberMainAppState( networkMonitor: NetworkMonitor, coroutineScope: CoroutineScope = rememberCoroutineScope(), - navController: NavHostController = rememberNavController() + navController: NavHostController = rememberNavController(), ): MainAppState { return remember(navController, coroutineScope, networkMonitor) { MainAppState(navController, coroutineScope, networkMonitor) @@ -39,13 +39,12 @@ class MainAppState( @Composable get() = navController .currentBackStackEntryAsState().value?.destination - val isOffline = networkMonitor.isOnline .map(Boolean::not) .stateIn( scope = coroutineScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = false + initialValue = false, ) fun onBackClick() { diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/feature/home/HomeScreen.kt b/app/src/main/java/com/loodos/samplecomposeandroid/feature/home/HomeScreen.kt index b30fe32..a649162 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/feature/home/HomeScreen.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/feature/home/HomeScreen.kt @@ -1,7 +1,16 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + package com.loodos.samplecomposeandroid.feature.home -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -9,9 +18,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import com.loodos.samplecomposeandroid.feature.home.navigation.homeNavigationRoute import com.loodos.samplecomposeandroid.ui.components.MainAppScaffold /** @@ -20,15 +26,13 @@ import com.loodos.samplecomposeandroid.ui.components.MainAppScaffold @Composable internal fun HomeScreenRoute( modifier: Modifier = Modifier, - viewModel: HomeViewModel = hiltViewModel() + viewModel: HomeViewModel = hiltViewModel(), ) { val homeUiState by viewModel.uiState.collectAsStateWithLifecycle() HomeScreen(homeUiState, modifier) } - -@OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeScreen( homeUiState: HomeUiState, @@ -40,7 +44,7 @@ fun HomeScreen( CenterAlignedTopAppBar( modifier = Modifier.fillMaxWidth(), title = { - Text(text = "Home Screen") + Text(text = homeUiState.title) }, ) }, @@ -57,8 +61,8 @@ fun Content(modifier: Modifier = Modifier) { .imePadding() .navigationBarsPadding(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Center, ) { Text(text = "This is Loodos ", color = Color.Blue) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/feature/home/HomeViewModel.kt b/app/src/main/java/com/loodos/samplecomposeandroid/feature/home/HomeViewModel.kt index 570a48a..765fe76 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/feature/home/HomeViewModel.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/feature/home/HomeViewModel.kt @@ -16,4 +16,5 @@ class HomeViewModel @Inject constructor() : BaseViewModel() { data class HomeUiState( val isLoading: Boolean = false, -) : IViewState \ No newline at end of file + val title: String = "Home", +) : IViewState diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/feature/home/navigation/HomeNavigation.kt b/app/src/main/java/com/loodos/samplecomposeandroid/feature/home/navigation/HomeNavigation.kt index 4b10e33..778016a 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/feature/home/navigation/HomeNavigation.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/feature/home/navigation/HomeNavigation.kt @@ -9,14 +9,14 @@ import com.loodos.samplecomposeandroid.feature.home.HomeScreenRoute /** * Created by mertcantoptas on 10.03.2023 */ -const val homeNavigationRoute = "home_route" +const val HomeNavigationRoute = "home_route" fun NavController.navigateHomeScreen(navOptions: NavOptions? = null) { - this.navigate(homeNavigationRoute, navOptions) + this.navigate(HomeNavigationRoute, navOptions) } fun NavGraphBuilder.homeScreen() { - composable(route = homeNavigationRoute) { + composable(route = HomeNavigationRoute) { HomeScreenRoute() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/feature/login/LoginScreen.kt b/app/src/main/java/com/loodos/samplecomposeandroid/feature/login/LoginScreen.kt index 3be7b60..8843d97 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/feature/login/LoginScreen.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/feature/login/LoginScreen.kt @@ -3,18 +3,27 @@ package com.loodos.samplecomposeandroid.feature.login import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.layout.width import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.* +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.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -46,7 +55,7 @@ internal fun LoginScreenRoute( EventEffect( event = loginUIState.navigateToHome, onConsumed = viewModel::onConsumeSingleEvent, - action = navigateToHome + action = navigateToHome, ) LoginScreen( @@ -67,7 +76,7 @@ fun LoginScreen( onLoginClicked: () -> Unit, ) { MainAppScaffold( - modifier = modifier.fillMaxSize() + modifier = modifier.fillMaxSize(), ) { Content( loginUIState, @@ -79,7 +88,6 @@ fun LoginScreen( } } - @Composable private fun Content( loginUIState: LoginViewState, @@ -88,109 +96,136 @@ private fun Content( modifier: Modifier = Modifier, onLoginClicked: () -> Unit, ) { - var isPasswordVisible by remember { mutableStateOf(false) } Column( modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Center, ) { Image( modifier = Modifier.size(120.dp), painter = painterResource(id = R.drawable.icon_compose), - contentDescription = "" + contentDescription = "", ) Spacer(modifier = Modifier.height(70.dp)) - CustomTextField( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + UserNameTextField( value = loginUIState.userName, - label = { Text(text = stringResource(id = R.string.username_hint)) }, + errorMessageRes = loginUIState.usernameErrorMessage, onValueChange = onUserNameValueChange, - isError = loginUIState.usernameErrorMessage != null, - trailingIcon = { - AnimatedVisibility(visible = loginUIState.userName.isNotEmpty()) { - Image( - modifier = Modifier - .size(24.dp) - .clickable { - onUserNameValueChange("") - }, - imageVector = ImageVector.vectorResource(id = R.drawable.ic_cancel), - contentDescription = "" - ) - } - - }, - supportingText = { - AnimatedVisibility(visible = loginUIState.usernameErrorMessage != null) { - val errorMessage = loginUIState.usernameErrorMessage?.let { - stringResource(id = it) - } ?: "" - - Text(text = errorMessage) - } - }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), - placeholder = { Text(text = stringResource(id = R.string.username_hint)) }) - + ) Spacer(modifier = Modifier.height(16.dp)) - CustomTextField( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + PasswordTextField( value = loginUIState.password, - label = { Text(text = stringResource(id = R.string.password_hint)) }, + errorMessageRes = loginUIState.passwordErrorMessage, onValueChange = onPasswordValueChange, - isError = loginUIState.passwordErrorMessage != null, - trailingIcon = { - AnimatedVisibility(visible = loginUIState.password.isNotEmpty()) { - Image( - modifier = Modifier - .size(24.dp) - .clickable { - isPasswordVisible = !isPasswordVisible - }, - imageVector = ImageVector.vectorResource( - id = - if (isPasswordVisible) R.drawable.ic_visibility_on - else R.drawable.ic_visibility_off_24 - ), - contentDescription = "" - ) - } - - }, - supportingText = { - AnimatedVisibility(visible = loginUIState.passwordErrorMessage != null) { - val errorMessage = loginUIState.passwordErrorMessage?.let { - stringResource(id = it) - } ?: "" - - Text(text = errorMessage) - } - }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), - visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), - placeholder = { Text(text = stringResource(id = R.string.password_hint)) }) + ) Button( onClick = onLoginClicked, shape = MaterialTheme.shapes.extraLarge, - colors = ButtonDefaults.buttonColors( - containerColor = Color(0xff295EA7) - ), enabled = loginUIState.userName.isNotEmpty() && loginUIState.password.isNotEmpty(), modifier = Modifier .width(180.dp) - .padding(top = 20.dp) + .padding(top = 20.dp), ) { Text(text = stringResource(id = R.string.login)) } } } +@Composable +private fun UserNameTextField( + value: String, + errorMessageRes: Int?, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + CustomTextField( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + value = value, + label = { Text(text = stringResource(id = R.string.username_hint)) }, + onValueChange = onValueChange, + isError = errorMessageRes != null, + trailingIcon = { + AnimatedVisibility(visible = value.isNotEmpty()) { + Image( + modifier = Modifier + .size(24.dp) + .clickable { + onValueChange("") + }, + imageVector = ImageVector.vectorResource(id = R.drawable.ic_cancel), + contentDescription = "", + ) + } + }, + supportingText = { + AnimatedVisibility(visible = errorMessageRes != null) { + val errorMessage = errorMessageRes?.let { + stringResource(id = it) + } ?: "" + + Text(text = errorMessage) + } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + placeholder = { Text(text = stringResource(id = R.string.username_hint)) }, + ) +} + +@Composable +private fun PasswordTextField( + value: String, + errorMessageRes: Int?, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + var isPasswordVisible by remember { mutableStateOf(false) } + CustomTextField( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + value = value, + label = { Text(text = stringResource(id = R.string.password_hint)) }, + onValueChange = onValueChange, + isError = errorMessageRes != null, + trailingIcon = { + AnimatedVisibility(visible = value.isNotEmpty()) { + Image( + modifier = Modifier + .size(24.dp) + .clickable { + isPasswordVisible = !isPasswordVisible + }, + imageVector = ImageVector.vectorResource( + id = + if (isPasswordVisible) { + R.drawable.ic_visibility_on + } else { + R.drawable.ic_visibility_off_24 + }, + ), + contentDescription = "", + ) + } + }, + supportingText = { + AnimatedVisibility(visible = errorMessageRes != null) { + val errorMessage = errorMessageRes?.let { + stringResource(id = it) + } ?: "" + + Text(text = errorMessage) + } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), + placeholder = { Text(text = stringResource(id = R.string.password_hint)) }, + ) +} + @Preview(showBackground = true) @Composable fun LoginScreenPreview() { @@ -199,6 +234,7 @@ fun LoginScreenPreview() { LoginViewState(), onUserNameValueChange = {}, onLoginClicked = {}, - onPasswordValueChange = {}) + onPasswordValueChange = {}, + ) } } diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/feature/login/LoginViewModel.kt b/app/src/main/java/com/loodos/samplecomposeandroid/feature/login/LoginViewModel.kt index f7ba4e1..869372a 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/feature/login/LoginViewModel.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/feature/login/LoginViewModel.kt @@ -33,24 +33,23 @@ class LoginViewModel @Inject constructor( fun onLoginClick() { viewModelScope.launch { validateAuthUseCase.invoke(currentState.userName, currentState.password).onEach { result -> - when (result) { - Resource.Loading -> { - setState { copy(loading = true) } - } + when (result) { + Resource.Loading -> { + setState { copy(loading = true) } + } - is Resource.Error -> { - setState { copy(loading = false) } - updateUIErrorState(result.exception) - } + is Resource.Error -> { + setState { copy(loading = false) } + updateUIErrorState(result.exception) + } - is Resource.Success -> { - setState { copy(loading = false) } - onLogin(currentState.userName, currentState.password) - } + is Resource.Success -> { + setState { copy(loading = false) } + onLogin(currentState.userName, currentState.password) } } + } .launchIn(this) - } } @@ -124,4 +123,4 @@ data class LoginViewState( val usernameErrorMessage: Int? = null, val navigateToHome: StateEvent = consumed, val navigateToHomeWithContent: StateEventWithContent = consumed(), -) : IViewState \ No newline at end of file +) : IViewState diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/feature/login/navigation/LoginNavigation.kt b/app/src/main/java/com/loodos/samplecomposeandroid/feature/login/navigation/LoginNavigation.kt index d5358e1..27f2076 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/feature/login/navigation/LoginNavigation.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/feature/login/navigation/LoginNavigation.kt @@ -10,14 +10,14 @@ import com.loodos.samplecomposeandroid.feature.login.LoginScreenRoute * Created by mertcantoptas on 10.05.2023 */ -const val loginNavigationRoute = "login_route" +const val LoginNavigationRoute = "login_route" fun NavController.navigateLoginScreen(navOptions: NavOptions? = null) { - this.navigate(loginNavigationRoute, navOptions) + this.navigate(LoginNavigationRoute, navOptions) } fun NavGraphBuilder.loginScreen(navigateToHome: () -> Unit) { - composable(loginNavigationRoute) { + composable(LoginNavigationRoute) { LoginScreenRoute(navigateToHome = navigateToHome) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/feature/main/MainActivity.kt b/app/src/main/java/com/loodos/samplecomposeandroid/feature/main/MainActivity.kt index c50d805..96bd901 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/feature/main/MainActivity.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/feature/main/MainActivity.kt @@ -34,6 +34,7 @@ class MainActivity : ComponentActivity() { companion object { const val splashFadeDurationMillis = 1000L + const val splashIconRotation = 360f } private val viewModel: MainActivityViewModel by viewModels() @@ -66,7 +67,7 @@ class MainActivity : ComponentActivity() { // Get logo and start a fade out animation splashScreenViewProvider.iconView .animate() - .rotation(360f) + .rotation(splashIconRotation) .setDuration(splashFadeDurationMillis) .alpha(0f) .withEndAction { @@ -103,4 +104,4 @@ fun DefaultPreview() { SampleComposeAndroidTheme { Greeting("Android") } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/feature/main/MainActivityViewModel.kt b/app/src/main/java/com/loodos/samplecomposeandroid/feature/main/MainActivityViewModel.kt index b679962..4017157 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/feature/main/MainActivityViewModel.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/feature/main/MainActivityViewModel.kt @@ -4,12 +4,17 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn import javax.inject.Inject /** * Created by mertcantoptas on 07.03.2023 */ +private const val MainDelay = 1000L @HiltViewModel class MainActivityViewModel @Inject constructor() : ViewModel() { @@ -17,14 +22,14 @@ class MainActivityViewModel @Inject constructor() : ViewModel() { .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = MainActivityUiState.Loading + initialValue = MainActivityUiState.Loading, ) } private fun mainUiState(): Flow { return flow { emit(MainActivityUiState.Loading) - delay(1000) + delay(MainDelay) emit(MainActivityUiState.Success) } } diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/feature/navigation/MainNavHost.kt b/app/src/main/java/com/loodos/samplecomposeandroid/feature/navigation/MainNavHost.kt index 7498582..b6e5379 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/feature/navigation/MainNavHost.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/feature/navigation/MainNavHost.kt @@ -5,10 +5,10 @@ import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.NavOptions import androidx.navigation.compose.NavHost -import com.loodos.samplecomposeandroid.feature.home.navigation.homeNavigationRoute +import com.loodos.samplecomposeandroid.feature.home.navigation.HomeNavigationRoute import com.loodos.samplecomposeandroid.feature.home.navigation.homeScreen import com.loodos.samplecomposeandroid.feature.home.navigation.navigateHomeScreen -import com.loodos.samplecomposeandroid.feature.login.navigation.loginNavigationRoute +import com.loodos.samplecomposeandroid.feature.login.navigation.LoginNavigationRoute import com.loodos.samplecomposeandroid.feature.login.navigation.loginScreen /** @@ -19,9 +19,10 @@ import com.loodos.samplecomposeandroid.feature.login.navigation.loginScreen fun MainNavHost( navController: NavHostController, modifier: Modifier = Modifier, - startDestination: String = loginNavigationRoute + startDestination: String = LoginNavigationRoute, ) { NavHost( + modifier = modifier, navController = navController, startDestination = startDestination, ) { @@ -30,9 +31,9 @@ fun MainNavHost( navController.navigateHomeScreen( navOptions = NavOptions.Builder() - .setPopUpTo(homeNavigationRoute, true) - .build() + .setPopUpTo(HomeNavigationRoute, true) + .build(), ) }) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/ui/components/CustomTextField.kt b/app/src/main/java/com/loodos/samplecomposeandroid/ui/components/CustomTextField.kt index 3f98247..475a6df 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/ui/components/CustomTextField.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/ui/components/CustomTextField.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + package com.loodos.samplecomposeandroid.ui.components import androidx.compose.foundation.Image @@ -6,7 +8,12 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldColors +import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -20,7 +27,6 @@ import com.loodos.samplecomposeandroid.R * Created by mertcantoptas on 10.05.2023 */ -@OptIn(ExperimentalMaterial3Api::class) @Composable fun CustomTextField( value: String, @@ -63,7 +69,7 @@ private fun CustomTextFieldPreview() { onValueChange = {}, label = { Text(text = "Label") }, placeholder = { Text(text = "Placeholder") }, - trailingIcon = { Icon(Icons.Filled.Favorite, contentDescription = null) } + trailingIcon = { Icon(Icons.Filled.Favorite, contentDescription = null) }, ) } @@ -80,9 +86,9 @@ private fun CustomTextFieldFillTextPreview() { Image( modifier = Modifier.size(24.dp), imageVector = ImageVector.vectorResource(id = R.drawable.ic_cancel), - contentDescription = "" + contentDescription = "", ) }, supportingText = { Text(text = "Supporting Text") }, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/ui/components/MainAppScaffold.kt b/app/src/main/java/com/loodos/samplecomposeandroid/ui/components/MainAppScaffold.kt index 65e5446..be8879b 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/ui/components/MainAppScaffold.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/ui/components/MainAppScaffold.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.graphics.Color * Created by mertcantoptas on 10.03.2023 */ - @OptIn(ExperimentalMaterial3Api::class) @Composable fun MainAppScaffold( @@ -40,4 +39,4 @@ fun MainAppScaffold( floatingActionButton = floatingActionButton, snackbarHost = snackbarHost, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/ui/theme/Color.kt b/app/src/main/java/com/loodos/samplecomposeandroid/ui/theme/Color.kt index 7f7642d..85d4c6a 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/ui/theme/Color.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/ui/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/java/com/loodos/samplecomposeandroid/ui/theme/Theme.kt b/app/src/main/java/com/loodos/samplecomposeandroid/ui/theme/Theme.kt index c388245..b2fa5a2 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/ui/theme/Theme.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/ui/theme/Theme.kt @@ -14,17 +14,18 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat +import kotlin.IllegalStateException private val DarkColorScheme = darkColorScheme( primary = Purple80, secondary = PurpleGrey80, - tertiary = Pink80 + tertiary = Pink80, ) private val LightColorScheme = lightColorScheme( primary = Purple40, secondary = PurpleGrey40, - tertiary = Pink40 + tertiary = Pink40, /* Other default colors to override background = Color(0xFFFFFBFE), @@ -42,7 +43,7 @@ fun SampleComposeAndroidTheme( 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 -> { @@ -56,16 +57,16 @@ fun SampleComposeAndroidTheme( if (!view.isInEditMode) { SideEffect { val currentWindow = (view.context as? Activity)?.window - ?: throw Exception("Not in an activity - unable to get Window reference") + ?: throw IllegalStateException("Not in an activity - unable to get Window reference") (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(currentWindow,view).isAppearanceLightStatusBars = darkTheme + WindowCompat.getInsetsController(currentWindow, view).isAppearanceLightStatusBars = darkTheme } } MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = content, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/loodos/samplecomposeandroid/ui/theme/Type.kt b/app/src/main/java/com/loodos/samplecomposeandroid/ui/theme/Type.kt index 9bcc500..79e8797 100644 --- a/app/src/main/java/com/loodos/samplecomposeandroid/ui/theme/Type.kt +++ b/app/src/main/java/com/loodos/samplecomposeandroid/ui/theme/Type.kt @@ -13,8 +13,8 @@ val Typography = Typography( fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) + letterSpacing = 0.5.sp, + ), /* Other default text styles to override titleLarge = TextStyle( fontFamily = FontFamily.Default, @@ -31,4 +31,4 @@ val Typography = Typography( letterSpacing = 0.5.sp ) */ -) \ No newline at end of file +) diff --git a/app/src/test/java/com/loodos/samplecomposeandroid/ExampleUnitTest.kt b/app/src/test/java/com/loodos/samplecomposeandroid/ExampleUnitTest.kt index c436683..ae860dd 100644 --- a/app/src/test/java/com/loodos/samplecomposeandroid/ExampleUnitTest.kt +++ b/app/src/test/java/com/loodos/samplecomposeandroid/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package com.loodos.samplecomposeandroid +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/build.gradle.kts b/build.gradle.kts index c21d8a6..c56e0f6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,58 @@ -buildscript { -} // Top-level build file where you can add configuration options common to all sub-projects/modules. +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") plugins { + alias(libs.plugins.detekt) + alias(libs.plugins.kotlinter) apply false alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.dagger.hilt) apply false +} + +apply(from = "buildscripts/githooks.gradle") + +tasks.register("clean", Delete::class) { + delete(rootProject.buildDir) +} + +afterEvaluate { + tasks.named("clean") { + dependsOn(":installGitHooks") + } +} + +// Detekt Config +apply(plugin = "io.gitlab.arturbosch.detekt") + +detekt { + config = files("${rootProject.projectDir}/config/detekt/detekt.yml") + + reports { + html.required.set(true) + xml.required.set(true) + txt.required.set(true) + } +} + +tasks { + /** + * The detektAll tasks enables parallel usage for detekt so if this project + * expands to multi module support, detekt can continue to run quickly. + * + * https://proandroiddev.com/how-to-use-detekt-in-a-multi-module-android-project-6781937fbef2 + */ + @Suppress("UnusedPrivateMember") + val detektAll by registering(io.gitlab.arturbosch.detekt.Detekt::class) { + parallel = true + setSource(files(projectDir)) + include("**/*.kt") + exclude("**/*.kts") + exclude("**/resources/**") + exclude("**/build/**") + exclude("**/.idea/**") + config.setFrom(files("$rootDir/config/detekt/detekt.yml")) + buildUponDefaultConfig = false + } } \ No newline at end of file diff --git a/buildSrc/src/main/java/com/loodos/buildsrc/Deps.kt b/buildSrc/src/main/java/com/loodos/buildsrc/Deps.kt deleted file mode 100644 index c2d63b5..0000000 --- a/buildSrc/src/main/java/com/loodos/buildsrc/Deps.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.loodos.buildsrc - -class Deps { -} \ No newline at end of file diff --git a/buildSrc/src/main/java/com/loodos/buildsrc/GradleUtils.kt b/buildSrc/src/main/java/com/loodos/buildsrc/GradleUtils.kt index f42ae6a..53e2c9d 100644 --- a/buildSrc/src/main/java/com/loodos/buildsrc/GradleUtils.kt +++ b/buildSrc/src/main/java/com/loodos/buildsrc/GradleUtils.kt @@ -5,4 +5,4 @@ import java.util.* fun getDateTime(): Long { val date = Date() return date.time -} \ No newline at end of file +} diff --git a/buildscripts/githooks.gradle b/buildscripts/githooks.gradle new file mode 100644 index 0000000..396ff31 --- /dev/null +++ b/buildscripts/githooks.gradle @@ -0,0 +1,29 @@ +// https://blog.sebastiano.dev/ooga-chaka-git-hooks-to-enforce-code-quality/ +static def isLinuxOrMacOs() { + def osName = System.getProperty('os.name').toLowerCase(Locale.ROOT) + return osName.contains('linux') || osName.contains('mac os') || osName.contains('macos') +} + +task copyGitHooks(type: Copy) { + description 'Copies the git hooks from /git-hooks to the .git folder.' + group 'git hooks' + from("${rootDir}/git-hooks/") { + include '**/*.sh' + rename '(.*).sh', '$1' + } + into "${rootDir}/.git/hooks" + onlyIf { isLinuxOrMacOs() } +} + +task installGitHooks(type: Exec) { + description 'Installs the pre-commit git hooks from /git-hooks.' + group 'git hooks' + workingDir rootDir + commandLine 'chmod' + args '-R', '+x', '.git/hooks/' + dependsOn copyGitHooks + onlyIf { isLinuxOrMacOs() } + doLast { + logger.info('Git hook installed successfully.') + } +} \ No newline at end of file diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml new file mode 100644 index 0000000..25b8f82 --- /dev/null +++ b/config/detekt/detekt.yml @@ -0,0 +1,613 @@ +build: + maxIssues: 0 + excludeCorrectable: false + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: 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' + +output-reports: + active: true + exclude: + # - 'TxtOutputReport' + # - 'XmlOutputReport' + # - 'HtmlOutputReport' + +comments: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + 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<])|([.?!:]$)' + UndocumentedPublicClass: + active: false + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + UndocumentedPublicFunction: + active: false + UndocumentedPublicProperty: + active: false + +complexity: + active: true + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: ['run', 'let', 'apply', 'with', 'also', 'use', 'forEach', 'isNotNull', 'ifNull'] + LabeledExpression: + active: false + ignoredLabels: [] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 60 + LongParameterList: + active: true + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotated: ['Composable'] + MethodOverloading: + active: false + threshold: 6 + NamedArguments: + active: false + threshold: 3 + NestedBlockDepth: + active: true + threshold: 4 + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + +coroutines: + active: true + GlobalCoroutineUsage: + active: false + RedundantSuspendModifier: + active: false + SleepInsteadOfDelay: + active: false + SuspendFunWithFlowReturnType: + active: false + +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: [toString, hashCode, equals, finalize] + InstanceOfCheckForException: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: false + ObjectExtendsThrowable: + active: false + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - InterruptedException + - NumberFormatException + - ParseException + - MalformedURLException + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - IllegalArgumentException + - IllegalStateException + - IOException + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - ArrayIndexOutOfBoundsException + - Error + - Exception + - IllegalMonitorStateException + - NullPointerException + - IndexOutOfBoundsException + - RuntimeException + - Throwable + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - Error + - Exception + - Throwable + - RuntimeException + +naming: + active: true + ClassNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverridden: true + EnumNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + forbiddenName: [] + FunctionMaxLength: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)' + excludeClassPattern: '$^' + ignoreOverridden: true + ignoreAnnotated: ['Composable'] + FunctionParameterNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverridden: true + InvalidPackageDeclaration: + active: false + excludes: ['*.kts'] + rootPackage: '' + MatchingDeclarationName: + active: true + mustBeFirst: true + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: false + NonBooleanPropertyPrefixedWithIs: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + ObjectPropertyNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + 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 + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + constantPattern: '[A-Z][A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + maximumVariableNameLength: 64 + VariableMinLength: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + minimumVariableNameLength: 1 + VariableNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverridden: true + +performance: + active: true + ArrayPrimitive: + active: true + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + CastToNullableType: + active: false + Deprecation: + active: false + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: false + DuplicateCaseInWhenExpression: + active: true + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: false + IgnoredReturnValue: + active: false + restrictToAnnotatedMethods: true + returnValueAnnotations: ['*.CheckReturnValue', '*.CheckResult'] + 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/**', '**/jsTest/**', '**/iosTest/**'] + excludeAnnotatedProperties: [] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: false + MissingWhenCase: + active: true + allowElseExpression: true + NullableToStringCall: + active: false + RedundantElseInWhen: + active: true + UnconditionalJumpStatementInLoop: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: false + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + UnsafeCast: + active: true + UnusedUnaryOperator: + active: false + UselessPostfixExpression: + active: false + WrongEqualsTypeParameter: + active: true + +style: + active: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: 'to' + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: false + maxDestructuringEntries: 3 + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaParameter: + active: false + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenComment: + active: false + values: ['TODO:', 'FIXME:', 'STOPSHIP:'] + allowedPatterns: '' + ForbiddenImport: + active: false + imports: [] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: false + methods: ['kotlin.io.println', 'kotlin.io.print'] + ForbiddenPublicDataClass: + active: true + excludes: ['**'] + ignorePackages: ['*.internal', '*.internal.*'] + ForbiddenVoid: + active: false + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: 'describeContents' + excludeAnnotatedFunction: ['dagger.Provides'] + LibraryCodeMustSpecifyReturnType: + active: true + excludes: ['**'] + LibraryEntitiesShouldNotBePublic: + active: true + excludes: ['**'] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + 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 + ignoreAnnotated: ['Preview'] + MandatoryBracesIfStatements: + active: false + MandatoryBracesLoops: + active: false + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + ObjectLiteralToLambda: + active: false + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + OptionalWhenBraces: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: false + 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 + ThrowsCount: + active: true + max: 5 + TrailingWhitespace: + active: false + UnderscoresInNumericLiterals: + active: false + acceptableDecimalLength: 5 + UnnecessaryAbstractClass: + active: true + excludeAnnotatedClasses: ['dagger.Module'] + UnnecessaryAnnotationUseSiteTarget: + active: false + UnnecessaryApply: + active: true + UnnecessaryFilter: + active: false + UnnecessaryInheritance: + active: true + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '(_|ignored|expected|serialVersionUID)' + ignoreAnnotated: ['Preview'] + UseArrayLiteralsInAnnotations: + active: false + UseCheckNotNull: + active: false + UseCheckOrError: + active: false + UseDataClass: + active: false + excludeAnnotatedClasses: [] + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + UseIsNullOrEmpty: + active: false + UseOrEmpty: + active: false + UseRequire: + active: false + UseRequireNotNull: + active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + WildcardImport: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] + excludeImports: ['java.util.*', 'kotlinx.android.synthetic.*'] diff --git a/git-hooks/pre-commit.sh b/git-hooks/pre-commit.sh new file mode 100644 index 0000000..093d89e --- /dev/null +++ b/git-hooks/pre-commit.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +######## KTLINT-GRADLE HOOK START ######## + +CHANGED_FILES="$(git --no-pager diff --name-status --no-color --cached | awk '$1 != "D" && $2 ~ /\.kts|\.kt/ { print $2}')" + +if [ -z "$CHANGED_FILES" ]; then + echo "No Kotlin staged files." + exit 0 +fi; + +echo "Running ktlint over these files:" +echo "$CHANGED_FILES" + +./gradlew --quiet formatKotlin -PinternalKtlintGitFilter="$CHANGED_FILES" + +echo "Completed ktlint run." + +echo "$CHANGED_FILES" | while read -r file; do + if [ -f $file ]; then + git add $file + fi +done + +echo "Completed ktlint hook." +######## KTLINT-GRADLE HOOK END ######## \ No newline at end of file diff --git a/git-hooks/pre-push.sh b/git-hooks/pre-push.sh new file mode 100644 index 0000000..a8c1c98 --- /dev/null +++ b/git-hooks/pre-push.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +echo "Running static analysis." + +./gradlew lintKotlin +./gradlew detektAll \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index efca172..edc1c62 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,8 @@ androidxComposeBom = "2023.04.00" chucker = "3.5.2" hilt = "2.45" hiltPlugin = "2.44.2" +detektGradlePlugin = "1.22.0" +kotlinterGradlePlugin = "3.14.0" hiltNavigationCompose = "1.0.0" composeLintChecks = "1.0.1" composeStateEvents = "1.2.3" @@ -56,7 +58,7 @@ okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhtt okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } compose-state-events = { group = "com.github.leonard-palm", name = "compose-state-events", version.ref = "composeStateEvents" } turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" } -truth = { group = "com.google.truth", name = "truth", version.ref= "truth" } +truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } truth-ext = { group = "androidx.test.ext", name = "truth", version.ref = "truthExt" } junit4 = { group = "junit", name = "junit", version.ref = "junit4" } jetbrains-kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlinTest" } @@ -67,4 +69,6 @@ android-application = { id = "com.android.application", version.ref = "androidGr android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "hiltPlugin" } \ No newline at end of file +dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "hiltPlugin" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detektGradlePlugin" } +kotlinter = { id = "org.jmailen.kotlinter", version.ref = "kotlinterGradlePlugin" } \ No newline at end of file