diff --git a/app/build.gradle b/app/build.gradle index 9b278d074..3fed1c295 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,7 @@ plugins { id 'org.jetbrains.kotlin.android' id 'com.google.dagger.hilt.android' id 'org.jlleitschuh.gradle.ktlint' version '11.2.0' + id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.10' } android { @@ -133,15 +134,26 @@ dependencies { implementation "io.ktor:ktor-client-core:$ktor_version" implementation "io.ktor:ktor-client-okhttp:$ktor_version" implementation "io.ktor:ktor-client-content-negotiation:$ktor_version" - implementation "io.ktor:ktor-serialization-gson:$ktor_version" + implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version" testImplementation("io.ktor:ktor-client-mock:$ktor_version") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1") + testImplementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1") + + //retrofit & okhttp implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0' - //gson implementation 'com.google.code.gson:gson:2.10.1' + implementation "io.ktor:ktor-serialization-gson:2.3.4" + + // koin + implementation "io.insert-koin:koin-androidx-compose:$koin_version" + implementation "io.insert-koin:koin-android:$koin_version" + implementation "io.insert-koin:koin-core:$koin_version" + testImplementation "io.insert-koin:koin-test:$koin_version" testImplementation project(":app-shared-tests") androidTestImplementation(project(":app-shared-tests")) { diff --git a/app/src/main/java/com/appunite/loudius/LoudiusApplication.kt b/app/src/main/java/com/appunite/loudius/LoudiusApplication.kt index bf5769404..18dddf734 100644 --- a/app/src/main/java/com/appunite/loudius/LoudiusApplication.kt +++ b/app/src/main/java/com/appunite/loudius/LoudiusApplication.kt @@ -17,7 +17,32 @@ package com.appunite.loudius import android.app.Application -import dagger.hilt.android.HiltAndroidApp +import com.appunite.loudius.di.dataSourceModule +import com.appunite.loudius.di.dispatcherModule +import com.appunite.loudius.di.githubHelperModule +import com.appunite.loudius.di.networkModule +import com.appunite.loudius.di.repositoryModule +import com.appunite.loudius.di.serviceModule +import com.appunite.loudius.di.viewModelModule +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.GlobalContext.startKoin -@HiltAndroidApp -class LoudiusApplication : Application() +class LoudiusApplication : Application() { + override fun onCreate() { + super.onCreate() + + startKoin { + androidContext(this@LoudiusApplication) + + modules( + dataSourceModule, + dispatcherModule, + githubHelperModule, + networkModule, + repositoryModule, + serviceModule, + viewModelModule, + ) + } + } +} diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 72b7abed2..2ae112fd6 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -20,7 +20,6 @@ import android.os.Bundle import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.viewModels import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -37,12 +36,11 @@ import com.appunite.loudius.ui.authenticating.AuthenticatingScreen import com.appunite.loudius.ui.login.LoginScreen import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.ui.reviewers.ReviewersScreen -import dagger.hilt.android.AndroidEntryPoint +import org.koin.androidx.viewmodel.ext.android.viewModel -@AndroidEntryPoint class MainActivity : ComponentActivity() { - private val viewModel: MainViewModel by viewModels() + private val viewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { diff --git a/app/src/main/java/com/appunite/loudius/common/Screen.kt b/app/src/main/java/com/appunite/loudius/common/Screen.kt index f29b7d1cf..bc75e83f9 100644 --- a/app/src/main/java/com/appunite/loudius/common/Screen.kt +++ b/app/src/main/java/com/appunite/loudius/common/Screen.kt @@ -24,7 +24,8 @@ import androidx.navigation.NavDeepLink import androidx.navigation.NavType import androidx.navigation.navArgument import androidx.navigation.navDeepLink -import java.time.LocalDateTime +import kotlinx.datetime.Instant +import kotlinx.datetime.toInstant sealed class Screen(val route: String) { open val arguments: List = emptyList() @@ -76,16 +77,14 @@ sealed class Screen(val route: String) { owner = checkNotNull(savedStateHandle[ownerArg]), repo = checkNotNull(savedStateHandle[repoArg]), pullRequestNumber = checkNotNull(savedStateHandle[pullRequestNumberArg]), - submissionTime = checkNotNull( - LocalDateTime.parse(savedStateHandle[submissionDateArg]), - ), + submissionTime = checkNotNull((savedStateHandle[submissionDateArg] ?: "").toInstant()), ) data class ReviewersInitialValues( val owner: String, val repo: String, val pullRequestNumber: String, - val submissionTime: LocalDateTime, + val submissionTime: Instant, ) } } diff --git a/app/src/main/java/com/appunite/loudius/di/APIQualifiers.kt b/app/src/main/java/com/appunite/loudius/di/APIQualifiers.kt deleted file mode 100644 index a67b01536..000000000 --- a/app/src/main/java/com/appunite/loudius/di/APIQualifiers.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2023 AppUnite S.A. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.appunite.loudius.di - -import javax.inject.Qualifier - -@Qualifier -@Retention(AnnotationRetention.RUNTIME) -annotation class BaseAPI - -@Qualifier -@Retention(AnnotationRetention.RUNTIME) -annotation class AuthAPI diff --git a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt index 55e4f192d..cc1dd6f73 100644 --- a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt @@ -16,7 +16,6 @@ package com.appunite.loudius.di -import android.content.Context import com.appunite.loudius.domain.store.UserLocalDataSource import com.appunite.loudius.domain.store.UserLocalDataSourceImpl import com.appunite.loudius.network.datasource.AuthDataSource @@ -25,40 +24,13 @@ import com.appunite.loudius.network.datasource.PullRequestDataSource import com.appunite.loudius.network.datasource.PullRequestsDataSourceImpl import com.appunite.loudius.network.datasource.UserDataSource import com.appunite.loudius.network.datasource.UserDataSourceImpl -import com.appunite.loudius.network.services.AuthService -import com.appunite.loudius.network.services.PullRequestsService -import com.appunite.loudius.network.services.UserService -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module -@InstallIn(SingletonComponent::class) -@Module -object DataSourceModule { - - @Provides - @Singleton - fun providePullRequestNetworkDataSource( - service: PullRequestsService, - ): PullRequestDataSource = PullRequestsDataSourceImpl(service) - - @Provides - @Singleton - fun provideUserDataSource( - userService: UserService, - ): UserDataSource = UserDataSourceImpl(userService) - - @Singleton - @Provides - fun provideUserLocalDataSource(@ApplicationContext context: Context): UserLocalDataSource = - UserLocalDataSourceImpl(context) - - @Singleton - @Provides - fun provideAuthDataSource( - service: AuthService, - ): AuthDataSource = AuthDataSourceImpl(service) +val dataSourceModule = module { + singleOf(::UserDataSourceImpl) { bind() } + singleOf(::UserLocalDataSourceImpl) { bind() } + singleOf(::AuthDataSourceImpl) { bind() } + singleOf(::PullRequestsDataSourceImpl) { bind() } } diff --git a/app/src/main/java/com/appunite/loudius/di/DispatchersModule.kt b/app/src/main/java/com/appunite/loudius/di/DispatchersModule.kt index 36afd4649..c3fc08f4f 100644 --- a/app/src/main/java/com/appunite/loudius/di/DispatchersModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/DispatchersModule.kt @@ -16,17 +16,10 @@ package com.appunite.loudius.di -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import org.koin.dsl.module -@InstallIn(SingletonComponent::class) -@Module -object DispatchersModule { - - @Provides - fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default +val dispatcherModule = module { + factory { Dispatchers.Default } } diff --git a/app/src/main/java/com/appunite/loudius/di/GithubHelperModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubHelperModule.kt index 1b9df1529..9170eb54f 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubHelperModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubHelperModule.kt @@ -16,20 +16,10 @@ package com.appunite.loudius.di -import android.content.Context import com.appunite.loudius.ui.login.GithubHelper -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module -@InstallIn(SingletonComponent::class) -@Module -object GithubHelperModule { - @Provides - @Singleton - fun providePullRequestNetworkDataSource(@ApplicationContext context: Context): GithubHelper = - GithubHelper(context) +val githubHelperModule = module { + singleOf(::GithubHelper) } diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index 2f94519ea..e2562ff35 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -20,91 +20,63 @@ import com.appunite.loudius.common.Constants import com.appunite.loudius.network.intercept.AuthFailureInterceptor import com.appunite.loudius.network.intercept.AuthInterceptor import com.appunite.loudius.network.utils.AuthFailureHandler -import com.appunite.loudius.network.utils.LocalDateTimeDeserializer -import com.google.gson.FieldNamingPolicy -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest -import io.ktor.http.ContentType -import io.ktor.serialization.gson.GsonConverter +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json import okhttp3.logging.HttpLoggingInterceptor -import java.time.LocalDateTime -import javax.inject.Singleton +import org.koin.core.module.dsl.singleOf +import org.koin.core.qualifier.named +import org.koin.dsl.module -@InstallIn(SingletonComponent::class) -@Module -object NetworkModule { - @Provides - @Singleton - fun provideLoggingInterceptor(): HttpLoggingInterceptor = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BASIC +val networkModule = module { + single { + HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BASIC + } } - @Provides - @AuthAPI - fun provideBaseAuthUrl() = Constants.AUTH_API_URL - - @Provides - @BaseAPI - fun provideBaseAPIUrl() = Constants.BASE_API_URL - - @Provides - @Singleton - @AuthAPI - fun provideAuthHttpClient( - gson: Gson, - @AuthAPI baseUrl: String, - loggingInterceptor: HttpLoggingInterceptor, - ): HttpClient = HttpClient(OkHttp) { - expectSuccess = true - engine { - addInterceptor(TestInterceptor) - addInterceptor(loggingInterceptor) - } - defaultRequest { - url(baseUrl) - } - install(ContentNegotiation) { - register(ContentType.Application.Json, GsonConverter(gson)) + single(named("auth")) { + HttpClient(OkHttp) { + expectSuccess = true + engine { + addInterceptor(TestInterceptor) + addInterceptor(get()) + } + defaultRequest { + url(Constants.AUTH_API_URL) + } + install(ContentNegotiation) { + json(get()) + } } } - @Provides - @Singleton - @BaseAPI - fun provideBaseHttpClient( - gson: Gson, - @BaseAPI baseUrl: String, - loggingInterceptor: HttpLoggingInterceptor, - authInterceptor: AuthInterceptor, - authFailureHandler: AuthFailureHandler, - ): HttpClient = HttpClient(OkHttp) { - expectSuccess = true - engine { - addInterceptor(authInterceptor) - addInterceptor(TestInterceptor) - addInterceptor(AuthFailureInterceptor(authFailureHandler)) - addInterceptor(loggingInterceptor) - } - defaultRequest { - url(baseUrl) + single(named("base")) { + HttpClient(OkHttp) { + expectSuccess = true + engine { + addInterceptor(TestInterceptor) + addInterceptor(get()) + addInterceptor(AuthFailureInterceptor(get())) + addInterceptor(AuthInterceptor(get())) + } + defaultRequest { + url(Constants.BASE_API_URL) + } + install(ContentNegotiation) { + json(get()) + } } - install(ContentNegotiation) { - register(ContentType.Application.Json, GsonConverter(gson)) + } + + single { + Json { + ignoreUnknownKeys = true } } - @Provides - @Singleton - fun provideGson(): Gson = - GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) - .registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeDeserializer()) - .create() + singleOf(::AuthFailureHandler) } diff --git a/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt b/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt index 47b489fd5..045b1d5cb 100644 --- a/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt @@ -20,31 +20,11 @@ import com.appunite.loudius.domain.repository.AuthRepository import com.appunite.loudius.domain.repository.AuthRepositoryImpl import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.domain.repository.PullRequestRepositoryImpl -import com.appunite.loudius.domain.store.UserLocalDataSource -import com.appunite.loudius.network.datasource.AuthDataSource -import com.appunite.loudius.network.datasource.PullRequestDataSource -import com.appunite.loudius.network.datasource.UserDataSource -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module -@InstallIn(SingletonComponent::class) -@Module -object RepositoryModule { - - @Provides - @Singleton - fun providePullRequestRepository( - dataSource: PullRequestDataSource, - userDataSource: UserDataSource, - ): PullRequestRepository = PullRequestRepositoryImpl(dataSource, userDataSource) - - @Singleton - @Provides - fun provideAuthRepository( - authDataSource: AuthDataSource, - userLocalDataSource: UserLocalDataSource, - ): AuthRepository = AuthRepositoryImpl(authDataSource, userLocalDataSource) +val repositoryModule = module { + singleOf(::PullRequestRepositoryImpl) { bind() } + singleOf(::AuthRepositoryImpl) { bind() } } diff --git a/app/src/main/java/com/appunite/loudius/di/ServiceModule.kt b/app/src/main/java/com/appunite/loudius/di/ServiceModule.kt index d0326cb50..a5b34aaee 100644 --- a/app/src/main/java/com/appunite/loudius/di/ServiceModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/ServiceModule.kt @@ -22,29 +22,11 @@ import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.services.PullRequestsServiceImpl import com.appunite.loudius.network.services.UserService import com.appunite.loudius.network.services.UserServiceImpl -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import io.ktor.client.HttpClient -import javax.inject.Singleton +import org.koin.core.qualifier.named +import org.koin.dsl.module -@InstallIn(SingletonComponent::class) -@Module -object ServiceModule { - - @Singleton - @Provides - fun provideAuthService(@AuthAPI httpClient: HttpClient): AuthService = - AuthServiceImpl(httpClient) - - @Singleton - @Provides - fun provideUserService(@BaseAPI httpClient: HttpClient): UserService = - UserServiceImpl(httpClient) - - @Singleton - @Provides - fun provideReposService(@BaseAPI httpClient: HttpClient): PullRequestsService = - PullRequestsServiceImpl(httpClient) +val serviceModule = module { + single { AuthServiceImpl(get(named("auth"))) } + single { UserServiceImpl(get(named("base"))) } + single { PullRequestsServiceImpl(get(named("base"))) } } diff --git a/app/src/main/java/com/appunite/loudius/di/ViewModelModule.kt b/app/src/main/java/com/appunite/loudius/di/ViewModelModule.kt new file mode 100644 index 000000000..036cfa13b --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/di/ViewModelModule.kt @@ -0,0 +1,17 @@ +package com.appunite.loudius.di + +import com.appunite.loudius.ui.MainViewModel +import com.appunite.loudius.ui.authenticating.AuthenticatingViewModel +import com.appunite.loudius.ui.login.LoginScreenViewModel +import com.appunite.loudius.ui.pullrequests.PullRequestsViewModel +import com.appunite.loudius.ui.reviewers.ReviewersViewModel +import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.dsl.module + +val viewModelModule = module { + viewModelOf(::AuthenticatingViewModel) + viewModelOf(::LoginScreenViewModel) + viewModelOf(::PullRequestsViewModel) + viewModelOf(::ReviewersViewModel) + viewModelOf(::MainViewModel) +} diff --git a/app/src/main/java/com/appunite/loudius/domain/repository/AuthRepository.kt b/app/src/main/java/com/appunite/loudius/domain/repository/AuthRepository.kt index afe36052e..ac63c337b 100644 --- a/app/src/main/java/com/appunite/loudius/domain/repository/AuthRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/repository/AuthRepository.kt @@ -19,8 +19,6 @@ package com.appunite.loudius.domain.repository import com.appunite.loudius.domain.store.UserLocalDataSource import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.model.AccessToken -import javax.inject.Inject -import javax.inject.Singleton interface AuthRepository { @@ -33,8 +31,7 @@ interface AuthRepository { fun getAccessToken(): AccessToken } -@Singleton -class AuthRepositoryImpl @Inject constructor( +class AuthRepositoryImpl( private val authDataSource: AuthDataSource, private val userLocalDataSource: UserLocalDataSource, ) : AuthRepository { diff --git a/app/src/main/java/com/appunite/loudius/domain/repository/PullRequestRepository.kt b/app/src/main/java/com/appunite/loudius/domain/repository/PullRequestRepository.kt index a51d3611f..2bb445fdb 100644 --- a/app/src/main/java/com/appunite/loudius/domain/repository/PullRequestRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/repository/PullRequestRepository.kt @@ -25,7 +25,6 @@ import com.appunite.loudius.network.model.Review import com.appunite.loudius.network.model.User import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope -import javax.inject.Inject interface PullRequestRepository { suspend fun getReviews( @@ -50,7 +49,7 @@ interface PullRequestRepository { ): Result } -class PullRequestRepositoryImpl @Inject constructor( +class PullRequestRepositoryImpl( private val pullRequestsDataSource: PullRequestDataSource, private val userDataSource: UserDataSource, ) : PullRequestRepository { diff --git a/app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSourceImpl.kt b/app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSourceImpl.kt index e47de8910..e81bb002c 100644 --- a/app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSourceImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSourceImpl.kt @@ -19,18 +19,13 @@ package com.appunite.loudius.domain.store import android.content.Context import android.content.SharedPreferences import com.appunite.loudius.network.model.AccessToken -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject -import javax.inject.Singleton interface UserLocalDataSource { fun saveAccessToken(accessToken: AccessToken) fun getAccessToken(): AccessToken } -@Singleton -class UserLocalDataSourceImpl @Inject constructor(@ApplicationContext context: Context) : - UserLocalDataSource { +class UserLocalDataSourceImpl(context: Context) : UserLocalDataSource { companion object { private const val FILE_NAME = "com.appunite.loudius.sharedPreferences" diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt index c3d8f6430..4d3fa450d 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt @@ -21,8 +21,6 @@ import com.appunite.loudius.network.model.AccessToken import com.appunite.loudius.network.model.AccessTokenResponse import com.appunite.loudius.network.services.AuthService import com.appunite.loudius.network.utils.WebException -import javax.inject.Inject -import javax.inject.Singleton interface AuthDataSource { @@ -33,8 +31,7 @@ interface AuthDataSource { ): Result } -@Singleton -class AuthDataSourceImpl @Inject constructor( +class AuthDataSourceImpl( private val authService: AuthService, ) : AuthDataSource { diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsDataSourceImpl.kt b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsDataSourceImpl.kt index de744af47..48fa66fd4 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsDataSourceImpl.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsDataSourceImpl.kt @@ -21,8 +21,6 @@ import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import com.appunite.loudius.network.model.request.NotifyRequestBody import com.appunite.loudius.network.services.PullRequestsService -import javax.inject.Inject -import javax.inject.Singleton interface PullRequestDataSource { suspend fun getReviewers( @@ -47,8 +45,7 @@ interface PullRequestDataSource { ): Result } -@Singleton -class PullRequestsDataSourceImpl @Inject constructor( +class PullRequestsDataSourceImpl( private val service: PullRequestsService, ) : PullRequestDataSource { diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt index 5c947aa92..4f860a95a 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt @@ -18,15 +18,12 @@ package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.User import com.appunite.loudius.network.services.UserService -import javax.inject.Inject -import javax.inject.Singleton interface UserDataSource { suspend fun getUser(): Result } -@Singleton -class UserDataSourceImpl @Inject constructor( +class UserDataSourceImpl( private val userService: UserService, ) : UserDataSource { override suspend fun getUser(): Result = userService.getUser() diff --git a/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt index 4269a6b28..5548b6d35 100644 --- a/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt +++ b/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt @@ -19,9 +19,8 @@ package com.appunite.loudius.network.intercept import com.appunite.loudius.network.utils.AuthFailureHandler import okhttp3.Interceptor import okhttp3.Response -import javax.inject.Inject -class AuthFailureInterceptor @Inject constructor( +class AuthFailureInterceptor( private val authFailureHandler: AuthFailureHandler, ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { diff --git a/app/src/main/java/com/appunite/loudius/network/intercept/AuthInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/intercept/AuthInterceptor.kt index b2aa76a1e..0363507bb 100644 --- a/app/src/main/java/com/appunite/loudius/network/intercept/AuthInterceptor.kt +++ b/app/src/main/java/com/appunite/loudius/network/intercept/AuthInterceptor.kt @@ -19,9 +19,8 @@ package com.appunite.loudius.network.intercept import com.appunite.loudius.domain.repository.AuthRepository import okhttp3.Interceptor import okhttp3.Response -import javax.inject.Inject -class AuthInterceptor @Inject constructor( +class AuthInterceptor( private val authRepository: AuthRepository, ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { diff --git a/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt index cb55b63a9..5af8b0f44 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt @@ -16,9 +16,14 @@ package com.appunite.loudius.network.model +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + typealias AccessToken = String +@Serializable data class AccessTokenResponse( - val accessToken: AccessToken?, + @SerialName("access_token") + val accessToken: AccessToken? = null, val error: String? = null, ) diff --git a/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt b/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt index 0be70ac02..14a87b566 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt @@ -17,15 +17,22 @@ package com.appunite.loudius.network.model import com.appunite.loudius.common.Constants -import java.time.LocalDateTime +import com.appunite.loudius.network.utils.InstantSerializer +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +@Serializable data class PullRequest( val id: Int, val draft: Boolean, val number: Int, + @SerialName("repository_url") val repositoryUrl: String, val title: String, - val createdAt: LocalDateTime, + @SerialName("created_at") + @Serializable(with = InstantSerializer::class) + val createdAt: Instant, ) { val fullRepositoryName: String get() = repositoryUrl.removePrefix(REPOSITORY_PATH) diff --git a/app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt index 1670a8a72..d065e4a9f 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt @@ -16,8 +16,14 @@ package com.appunite.loudius.network.model +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable data class PullRequestsResponse( + @SerialName("incomplete_results") val incompleteResults: Boolean, val items: List, + @SerialName("total_count") val totalCount: Int, ) diff --git a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt index b05c0985e..8a8150906 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt @@ -16,6 +16,9 @@ package com.appunite.loudius.network.model +import kotlinx.serialization.Serializable + +@Serializable data class RequestedReviewer( val id: Int, val login: String, diff --git a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt index f88a6da98..71dd9ef9d 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt @@ -16,6 +16,9 @@ package com.appunite.loudius.network.model +import kotlinx.serialization.Serializable + +@Serializable data class RequestedReviewersResponse( val users: List, ) diff --git a/app/src/main/java/com/appunite/loudius/network/model/Review.kt b/app/src/main/java/com/appunite/loudius/network/model/Review.kt index 17b2c5ec1..8e0363160 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/Review.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/Review.kt @@ -16,11 +16,35 @@ package com.appunite.loudius.network.model -import java.time.LocalDateTime +import com.appunite.loudius.network.utils.InstantSerializer +import kotlinx.datetime.Instant +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +@Serializable data class Review( + @Serializable(with = IdSerializer::class) val id: String, val user: User, val state: ReviewState, - val submittedAt: LocalDateTime, + @SerialName("submitted_at") + @Serializable(with = InstantSerializer::class) + val submittedAt: Instant, ) + +object IdSerializer : KSerializer { + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("id", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): String { + return decoder.decodeInt().toString() + } + + override fun serialize(encoder: Encoder, value: String) { } +} diff --git a/app/src/main/java/com/appunite/loudius/network/model/User.kt b/app/src/main/java/com/appunite/loudius/network/model/User.kt index dbc12a21d..b386feb27 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/User.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/User.kt @@ -16,4 +16,7 @@ package com.appunite.loudius.network.model +import kotlinx.serialization.Serializable + +@Serializable data class User(val id: Int, val login: String) diff --git a/app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt b/app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt index d7544f817..db009683a 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt @@ -16,6 +16,9 @@ package com.appunite.loudius.network.model.request +import kotlinx.serialization.Serializable + +@Serializable data class NotifyRequestBody( val body: String, ) diff --git a/app/src/main/java/com/appunite/loudius/network/services/AuthService.kt b/app/src/main/java/com/appunite/loudius/network/services/AuthService.kt index 4d26a091e..7ef8fc432 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/AuthService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/AuthService.kt @@ -24,7 +24,6 @@ import io.ktor.client.request.headers import io.ktor.http.ContentType import io.ktor.http.HttpHeaders import io.ktor.http.parameters -import javax.inject.Inject interface AuthService { @@ -35,7 +34,7 @@ interface AuthService { ): Result } -class AuthServiceImpl @Inject constructor(private val client: HttpClient) : AuthService { +class AuthServiceImpl(private val client: HttpClient) : AuthService { override suspend fun getAccessToken( clientId: String, diff --git a/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt b/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt index 9d74b98e5..af663b63d 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt @@ -29,7 +29,6 @@ import io.ktor.client.request.setBody import io.ktor.http.ContentType import io.ktor.http.contentType import io.ktor.http.encodeURLParameter -import javax.inject.Inject interface PullRequestsService { @@ -59,8 +58,9 @@ interface PullRequestsService { ): Result } -class PullRequestsServiceImpl @Inject constructor(private val client: HttpClient) : - PullRequestsService { +class PullRequestsServiceImpl( + private val client: HttpClient, +) : PullRequestsService { override suspend fun getPullRequestsForUser( query: String, page: Int, diff --git a/app/src/main/java/com/appunite/loudius/network/services/UserService.kt b/app/src/main/java/com/appunite/loudius/network/services/UserService.kt index bb9e1d595..5f9e4b5ec 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/UserService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/UserService.kt @@ -23,14 +23,13 @@ import io.ktor.client.request.get import io.ktor.client.request.headers import io.ktor.http.ContentType import io.ktor.http.HttpHeaders -import javax.inject.Inject interface UserService { suspend fun getUser(): Result } -class UserServiceImpl @Inject constructor(private val client: HttpClient) : UserService { +class UserServiceImpl(private val client: HttpClient) : UserService { override suspend fun getUser(): Result = runCatching { client.get("user") { diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt new file mode 100644 index 000000000..e69de29bb diff --git a/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt index dc0be7fc7..7cc5021bd 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt @@ -21,11 +21,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class AuthFailureHandler @Inject constructor( +class AuthFailureHandler( private val dispatcher: CoroutineDispatcher, ) { private val _authFailureFlow = MutableSharedFlow() diff --git a/app/src/main/java/com/appunite/loudius/network/utils/InstantSerializer.kt b/app/src/main/java/com/appunite/loudius/network/utils/InstantSerializer.kt new file mode 100644 index 000000000..8b74a5c14 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/utils/InstantSerializer.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.network.utils + +import kotlinx.datetime.Instant +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +object InstantSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): Instant { + return Instant.parse(decoder.decodeString()) + } +} diff --git a/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt b/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt deleted file mode 100644 index d5fcc5383..000000000 --- a/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2023 AppUnite S.A. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.appunite.loudius.network.utils - -import com.google.gson.JsonDeserializationContext -import com.google.gson.JsonDeserializer -import com.google.gson.JsonElement -import com.google.gson.JsonParseException -import java.lang.reflect.Type -import java.time.LocalDateTime -import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME - -class LocalDateTimeDeserializer : JsonDeserializer { - - override fun deserialize( - json: JsonElement?, - typeOfT: Type, - context: JsonDeserializationContext, - ): LocalDateTime { - try { - json ?: throw JsonParseException("Cannot deserialize null value") - val dateString = json.asJsonPrimitive.asString - val offsetDateTime = OffsetDateTime.parse(dateString, ISO_OFFSET_DATE_TIME) - return offsetDateTime.toLocalDateTime() - } catch (e: Exception) { - throw JsonParseException(e) - } - } -} diff --git a/app/src/main/java/com/appunite/loudius/network/utils/RequestErrorParser.kt b/app/src/main/java/com/appunite/loudius/network/utils/RequestErrorParser.kt deleted file mode 100644 index ae8e76198..000000000 --- a/app/src/main/java/com/appunite/loudius/network/utils/RequestErrorParser.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2023 AppUnite S.A. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.appunite.loudius.network.utils - -interface RequestErrorParser { - - operator fun invoke(responseCode: Int, responseMessage: String): Exception -} diff --git a/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt index 38f431554..4ae683118 100644 --- a/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt @@ -22,17 +22,13 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.appunite.loudius.network.utils.AuthFailureHandler -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject data class MainState( val authFailureEvent: Unit? = null, ) -@HiltViewModel -class MainViewModel @Inject constructor(private val authFailureHandler: AuthFailureHandler) : - ViewModel() { +class MainViewModel(private val authFailureHandler: AuthFailureHandler) : ViewModel() { var state by mutableStateOf(MainState()) private set diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt index c0d30d3d1..6c7a74825 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt @@ -20,16 +20,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel import com.airbnb.android.showkase.annotation.ShowkaseComposable import com.appunite.loudius.R import com.appunite.loudius.components.components.LoudiusFullScreenError import com.appunite.loudius.components.components.LoudiusLoadingIndicator import com.appunite.loudius.components.theme.LoudiusTheme +import org.koin.androidx.compose.koinViewModel @Composable fun AuthenticatingScreen( - viewModel: AuthenticatingViewModel = hiltViewModel(), + viewModel: AuthenticatingViewModel = koinViewModel(), onNavigateToPullRequest: () -> Unit, onNavigateToLogin: () -> Unit, ) { diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt index c3493ee1a..ac1635d01 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt @@ -27,9 +27,7 @@ import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.common.Screen import com.appunite.loudius.domain.repository.AuthRepository import com.appunite.loudius.network.datasource.BadVerificationCodeException -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject sealed class AuthenticatingAction { @@ -53,8 +51,7 @@ sealed class AuthenticatingScreenNavigation { object NavigateToLogin : AuthenticatingScreenNavigation() } -@HiltViewModel -class AuthenticatingViewModel @Inject constructor( +class AuthenticatingViewModel( private val authRepository: AuthRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index e467f4b07..22a7d2e99 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -39,7 +39,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.airbnb.android.showkase.annotation.ShowkaseComposable import com.airbnb.android.showkase.models.Showkase import com.appunite.loudius.BuildConfig @@ -52,11 +51,12 @@ import com.appunite.loudius.components.components.LoudiusOutlinedButtonStyle import com.appunite.loudius.components.components.LoudiusText import com.appunite.loudius.components.components.LoudiusTextStyle import com.appunite.loudius.getBrowserIntent +import org.koin.androidx.compose.koinViewModel import com.appunite.loudius.components.R as componentsR @Composable fun LoginScreen( - viewModel: LoginScreenViewModel = hiltViewModel(), + viewModel: LoginScreenViewModel = koinViewModel(), ) { val context = LocalContext.current val navigateTo = viewModel.state.navigateTo diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt index c10428046..ee483f0fb 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt @@ -20,8 +20,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject sealed class LoginAction { object ClearNavigation : LoginAction() @@ -43,8 +41,7 @@ data class LoginState( val navigateTo: LoginNavigateTo? = null, ) -@HiltViewModel -class LoginScreenViewModel @Inject constructor( +class LoginScreenViewModel( private val githubHelper: GithubHelper, ) : ViewModel() { diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 4130cb24c..7578b245d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -38,7 +38,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel import com.airbnb.android.showkase.annotation.ShowkaseComposable import com.appunite.loudius.R import com.appunite.loudius.common.Constants @@ -53,13 +52,14 @@ import com.appunite.loudius.components.components.LoudiusTextStyle import com.appunite.loudius.components.components.LoudiusTopAppBar import com.appunite.loudius.components.theme.LoudiusTheme import com.appunite.loudius.network.model.PullRequest -import java.time.LocalDateTime +import kotlinx.datetime.Instant +import org.koin.androidx.compose.koinViewModel typealias NavigateToReviewers = (String, String, String, String) -> Unit @Composable fun PullRequestsScreen( - viewModel: PullRequestsViewModel = hiltViewModel(), + viewModel: PullRequestsViewModel = koinViewModel(), navigateToReviewers: NavigateToReviewers, ) { val state = viewModel.state @@ -228,7 +228,7 @@ private val successData = Data.Success( number = 0, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", title = "[SIL-67] Details screen - network layer", - createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), + createdAt = Instant.parse("2021-11-29T16:31:41Z"), ), PullRequest( id = 1, @@ -236,7 +236,7 @@ private val successData = Data.Success( number = 1, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", title = "[SIL-66] Add client secret to build config", - createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), + createdAt = Instant.parse("2022-11-29T16:31:41Z"), ), PullRequest( id = 2, @@ -244,7 +244,7 @@ private val successData = Data.Success( number = 2, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", title = "[SIL-73] Storing access token", - createdAt = LocalDateTime.parse("2023-01-29T16:31:41"), + createdAt = Instant.parse("2023-01-29T16:31:41Z"), ), PullRequest( id = 3, @@ -252,7 +252,7 @@ private val successData = Data.Success( number = 3, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", title = "[SIL-62/SIL-75] Provide new annotation for API instances", - createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), + createdAt = Instant.parse("2022-01-29T16:31:41Z"), ), ), ) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index 7ef83c4a5..9a15c87cd 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -23,9 +23,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.network.model.PullRequest -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import javax.inject.Inject sealed class PulLRequestsAction { data class ItemClick(val id: Int) : PulLRequestsAction() @@ -51,8 +49,7 @@ data class NavigationPayload( val submissionTime: String, ) -@HiltViewModel -class PullRequestsViewModel @Inject constructor( +class PullRequestsViewModel( private val pullRequestsRepository: PullRequestRepository, ) : ViewModel() { var state: PullRequestState by mutableStateOf(PullRequestState()) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index e77858c01..ea1c5f541 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -46,7 +46,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import com.airbnb.android.showkase.annotation.ShowkaseComposable import com.appunite.loudius.R import com.appunite.loudius.components.components.LoudiusFullScreenError @@ -62,11 +61,12 @@ import com.appunite.loudius.components.components.LoudiusTopAppBar import com.appunite.loudius.components.theme.LoudiusTheme import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS +import org.koin.androidx.compose.koinViewModel import com.appunite.loudius.components.R as componentsR @Composable fun ReviewersScreen( - viewModel: ReviewersViewModel = hiltViewModel(), + viewModel: ReviewersViewModel = koinViewModel(), navigateBack: () -> Unit, ) { val state = viewModel.state @@ -147,6 +147,7 @@ private fun ReviewersScreenStateless( ) } +@OptIn(ExperimentalMaterialApi::class) @Composable private fun ReviewersScreenContent( data: Data.Success, diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 602d4078b..ba92025fd 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -29,15 +29,15 @@ import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import java.time.LocalDateTime -import java.time.temporal.ChronoUnit -import javax.inject.Inject +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.Instant +import kotlinx.datetime.minus sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() @@ -61,8 +61,7 @@ enum class ReviewersSnackbarType { SUCCESS, FAILURE } -@HiltViewModel -class ReviewersViewModel @Inject constructor( +class ReviewersViewModel( private val repository: PullRequestRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { @@ -168,8 +167,8 @@ class ReviewersViewModel @Inject constructor( } } - private fun countHoursTillNow(submissionTime: LocalDateTime): Long = - ChronoUnit.HOURS.between(submissionTime, LocalDateTime.now()) + private fun countHoursTillNow(submissionTime: Instant): Long = + Clock.System.now().minus(submissionTime, DateTimeUnit.HOUR) fun onAction(action: ReviewersAction) = when (action) { is ReviewersAction.Notify -> notifyReviewer(action.userLogin) diff --git a/app/src/test/java/com/appunite/loudius/di/CheckModulesTest.kt b/app/src/test/java/com/appunite/loudius/di/CheckModulesTest.kt new file mode 100644 index 000000000..e152e8063 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/di/CheckModulesTest.kt @@ -0,0 +1,42 @@ +package com.appunite.loudius.di + +import android.content.Context +import android.content.SharedPreferences +import com.appunite.loudius.util.MainDispatcherExtension +import io.mockk.every +import io.mockk.mockkClass +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.koin.dsl.koinApplication +import org.koin.test.KoinTest +import org.koin.test.check.checkModules +import org.koin.test.mock.MockProvider + +@ExtendWith(MainDispatcherExtension::class) +class CheckModulesTest : KoinTest { + + @Test + fun verifyKoinApp() { + val mockContext = mockkClass(Context::class) + val mockSharedPref = mockkClass(SharedPreferences::class) + + every { mockContext.getSharedPreferences(any(), any()) } returns mockSharedPref + + MockProvider.register { mockkClass(it) } + + koinApplication { + modules( + dataSourceModule, + dispatcherModule, + githubHelperModule, + networkModule, + repositoryModule, + serviceModule, + ) + + checkModules() { + withInstance(mockContext) + } + } + } +} diff --git a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt index f118061ef..44ea06a70 100644 --- a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt @@ -29,7 +29,6 @@ import com.appunite.loudius.util.Defaults import io.mockk.coEvery import io.mockk.mockk import io.mockk.spyk -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -39,7 +38,6 @@ import strikt.assertions.isEqualTo import strikt.assertions.isFailure import strikt.assertions.isSuccess -@OptIn(ExperimentalCoroutinesApi::class) class PullRequestRepositoryImpTest { private val pullRequestDataSource = spyk(FakePullRequestDataSource()) diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index f83c8fd39..ca7fe1ee1 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -16,24 +16,17 @@ package com.appunite.loudius.network -import com.appunite.loudius.network.utils.LocalDateTimeDeserializer -import com.google.gson.FieldNamingPolicy -import com.google.gson.GsonBuilder import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.engine.okhttp.OkHttpConfig import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest -import io.ktor.http.ContentType -import io.ktor.serialization.gson.GsonConverter +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json import okhttp3.mockwebserver.MockWebServer -import java.time.LocalDateTime -private fun testGson() = - GsonBuilder() - .registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeDeserializer()) - .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create() +private fun registerJson() = Json { ignoreUnknownKeys = true } fun httpClientTestDouble( mockWebServer: MockWebServer, @@ -48,9 +41,6 @@ fun httpClientTestDouble( ) } install(ContentNegotiation) { - register( - ContentType.Application.Json, - GsonConverter(testGson()), - ) + json(registerJson()) } } diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsDataSourceImplTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsDataSourceImplTest.kt index fa855de00..4281abcd7 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsDataSourceImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsDataSourceImplTest.kt @@ -29,6 +29,7 @@ import io.ktor.client.plugins.ClientRequestException import io.ktor.serialization.ContentConvertException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.SocketPolicy @@ -43,7 +44,6 @@ import strikt.assertions.isFailure import strikt.assertions.isSuccess import strikt.assertions.single import java.net.ConnectException -import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsDataSourceImplTest { @@ -409,7 +409,7 @@ class PullRequestsDataSourceImplTest { "1", User(10000000, "exampleUser"), ReviewState.COMMENTED, - LocalDateTime.parse("2023-03-02T10:21:36"), + Instant.parse("2023-03-02T10:21:36Z"), ), ) } diff --git a/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt b/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt index 0cf3d9d1e..6536c1312 100644 --- a/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt @@ -29,6 +29,7 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.jupiter.api.AfterEach @@ -106,5 +107,6 @@ class AuthFailureInterceptorTest { suspend fun makeARequest(): TestData = client.get("/test").body() } + @Serializable private data class TestData(val message: String) } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 1b64dca1d..3560fb73f 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -26,11 +26,12 @@ import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.every import io.mockk.mockk -import io.mockk.mockkStatic +import io.mockk.mockkObject import io.mockk.spyk import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -47,23 +48,16 @@ import strikt.assertions.isEqualTo import strikt.assertions.isFalse import strikt.assertions.isNull import strikt.assertions.isTrue -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset -@OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) class ReviewersViewModelTest { - private val systemNow = LocalDateTime.parse("2022-01-29T15:00:00") - private val systemClockFixed = - Clock.fixed(systemNow.toInstant(ZoneOffset.UTC), ZoneId.of("UTC")) + private val systemNow = Instant.parse("2022-01-29T15:00:00Z") private val repository = spyk(FakePullRequestRepository()) private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) { every { get(any()) } returns "example" - every { get("submission_date") } returns "2022-01-29T08:00:00" + every { get("submission_date") } returns "2022-01-29T08:00:00Z" every { get("pull_request_number") } returns "correctPullRequestNumber" } private lateinit var viewModel: ReviewersViewModel @@ -72,8 +66,8 @@ class ReviewersViewModelTest { @BeforeEach fun setup() { - mockkStatic(Clock::class) - every { Clock.systemDefaultZone() } returns systemClockFixed + mockkObject(Clock.System) + every { Clock.System.now() } returns systemNow } @Nested diff --git a/app/src/test/java/com/appunite/loudius/util/Defaults.kt b/app/src/test/java/com/appunite/loudius/util/Defaults.kt index 3ef477ffc..74136370e 100644 --- a/app/src/test/java/com/appunite/loudius/util/Defaults.kt +++ b/app/src/test/java/com/appunite/loudius/util/Defaults.kt @@ -22,12 +22,13 @@ import com.appunite.loudius.network.model.RequestedReviewer import com.appunite.loudius.network.model.Review import com.appunite.loudius.network.model.ReviewState import com.appunite.loudius.network.model.User -import java.time.LocalDateTime +import kotlinx.datetime.Instant +import kotlin.time.Duration.Companion.hours object Defaults { - val date1: LocalDateTime = LocalDateTime.parse("2022-01-29T10:00:00") - val date2: LocalDateTime = LocalDateTime.parse("2022-01-29T11:00:00") - val date3: LocalDateTime = LocalDateTime.parse("2022-01-29T12:00:00") + val date1: Instant = Instant.parse("2022-01-29T10:00:00Z") + val date2: Instant = Instant.parse("2022-01-29T11:00:00Z") + val date3: Instant = Instant.parse("2022-01-29T12:00:00Z") fun pullRequest(id: Int = 1) = PullRequest( id = id, @@ -35,7 +36,7 @@ object Defaults { number = id, repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", title = "example title", - LocalDateTime.parse("2023-03-07T08:21:45").plusHours(id.toLong()), + Instant.parse("2023-03-07T08:21:45Z").plus(id.toLong().hours), ) fun reviews() = listOf( diff --git a/build.gradle b/build.gradle index b60e28894..c48981c34 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ buildscript { ext { compose_version = '1.3.3' ktor_version = '2.3.4' + koin_version = '3.4.3' } }// Top-level build file where you can add configuration options common to all sub-projects/modules.