diff --git a/Mani-Quotify/app/build.gradle.kts b/Mani-Quotify/app/build.gradle.kts index 831ea3c..55d001f 100644 --- a/Mani-Quotify/app/build.gradle.kts +++ b/Mani-Quotify/app/build.gradle.kts @@ -1,7 +1,11 @@ +import java.util.Locale + plugins { alias(libs.plugins.androidApplication) alias(libs.plugins.jetbrainsKotlinAndroid) alias(libs.plugins.ksp) + alias(libs.plugins.kotlinSerialization) + jacoco } android { @@ -22,6 +26,10 @@ android { } buildTypes { + debug { + enableAndroidTestCoverage = true + enableUnitTestCoverage = true + } release { isMinifyEnabled = false proguardFiles( @@ -63,6 +71,11 @@ dependencies { implementation(libs.androidx.ui.navigation) implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) + implementation(libs.retrofit) + implementation(libs.ktx.sxn.json) + implementation(libs.ktx.sxn.converter) + implementation(libs.okhttp) + implementation(libs.okhttp.logging) ksp(libs.androidx.room.compiler) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) @@ -71,4 +84,65 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.mockk) + testImplementation(libs.kotlin.test.junit) +} + +val exclusions = listOf( + "**/R.class", + "**/R\$*.class", + "**/BuildConfig.*", + "**/Manifest*.*", + "**/*Test*.*", + "**/com/mani/quotify007/data/*", + "**/com/mani/quotify007/domain/*", + "**/com/mani/quotify007/ui/screens/*" +) + +tasks.withType(Test::class) { + configure { + isIncludeNoLocationClasses = true + excludes = listOf("jdk.internal.*", "sun/util/resources/cldr/provider/CLDRLocaleDataMetaInfo") + } +} + +android { + applicationVariants.all { + val variant = name.replaceFirstChar { + if (it.isLowerCase()) it.titlecase( + Locale.getDefault() + ) else it.toString() + } + + val unitTests = "test${variant}UnitTest" + val androidTests = "connected${variant}AndroidTest" + + tasks.register("Jacoco${variant}CodeCoverage") { + dependsOn(listOf(unitTests, androidTests)) + group = "Reporting" + description = "Execute ui and unit tests, generate and combine Jacoco coverage report" + reports { + xml.required.set(true) + html.required.set(true) + } + sourceDirectories.setFrom( + files( + "src/main/java", + "src/$variant/java" + ) + ) + classDirectories.setFrom(files( + fileTree(layout.buildDirectory.dir("intermediates/javac/")) { + exclude(exclusions) + }, + fileTree(layout.buildDirectory.dir("tmp/kotlin-classes/")) { + exclude(exclusions) + } + )) + executionData.setFrom(files( + fileTree(layout.buildDirectory) { include(listOf("**/*.exec", "**/*.ec")) } + )) + } + } } \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/AndroidManifest.xml b/Mani-Quotify/app/src/main/AndroidManifest.xml index 5a168bd..deb8f01 100644 --- a/Mani-Quotify/app/src/main/AndroidManifest.xml +++ b/Mani-Quotify/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + + val certificateFactory = java.security.cert.CertificateFactory.getInstance("X.509") + val certificate = certificateFactory.generateCertificate(certInputStream) + setCertificateEntry("api_quotable_io", certificate) + } + } + + // Create a TrustManager that trusts the CAs in our KeyStore + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { + init(keyStore) + } + val trustManagers = trustManagerFactory.trustManagers + val sslContext = SSLContext.getInstance("TLS").apply { + init(null, trustManagers, java.security.SecureRandom()) + } + val sslSocketFactory = sslContext.socketFactory + + OkHttpClient.Builder() + .sslSocketFactory(sslSocketFactory, trustManagers[0] as X509TrustManager) + .build() + } catch (e: Exception) { + throw RuntimeException(e) + } +} \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/data/remote/mapper/QuoteMapper.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/data/remote/mapper/QuoteMapper.kt new file mode 100644 index 0000000..ea0fa0e --- /dev/null +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/data/remote/mapper/QuoteMapper.kt @@ -0,0 +1,21 @@ +package com.mani.quotify007.data.remote.mapper + +import com.mani.quotify007.data.remote.model.QuoteNetworkModel +import com.mani.quotify007.domain.model.Quote +import com.mani.quotify007.domain.model.QuoteResult + +fun QuoteNetworkModel.toResults() = QuoteResult( + count = count, + lastItemIndex = lastItemIndex, + page = page, + results = results.map { + Quote( + id = it.id, + author = it.author, + content = it.content, + isFavorite = false + ) + }, + totalCount = totalCount, + totalPages = totalPages +) diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/data/remote/model/QuoteNetworkModel.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/data/remote/model/QuoteNetworkModel.kt new file mode 100644 index 0000000..58e34bc --- /dev/null +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/data/remote/model/QuoteNetworkModel.kt @@ -0,0 +1,27 @@ +package com.mani.quotify007.data.remote.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class QuoteNetworkModel ( + val count: Long, + val totalCount: Long, + val page: Long, + val totalPages: Long, + val lastItemIndex: Long, + val results: List +) + +@Serializable +data class QuoteItemNetworkModel ( + @SerialName("_id") + val id: String, + val author: String, + val content: String, + val tags: List, + val authorSlug: String, + val length: Long, + val dateAdded: String, + val dateModified: String +) \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/data/repository/QuoteRepositoryImpl.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/data/repository/QuoteRepositoryImpl.kt index adac15b..4031680 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/data/repository/QuoteRepositoryImpl.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/data/repository/QuoteRepositoryImpl.kt @@ -1,73 +1,46 @@ package com.mani.quotify007.data.repository -import com.mani.quotify007.data.local.FavoriteQuoteDao import com.mani.quotify007.data.local.FavoriteQuoteEntity +import com.mani.quotify007.data.local.QuotifyDatabase +import com.mani.quotify007.data.remote.QuoteApiService +import com.mani.quotify007.data.remote.mapper.toResults import com.mani.quotify007.domain.model.Quote +import com.mani.quotify007.domain.model.QuoteResult import com.mani.quotify007.domain.repository.QuoteRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -class QuoteRepositoryImpl(private val favoriteQuoteDao: FavoriteQuoteDao) : QuoteRepository { - private val quotes = mutableListOf( - Quote( - 1, - "The greatest glory in living lies not in never falling, " + - "but in rising every time we fall.", - "Nelson Mandela" - ), - Quote( - 2, - "The way to get started is to quit talking and begin doing.", - "Walt Disney" - ), - Quote( - 3, - "Your time is limited, so don't waste it living someone else's life.", - "Steve Jobs" - ), - Quote( - 4, - "If life were predictable it would cease to be life, and be without flavor.", - "Eleanor Roosevelt" - ), - Quote( - 5, - "If you look at what you have in life, you'll always have more. " + - "If you look at what you don't have in life, you'll never have enough.", - "Oprah Winfrey" - ), - Quote( - 6, - "If you set your goals ridiculously high and it's a failure, " + - "you will fail above everyone else's success.", - "James Cameron" - ) - ) +class QuoteRepositoryImpl( + private val quoteApiService: QuoteApiService, + private val quoteDb: QuotifyDatabase +) : QuoteRepository { - override fun getQuotes(): List = quotes + override suspend fun getQuoteResult(): QuoteResult = + quoteApiService.getQuoteResult().toResults() override fun addQuote(quote: Quote) { - quotes.add(quote) + //TODO: add quote to list } override fun removeQuote(quote: Quote) { - quotes.remove(quote) + //TODO: remove quote from list } - override fun getFavoriteQuotes(): Flow> = - favoriteQuoteDao.getAllFavoriteQuotes().map {entities -> + override suspend fun getFavoriteQuotes(): Flow> = + quoteDb.favoriteQuoteDao().getAllFavoriteQuotes().map {entities -> entities.map { entity -> entity.toDomainModel() } } override suspend fun addFavoriteQuote(quote: Quote) { - favoriteQuoteDao.insertFavoriteQuote(quote.toEntity()) + quoteDb.favoriteQuoteDao().insertFavoriteQuote(quote.toEntity()) } override suspend fun removeFavoriteQuote(quote: Quote) { - favoriteQuoteDao.deleteFavoriteQuote(quote.toEntity()) + quoteDb.favoriteQuoteDao().deleteFavoriteQuote(quote.toEntity()) } } -private fun Quote.toEntity() = FavoriteQuoteEntity(id = id, text = text, author = author ?: "") +private fun Quote.toEntity() = FavoriteQuoteEntity(id = id, content = content, author = author) -private fun FavoriteQuoteEntity.toDomainModel() = Quote(id, text, author) +private fun FavoriteQuoteEntity.toDomainModel() = + Quote(id, content = content, author = author, isFavorite = true) diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/domain/model/Quote.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/domain/model/Quote.kt index 9eb3074..f2c95b8 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/domain/model/Quote.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/domain/model/Quote.kt @@ -1,8 +1,8 @@ package com.mani.quotify007.domain.model data class Quote( - val id: Int, - val text: String, - val author: String? = null, + val id: String, + val content: String, + val author: String, var isFavorite: Boolean = false ) diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/domain/model/QuoteResult.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/domain/model/QuoteResult.kt new file mode 100644 index 0000000..0af1a08 --- /dev/null +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/domain/model/QuoteResult.kt @@ -0,0 +1,10 @@ +package com.mani.quotify007.domain.model + +data class QuoteResult( + val count: Long? = null, + val lastItemIndex: Long? = null, + val page: Long? = null, + val results: List, + val totalCount: Long? = null, + val totalPages: Long? = null +) \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/domain/repository/QuoteRepository.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/domain/repository/QuoteRepository.kt index cdddc6c..fb3da42 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/domain/repository/QuoteRepository.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/domain/repository/QuoteRepository.kt @@ -1,13 +1,14 @@ package com.mani.quotify007.domain.repository import com.mani.quotify007.domain.model.Quote +import com.mani.quotify007.domain.model.QuoteResult import kotlinx.coroutines.flow.Flow interface QuoteRepository { - fun getQuotes(): List + suspend fun getQuoteResult(): QuoteResult fun addQuote(quote: Quote) fun removeQuote(quote: Quote) - fun getFavoriteQuotes(): Flow> + suspend fun getFavoriteQuotes(): Flow> suspend fun addFavoriteQuote(quote: Quote) suspend fun removeFavoriteQuote(quote: Quote) } \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/domain/usecase/GetQuoteUseCase.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/domain/usecase/GetQuoteUseCase.kt index c01d784..6cad5fe 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/domain/usecase/GetQuoteUseCase.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/domain/usecase/GetQuoteUseCase.kt @@ -1,8 +1,19 @@ package com.mani.quotify007.domain.usecase import com.mani.quotify007.domain.model.Quote +import com.mani.quotify007.domain.model.QuoteResult import com.mani.quotify007.domain.repository.QuoteRepository +import com.mani.quotify007.ui.navigation.model.ResponseResult class GetQuoteUseCase(private val repository: QuoteRepository) { - fun execute(): List = repository.getQuotes() + suspend fun result(): ResponseResult { + return try { + ResponseResult.Success(repository.getQuoteResult()) + } catch (e: Exception) { + ResponseResult.Error(e) + } + } + suspend fun dbQuotes() = repository.getFavoriteQuotes() + suspend fun addFavoriteQuote(quote: Quote) = repository.addFavoriteQuote(quote) + suspend fun removeFavoriteQuote(quote: Quote) = repository.removeFavoriteQuote(quote) } \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/MainActivity.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/MainActivity.kt index 8db4517..1dd5146 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/MainActivity.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/MainActivity.kt @@ -3,17 +3,30 @@ package com.mani.quotify007.ui import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.viewModels import androidx.compose.runtime.collectAsState +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.mani.quotify007.QuotifyApp import com.mani.quotify007.ui.navigation.MainScreen +import com.mani.quotify007.ui.navigation.model.UiActionEvent import com.mani.quotify007.ui.navigation.viewmodel.MainViewModel +import com.mani.quotify007.ui.navigation.viewmodel.QuoteViewModelFactory import com.mani.quotify007.ui.screens.utils.onCopyText import com.mani.quotify007.ui.screens.utils.shareQuote +import com.mani.quotify007.ui.screens.utils.showToast import com.mani.quotify007.ui.theme.QuotifyAppTheme +import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { - private val viewModel: MainViewModel by viewModels() + private val viewModel: MainViewModel by lazy { + ViewModelProvider( + this, + factory = QuoteViewModelFactory(QuotifyApp.instance.quoteUseCase) + ).get(MainViewModel::class.java) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { @@ -23,11 +36,21 @@ class MainActivity : ComponentActivity() { val state = viewModel.state.collectAsState().value MainScreen( state, - onEvent = { event -> viewModel.onEvent(event) }, - onCopyText = { quote -> onCopyText(this, quote) }, - onShareClick = { quote -> shareQuote(this, quote) } + onEvent = { event -> viewModel.onEvent(event) } ) } } + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiActionEvent.collect { event -> + when (event) { + is UiActionEvent.CopyText -> onCopyText(this@MainActivity, event.quote) + is UiActionEvent.ShareClick -> shareQuote(this@MainActivity, event.quote) + is UiActionEvent.ShowToast -> showToast(event.message) + else -> { /* Do nothing */} + } + } + } + } } } \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/MainScreen.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/MainScreen.kt index 77c9758..d5dcb2b 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/MainScreen.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/MainScreen.kt @@ -1,12 +1,14 @@ package com.mani.quotify007.ui.navigation +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.compose.rememberNavController -import com.mani.quotify007.domain.model.Quote import com.mani.quotify007.ui.navigation.model.MainEvent import com.mani.quotify007.ui.navigation.model.MainState import com.mani.quotify007.ui.screens.bottomappbar.BottomAppBar @@ -15,24 +17,23 @@ import com.mani.quotify007.ui.theme.QuotifyAppTheme @Composable fun MainScreen( state: MainState, - onEvent: (MainEvent) -> Unit, - onCopyText: (Quote) -> Unit, - onShareClick: (Quote) -> Unit + onEvent: (MainEvent) -> Unit ) { val navController = rememberNavController() Scaffold( bottomBar = { BottomAppBar(navController) } ) { innerPadding -> - NavigationGraph( - navController = navController, - modifier = Modifier.padding(innerPadding), - state = state, - quotes = state.quotes, - favoriteQuotes = state.favoriteQuotes, - onEvent = onEvent, - onCopyText = onCopyText, - onShareClick = onShareClick - ) + Box { + if (state.isLoading) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + NavigationGraph( + navController = navController, + modifier = Modifier.padding(innerPadding), + state = state, + onEvent = onEvent + ) + } } } @@ -42,9 +43,7 @@ fun MainScreenPreview() { QuotifyAppTheme { MainScreen( state = MainState(), - onEvent = {}, - onCopyText = {}, - onShareClick = {} + onEvent = {} ) } } \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/NavigationGraph.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/NavigationGraph.kt index a1eeec4..c6f9d06 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/NavigationGraph.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/NavigationGraph.kt @@ -7,7 +7,6 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import com.mani.quotify007.domain.model.Quote import com.mani.quotify007.ui.navigation.model.MainEvent import com.mani.quotify007.ui.navigation.model.MainState import com.mani.quotify007.ui.screens.bottomappbar.BottomNavItem @@ -20,35 +19,25 @@ fun NavigationGraph( navController: NavHostController, modifier: Modifier = Modifier, state: MainState, - quotes: List, - favoriteQuotes: List, - onEvent: (MainEvent) -> Unit, - onCopyText: (Quote) -> Unit, - onShareClick: (Quote) -> Unit + onEvent: (MainEvent) -> Unit ) { NavHost(navController, startDestination = BottomNavItem.HOME.route, modifier = modifier) { composable(BottomNavItem.HOME.route) { HomeScreen( state, - onEvent, - onCopyText, - onShareClick + onEvent ) } composable(BottomNavItem.SEARCH.route) { SearchScreen( - quotes, - onEvent, - onCopyText, - onShareClick + state, + onEvent ) } composable(BottomNavItem.FAVORITES.route) { FavoritesScreen( - favoriteQuotes, - onEvent, - onCopyText, - onShareClick + state, + onEvent ) } } @@ -61,10 +50,6 @@ fun NavigationGraphPreview() { NavigationGraph( navController = navController, state = MainState(), - quotes = listOf(Quote(0,"Sample quote", "Sample Author")), - favoriteQuotes = listOf(Quote(0,"Sample favorite quote", "Sample Author")), - onEvent = {}, - onCopyText = {}, - onShareClick = {} + onEvent = {} ) } \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/model/MainEvent.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/model/MainEvent.kt index 12bae21..0fac455 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/model/MainEvent.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/model/MainEvent.kt @@ -6,4 +6,7 @@ sealed class MainEvent { data class AddFavorite(val quote: Quote) : MainEvent() data class RemoveFavorite(val quote: Quote): MainEvent() data object GetRandomQuote: MainEvent() + data class CopyText(val quote: Quote): MainEvent() + data class ShareClick(val quote: Quote): MainEvent() + data class ShowToast(val message: String): MainEvent() } \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/model/MainState.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/model/MainState.kt index 0cb866a..ba75158 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/model/MainState.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/model/MainState.kt @@ -5,5 +5,6 @@ import com.mani.quotify007.domain.model.Quote data class MainState( val quotes: List = emptyList(), val favoriteQuotes: List = emptyList(), - val randomQuote: Quote? = null + val randomQuote: Quote? = null, + var isLoading: Boolean = false ) \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/model/ResponseResult.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/model/ResponseResult.kt new file mode 100644 index 0000000..2fcd398 --- /dev/null +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/model/ResponseResult.kt @@ -0,0 +1,6 @@ +package com.mani.quotify007.ui.navigation.model + +sealed class ResponseResult { + data class Success(val data: T) : ResponseResult() + data class Error(val exception: Exception) : ResponseResult() +} \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/model/UiActionEvent.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/model/UiActionEvent.kt new file mode 100644 index 0000000..865eba4 --- /dev/null +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/model/UiActionEvent.kt @@ -0,0 +1,9 @@ +package com.mani.quotify007.ui.navigation.model + +import com.mani.quotify007.domain.model.Quote + +sealed class UiActionEvent { + data class CopyText(val quote: Quote) : UiActionEvent() + data class ShareClick(val quote: Quote) : UiActionEvent() + data class ShowToast(val message: String) : UiActionEvent() +} \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/viewmodel/MainViewModel.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/viewmodel/MainViewModel.kt index 263b902..0f3db26 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/viewmodel/MainViewModel.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/viewmodel/MainViewModel.kt @@ -1,40 +1,57 @@ package com.mani.quotify007.ui.navigation.viewmodel -import android.app.Application -import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.mani.quotify007.QuotifyApp -import com.mani.quotify007.data.local.FavoriteQuoteEntity -import com.mani.quotify007.domain.model.Quote +import com.mani.quotify007.domain.usecase.GetQuoteUseCase import com.mani.quotify007.ui.navigation.model.MainEvent import com.mani.quotify007.ui.navigation.model.MainState +import com.mani.quotify007.ui.navigation.model.ResponseResult +import com.mani.quotify007.ui.navigation.model.UiActionEvent +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.launch -class MainViewModel(private val application: Application) : AndroidViewModel(application) { +class MainViewModel(private val useCase: GetQuoteUseCase) : ViewModel() { private val _state = MutableStateFlow(MainState()) val state: StateFlow = _state - private val getQuoteUseCase = (application as QuotifyApp).getQuoteUseCase + private val _uiActionEvent = MutableSharedFlow() + val uiActionEvent: SharedFlow = _uiActionEvent init { + loadQuotesData() + } + + fun loadQuotesData() { viewModelScope.launch { - (application as QuotifyApp).quoteDb.favoriteQuoteDao().getAllFavoriteQuotes() + _state.value.isLoading = true + useCase.dbQuotes() + .catch { e -> e.printStackTrace() } .collect { quotes -> - _state.value = - _state.value.copy(favoriteQuotes = quotes.map { it.toDomainModel() }) - _state.value = _state.value.copy( - quotes = getQuoteUseCase.execute().map { quote -> - if (state.value.favoriteQuotes.any { it.id == quote.id }) { - quote.isFavorite = true - } else { - quote.isFavorite = false - } - quote - }, - randomQuote = getQuoteUseCase.execute().randomOrNull() - ) + when(val result = useCase.result()) { + is ResponseResult.Success -> { + _state.value = _state.value.copy( + favoriteQuotes = quotes, + quotes = result.data.results.map { quote -> + quote.isFavorite = quotes.any { it.id == quote.id } + quote + }, + isLoading = false + ) + } + is ResponseResult.Error -> { + _state.value = _state.value.copy( + favoriteQuotes = quotes, + isLoading = false + ) + _uiActionEvent.emit( + UiActionEvent.ShowToast(result.exception.message.toString()) + ) + } + } } } } @@ -43,15 +60,13 @@ class MainViewModel(private val application: Application) : AndroidViewModel(app viewModelScope.launch { when (event) { is MainEvent.AddFavorite -> { - (application as QuotifyApp).quoteDb.favoriteQuoteDao() - .insertFavoriteQuote(event.quote.toEntity()) + useCase.addFavoriteQuote(event.quote) _state.value = _state.value.copy( favoriteQuotes = _state.value.favoriteQuotes + event.quote ) } is MainEvent.RemoveFavorite -> { - (application as QuotifyApp).quoteDb.favoriteQuoteDao() - .deleteFavoriteQuote(event.quote.toEntity()) + useCase.removeFavoriteQuote(event.quote) _state.value = _state.value.copy( favoriteQuotes = _state.value.favoriteQuotes - event.quote ) @@ -60,11 +75,12 @@ class MainViewModel(private val application: Application) : AndroidViewModel(app val randomQuote = _state.value.quotes.randomOrNull() _state.value = _state.value.copy(randomQuote = randomQuote) } + is MainEvent.CopyText -> _uiActionEvent.emit(UiActionEvent.CopyText(event.quote)) + + is MainEvent.ShareClick -> _uiActionEvent.emit(UiActionEvent.ShareClick(event.quote)) + + is MainEvent.ShowToast -> _uiActionEvent.emit(UiActionEvent.ShowToast(event.message)) } } } - - private fun FavoriteQuoteEntity.toDomainModel() = Quote(id, text, author, true) - - private fun Quote.toEntity() = FavoriteQuoteEntity(id = id, text = text, author = author ?: "") } \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/viewmodel/QuoteViewModelFactory.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/viewmodel/QuoteViewModelFactory.kt new file mode 100644 index 0000000..57e7fef --- /dev/null +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/navigation/viewmodel/QuoteViewModelFactory.kt @@ -0,0 +1,17 @@ +package com.mani.quotify007.ui.navigation.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.mani.quotify007.domain.usecase.GetQuoteUseCase + +class QuoteViewModelFactory( + private val useCase: GetQuoteUseCase +): ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(MainViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return MainViewModel(useCase) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/favorites/FavoritesScreen.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/favorites/FavoritesScreen.kt index 0d9899d..083309f 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/favorites/FavoritesScreen.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/favorites/FavoritesScreen.kt @@ -12,18 +12,16 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.mani.quotify007.domain.model.Quote import com.mani.quotify007.ui.navigation.model.MainEvent +import com.mani.quotify007.ui.navigation.model.MainState import com.mani.quotify007.ui.screens.quote.QuotesScreen @Composable fun FavoritesScreen( - quotes: List, - onEvent: (MainEvent) -> Unit, - onCopyText: (Quote) -> Unit, - onShareClick: (Quote) -> Unit + state: MainState, + onEvent: (MainEvent) -> Unit ) { - if (quotes.isEmpty()) { + if (state.favoriteQuotes.isEmpty()) { Column( modifier = Modifier .fillMaxSize() @@ -41,13 +39,11 @@ fun FavoritesScreen( horizontalAlignment = Alignment.CenterHorizontally ) { LazyColumn { - items(quotes) { quote -> + items(state.favoriteQuotes) { quote -> QuotesScreen( quote, - onEvent = { onEvent(MainEvent.RemoveFavorite(quote)) }, - false, - onCopyText = onCopyText, - onShareClick = onShareClick + onEvent = onEvent, + false ) } } @@ -59,8 +55,7 @@ fun FavoritesScreen( @Composable fun FavoritesScreenPreview() { FavoritesScreen( - listOf(Quote(0, "Sample quote", "Sample Author")), - onEvent = {}, - onCopyText = {}, - onShareClick = {}) + MainState(), + onEvent = {} + ) } \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/home/HomeScreen.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/home/HomeScreen.kt index b2e36a3..92c739c 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/home/HomeScreen.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/home/HomeScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.mani.quotify007.domain.model.Quote import com.mani.quotify007.ui.navigation.model.MainEvent import com.mani.quotify007.ui.navigation.model.MainState import com.mani.quotify007.ui.screens.quote.QuotesScreen @@ -28,9 +27,7 @@ import com.mani.quotify007.ui.screens.utils.QUOTE_OF_THE_DAY_HEADER @Composable fun HomeScreen( state: MainState, - onEvent: (MainEvent) -> Unit, - onCopyText: (Quote) -> Unit, - onShareClick: (Quote) -> Unit + onEvent: (MainEvent) -> Unit ) { Column( modifier = Modifier @@ -40,13 +37,12 @@ fun HomeScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Text(QUOTE_OF_THE_DAY_HEADER, fontStyle = FontStyle.Italic, fontSize = 30.sp) + if (state.randomQuote == null) onEvent(MainEvent.GetRandomQuote) state.randomQuote?.let { quote -> QuotesScreen( - quote = quote, - onEvent = { onEvent(MainEvent.AddFavorite(quote)) }, - isAddOnly = true, - onCopyText = onCopyText, - onShareClick = onShareClick + quote = state.randomQuote, + onEvent = onEvent, + isAddOnly = true ) } Spacer(modifier = Modifier.height(16.dp)) @@ -61,5 +57,5 @@ fun HomeScreen( @Preview(showBackground = true) @Composable fun HomeScreenPreview() { - HomeScreen(MainState(), onEvent = {}, onCopyText = {}, onShareClick = {}) + HomeScreen(MainState(), onEvent = {}) } \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/quote/QuotesScreen.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/quote/QuotesScreen.kt index cd3aa34..7b93607 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/quote/QuotesScreen.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/quote/QuotesScreen.kt @@ -1,6 +1,5 @@ package com.mani.quotify007.ui.screens.quote -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -21,7 +20,6 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -30,18 +28,18 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.mani.quotify007.domain.model.Quote import com.mani.quotify007.ui.navigation.model.MainEvent +import com.mani.quotify007.ui.screens.utils.ADDED_TO_FAVORITES +import com.mani.quotify007.ui.screens.utils.ALREADY_ADDED_TO_FAVORITES import com.mani.quotify007.ui.screens.utils.HYPHEN_SPACE +import com.mani.quotify007.ui.screens.utils.REMOVED_FROM_FAVORITES +import com.mani.quotify007.ui.screens.utils.TEXT_COPIED_TO_CLIPBOARD @Composable fun QuotesScreen( quote: Quote, onEvent: (MainEvent) -> Unit, - isAddOnly: Boolean, - onCopyText: (Quote) -> Unit, - onShareClick: (Quote) -> Unit + isAddOnly: Boolean ) { - // TODO: Approach to be discussed - context should be passed from MainActivity. - val context = LocalContext.current Card( modifier = Modifier .fillMaxWidth() @@ -53,7 +51,7 @@ fun QuotesScreen( .padding(16.dp) ) { Text( - text = quote.text, + text = quote.content, style = MaterialTheme.typography.titleMedium, color = Color.Black, fontFamily = FontFamily.Serif, @@ -82,15 +80,15 @@ fun QuotesScreen( if (!quote.isFavorite) { quote.isFavorite = true onEvent(MainEvent.AddFavorite(quote)) - Toast.makeText(context, "Added to favorites", Toast.LENGTH_SHORT).show() + onEvent(MainEvent.ShowToast(ADDED_TO_FAVORITES)) } else { - Toast.makeText(context, "Already added to favorites", Toast.LENGTH_SHORT).show() + onEvent(MainEvent.ShowToast(ALREADY_ADDED_TO_FAVORITES)) } } else { if (quote.isFavorite) { quote.isFavorite = false onEvent(MainEvent.RemoveFavorite(quote)) - Toast.makeText(context, "Removed from favorites", Toast.LENGTH_SHORT).show() + onEvent(MainEvent.ShowToast(REMOVED_FROM_FAVORITES)) } } }) { @@ -102,13 +100,14 @@ fun QuotesScreen( Text("Save") } TextButton(onClick = { - onCopyText(quote) + onEvent(MainEvent.CopyText(quote)) + onEvent(MainEvent.ShowToast(TEXT_COPIED_TO_CLIPBOARD)) }) { Spacer(modifier = Modifier.width(4.dp)) Text("Copy Text") } TextButton(onClick = { - onShareClick(quote) + onEvent(MainEvent.ShareClick(quote)) }) { Icon(imageVector = Icons.Default.Share, contentDescription = "Share") Spacer(modifier = Modifier.width(4.dp)) @@ -122,9 +121,8 @@ fun QuotesScreen( @Composable fun QuotesScreenPreview() { QuotesScreen( - Quote(0, "Sample quote", "Sample author"), + Quote(id = "000", content = "Sample quote", author = "Sample author"), onEvent = {}, - true, - onCopyText = {}, - onShareClick = {}) + true + ) } \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/search/SearchScreen.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/search/SearchScreen.kt index 4eadd80..f29e3ac 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/search/SearchScreen.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/search/SearchScreen.kt @@ -25,19 +25,18 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.mani.quotify007.domain.model.Quote import com.mani.quotify007.ui.navigation.model.MainEvent +import com.mani.quotify007.ui.navigation.model.MainState import com.mani.quotify007.ui.screens.quote.QuotesScreen @Composable fun SearchScreen( - quotes: List, - onEvent: (MainEvent) -> Unit, - onCopyText: (Quote) -> Unit, - onShareClick: (Quote) -> Unit + state: MainState, + onEvent: (MainEvent) -> Unit ) { var searchQuery by remember { mutableStateOf(TextFieldValue("")) } - val filteredQuotes = quotes.filter { it.text.contains(searchQuery.text, ignoreCase = true) } + val filteredQuotes = + state.quotes.filter { it.content.contains(searchQuery.text, ignoreCase = true) } Column(modifier = Modifier .fillMaxSize() @@ -63,10 +62,8 @@ fun SearchScreen( items(filteredQuotes) { quote -> QuotesScreen( quote, - onEvent = { onEvent(MainEvent.AddFavorite(quote)) }, - true, - onCopyText = onCopyText, - onShareClick = onShareClick + onEvent = onEvent, + true ) } } @@ -77,9 +74,7 @@ fun SearchScreen( @Composable fun SearchScreenPreview() { SearchScreen( - listOf(Quote(0, "Sample quote")), - onEvent = {}, - onCopyText = {}, - onShareClick = {} + MainState(), + onEvent = {} ) } \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/ApiUtil.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/ApiUtil.kt new file mode 100644 index 0000000..4d7ec62 --- /dev/null +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/ApiUtil.kt @@ -0,0 +1,4 @@ +package com.mani.quotify007.ui.screens.utils + +const val BASE_URL = "https://api.quotable.io/" +const val JSON_TYPE = "application/json" \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/ClipboardUtil.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/ClipboardUtil.kt index 7b2e051..7078e5a 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/ClipboardUtil.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/ClipboardUtil.kt @@ -3,7 +3,6 @@ package com.mani.quotify007.ui.screens.utils import android.content.ClipData import android.content.ClipboardManager import android.content.Context -import android.widget.Toast import com.mani.quotify007.domain.model.Quote fun onCopyText(context: Context, quote: Quote) { @@ -12,8 +11,7 @@ fun onCopyText(context: Context, quote: Quote) { clipboardManager.setPrimaryClip( ClipData.newPlainText( "quote", - "${quote.text} - ${quote.author}" + "${quote.content} - ${quote.author}" ) ) - Toast.makeText(context, "Text copied to clipboard", Toast.LENGTH_SHORT).show() } \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/ContextExtensions.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/ContextExtensions.kt new file mode 100644 index 0000000..b9151b2 --- /dev/null +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/ContextExtensions.kt @@ -0,0 +1,8 @@ +package com.mani.quotify007.ui.screens.utils + +import android.content.Context +import android.widget.Toast + +fun Context.showToast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() +} \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/ShareUtil.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/ShareUtil.kt index abcfba5..37f38f7 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/ShareUtil.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/ShareUtil.kt @@ -7,7 +7,7 @@ import com.mani.quotify007.domain.model.Quote fun shareQuote(context: Context, quote: Quote) { val shareIntent = Intent().apply { action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, "${quote.text} - ${quote.author}") + putExtra(Intent.EXTRA_TEXT, "${quote.content} - ${quote.author}") type = "text/plain" } context.startActivity(Intent.createChooser(shareIntent, null)) diff --git a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/StringUtil.kt b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/StringUtil.kt index 81cfe84..2663a24 100644 --- a/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/StringUtil.kt +++ b/Mani-Quotify/app/src/main/java/com/mani/quotify007/ui/screens/utils/StringUtil.kt @@ -1,4 +1,8 @@ package com.mani.quotify007.ui.screens.utils const val QUOTE_OF_THE_DAY_HEADER = "Quote of the Day" -const val HYPHEN_SPACE = "- " \ No newline at end of file +const val HYPHEN_SPACE = "- " +const val ALREADY_ADDED_TO_FAVORITES = "Already added to favorites" +const val ADDED_TO_FAVORITES = "Added to favorites" +const val REMOVED_FROM_FAVORITES = "Removed from favorites" +const val TEXT_COPIED_TO_CLIPBOARD = "Text copied to clipboard" \ No newline at end of file diff --git a/Mani-Quotify/app/src/main/res/raw/api_quotable_io.pem b/Mani-Quotify/app/src/main/res/raw/api_quotable_io.pem new file mode 100644 index 0000000..a4255df --- /dev/null +++ b/Mani-Quotify/app/src/main/res/raw/api_quotable_io.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE7TCCA9WgAwIBAgISA/ei+uPb3vM4UWafWlbTMdGzMA0GCSqGSIb3DQEBCwUA +MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD +EwNSMTAwHhcNMjQwNjEyMjMwNTA2WhcNMjQwOTEwMjMwNTA1WjAaMRgwFgYDVQQD +Ew9hcGkucXVvdGFibGUuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQDSdLfJgZR0UGYFWvAczzox7BC9yrdicfeFKMs0usXefhudus77t7XToTFIaRz+ +HRbq0uGWaueoBg7TFHDu7G4atQ9Uu03GVvJE/G5u1GJyBKiaVya02oO13ll77S9S +2jMGlrn0Sy80DHLuG+7AjnrS1lyMQnPSYA0dK6rxV3QEHutJjLkYIbPmDoe71eEv +GjPA2m6ToVtxlIWHVHyaXMxc9Rw5w/tdZaod0TqtPs4hcpw5B9NXoG2qyZJjHBeo +dZl71SwcuKVi/F1TwKJpIkjhOHKq1byVBzUfJ83hTmDzVfvqnpuCvDLgndjzxvQw +KUTE1zWAxkik8v0APEN8riUZAgMBAAGjggISMIICDjAOBgNVHQ8BAf8EBAMCBaAw +HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYD +VR0OBBYEFOYIf/xJHIiu70Hu33S6rc2K92hEMB8GA1UdIwQYMBaAFLu8w0el5Lyp +xsOkcgwQjaI14cjoMFcGCCsGAQUFBwEBBEswSTAiBggrBgEFBQcwAYYWaHR0cDov +L3IxMC5vLmxlbmNyLm9yZzAjBggrBgEFBQcwAoYXaHR0cDovL3IxMC5pLmxlbmNy +Lm9yZy8wGgYDVR0RBBMwEYIPYXBpLnF1b3RhYmxlLmlvMBMGA1UdIAQMMAowCAYG +Z4EMAQIBMIIBAwYKKwYBBAHWeQIEAgSB9ASB8QDvAHUAPxdLT9ciR1iUHWUchL4N +Eu2QN38fhWrrwb8ohez4ZG4AAAGQDumRgQAABAMARjBEAiAcrtm5Jz29I8ksTnE0 +XaF1SGvvSUdqVdoe87+LqIxmvAIgRpjfZ/jYYXYeXEps3SMmk25zU9rYauZsNYOu +THbAOXsAdgAZmBBxCfDWUi4wgNKeP2S7g24ozPkPUo7u385KPxa0ygAAAZAO6ZGW +AAAEAwBHMEUCIDDMovkuAnaB6bp9urfxSCIoTPGF5B3jp1C5Kwhi1rVNAiEA5bCt +Z2+EpQfVvXcnxaGTuT25uE/n8eVrHGVGyLTJm9kwDQYJKoZIhvcNAQELBQADggEB +AImQm9nT5D1b7zQmblhHaex284QM78Q+oS0/IPaRMc2uAA269fEQXtYlp8ubnZl3 +Mv3mOn/B8AsgQClTXALZ6x2pcRNCbluJP3RK7uFn/YRFkTG/az1sk3mE6uIf23mm +Zy7LCPbPmSR6hOKAwYoee+LrZsEcHT3J7eHk9pCCTbOFPzWg1hMptomyYt8omgm/ +m5iAQJNwUF3MUmmmu5/SSutNBL6PLLKa68KHgBuaYcSmzf1oBo3ioFMs1a34Yz9m +Kcj6ahMYxA3rpFDXj6RA4c08Q1H0/Up+x4/hUPZd+Z5HdEe1i9PJ6EDVNCcykqDb +cbFMb9/PpYSjg8XSZ0CrXnM= +-----END CERTIFICATE----- diff --git a/Mani-Quotify/app/src/test/java/com/mani/quotify007/ExampleUnitTest.kt b/Mani-Quotify/app/src/test/java/com/mani/quotify007/ExampleUnitTest.kt deleted file mode 100644 index 90945eb..0000000 --- a/Mani-Quotify/app/src/test/java/com/mani/quotify007/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.mani.quotify007 - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/Mani-Quotify/app/src/test/java/com/mani/quotify007/ui/navigation/viewmodel/MainViewModelTest.kt b/Mani-Quotify/app/src/test/java/com/mani/quotify007/ui/navigation/viewmodel/MainViewModelTest.kt new file mode 100644 index 0000000..4b83b75 --- /dev/null +++ b/Mani-Quotify/app/src/test/java/com/mani/quotify007/ui/navigation/viewmodel/MainViewModelTest.kt @@ -0,0 +1,86 @@ +package com.mani.quotify007.ui.navigation.viewmodel + +import com.mani.quotify007.domain.model.Quote +import com.mani.quotify007.domain.model.QuoteResult +import com.mani.quotify007.domain.usecase.GetQuoteUseCase +import com.mani.quotify007.ui.navigation.model.MainEvent +import com.mani.quotify007.ui.navigation.model.ResponseResult +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalCoroutinesApi::class) +class MainViewModelTest { + + private lateinit var useCase: GetQuoteUseCase + + private val viewModel: MainViewModel by lazy { + MainViewModel(useCase) + } + + @Before + fun setUp() { + useCase = mockk() + Dispatchers.setMain(Dispatchers.Unconfined) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun loadQuotesData_should_update_state_with_quotes_on_success() = runTest { + val quotes = listOf(Quote(id = "1", content = "Test Quote", author = "Author")) + val quoteResult = QuoteResult(results = quotes) + coEvery { useCase.dbQuotes() } returns flowOf(quotes) + coEvery { useCase.result() } returns ResponseResult.Success(quoteResult) + + viewModel.loadQuotesData() + advanceUntilIdle() + + assertEquals(quotes, viewModel.state.value.quotes) + assertEquals(false, viewModel.state.value.isLoading) + assertEquals("Test Quote", viewModel.state.value.quotes[0].content) + } + + @Test + fun loadQuotesData_should_emit_error_event_on_failure() = runTest{ + val exception = Exception("Test Exception") + coEvery { useCase.dbQuotes() } returns flowOf(emptyList()) + coEvery { useCase.result() } returns ResponseResult.Error(exception) + + viewModel.loadQuotesData() + advanceUntilIdle() + + assertEquals(emptyList(), viewModel.state.value.quotes) + assertEquals(false, viewModel.state.value.isLoading) + + } + + @Test + fun `onEvent CopyText should emit UiActionEvent CopyText`() = runTest(UnconfinedTestDispatcher()) { + val quote = Quote(id = "1", content = "Test Quote", author = "Author") + + val quotes = listOf(Quote(id = "1", content = "Test Quote", author = "Author")) + val quoteResult = QuoteResult(results = quotes) + coEvery { useCase.dbQuotes() } returns flowOf(quotes) + coEvery { useCase.result() } returns ResponseResult.Success(quoteResult) + + viewModel.onEvent(MainEvent.CopyText(quote)) + viewModel.onEvent(MainEvent.ShareClick(quote)) + viewModel.onEvent(MainEvent.ShowToast("quote")) + } + +} \ No newline at end of file diff --git a/Mani-Quotify/gradle/libs.versions.toml b/Mani-Quotify/gradle/libs.versions.toml index 0ff70a9..45c5653 100644 --- a/Mani-Quotify/gradle/libs.versions.toml +++ b/Mani-Quotify/gradle/libs.versions.toml @@ -2,6 +2,7 @@ agp = "8.3.0" kotlin = "1.9.0" ksp = "1.9.0-1.0.13" +serializationPlugin = "1.9.22" coreKtx = "1.13.1" junit = "4.13.2" junitVersion = "1.2.1" @@ -11,6 +12,13 @@ activityCompose = "1.9.2" composeBom = "2023.08.00" navigationCompose = "2.8.2" roomVersion = "2.6.1" +retrofit = "2.11.0" +jsonConverter = "1.6.0" +ktxConvertor = "1.0.0" +okhttp = "4.11.0" +kotlinTestJunit = "1.6.10" +mockk = "1.12.0" +kotlinxCoroutinesTest = "1.6.4" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -24,6 +32,15 @@ androidx-ui-navigation = { group = "androidx.navigation", name = "navigation-com androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "roomVersion" } androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomVersion" } androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "roomVersion" } +# Kotlin serialization +kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlinTestJunit" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +ktx-sxn-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "jsonConverter" } +ktx-sxn-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "ktxConvertor" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } @@ -36,4 +53,5 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3" androidApplication = { id = "com.android.application", version.ref = "agp" } jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "serializationPlugin" }