diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt index 998043e..afb786c 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt @@ -5,6 +5,7 @@ import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity interface RecipeRepo { + fun createPager(): Pager suspend fun clearLocalData() @@ -12,4 +13,6 @@ interface RecipeRepo { suspend fun refreshRecipeInfo(recipeSlug: String): Result suspend fun loadRecipeInfo(recipeId: String): FullRecipeEntity? + + fun updateNameQuery(name: String?) } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt index 89f9439..324282d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt @@ -9,7 +9,7 @@ import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity interface RecipeStorage { suspend fun saveRecipes(recipes: List) - fun queryRecipes(): PagingSource + fun queryRecipes(query: String?): PagingSource suspend fun refreshAll(recipes: List) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt index 3431207..2b25783 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt @@ -23,21 +23,17 @@ class RecipeStorageImpl @Inject constructor( ) : RecipeStorage { private val recipeDao: RecipeDao by lazy { db.recipeDao() } - override suspend fun saveRecipes( - recipes: List - ) = db.withTransaction { + override suspend fun saveRecipes(recipes: List) { logger.v { "saveRecipes() called with $recipes" } - - for (recipe in recipes) { - val recipeSummaryEntity = recipe.toRecipeSummaryEntity() - recipeDao.insertRecipe(recipeSummaryEntity) - } + val entities = recipes.map { it.toRecipeSummaryEntity() } + logger.v { "saveRecipes: entities = $entities" } + db.withTransaction { recipeDao.insertRecipes(entities) } } - - override fun queryRecipes(): PagingSource { - logger.v { "queryRecipes() called" } - return recipeDao.queryRecipesByPages() + override fun queryRecipes(query: String?): PagingSource { + logger.v { "queryRecipes() called with: query = $query" } + return if (query == null) recipeDao.queryRecipesByPages() + else recipeDao.queryRecipesByPages(query) } override suspend fun refreshAll(recipes: List) { diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactory.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactory.kt new file mode 100644 index 0000000..62ff23e --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactory.kt @@ -0,0 +1,9 @@ +package gq.kirmanak.mealient.data.recipes.impl + +import androidx.paging.PagingSource +import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity + +interface RecipePagingSourceFactory : () -> PagingSource { + fun setQuery(newQuery: String?) + fun invalidate() +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImpl.kt new file mode 100644 index 0000000..35c5500 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImpl.kt @@ -0,0 +1,33 @@ +package gq.kirmanak.mealient.data.recipes.impl + +import androidx.paging.InvalidatingPagingSourceFactory +import gq.kirmanak.mealient.data.recipes.db.RecipeStorage +import gq.kirmanak.mealient.logging.Logger +import java.util.concurrent.atomic.AtomicReference +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RecipePagingSourceFactoryImpl @Inject constructor( + private val recipeStorage: RecipeStorage, + private val logger: Logger, +) : RecipePagingSourceFactory { + + private val query = AtomicReference(null) + + private val delegate = InvalidatingPagingSourceFactory { + val currentQuery = query.get() + logger.d { "Creating paging source, query is $currentQuery" } + recipeStorage.queryRecipes(currentQuery) + } + + override fun invoke() = delegate.invoke() + + override fun setQuery(newQuery: String?) { + logger.v { "setQuery() called with: newQuery = $newQuery" } + query.set(newQuery) + invalidate() + } + + override fun invalidate() = delegate.invalidate() +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt index 4bedf65..97ab134 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt @@ -1,7 +1,6 @@ package gq.kirmanak.mealient.data.recipes.impl import androidx.paging.ExperimentalPagingApi -import androidx.paging.InvalidatingPagingSourceFactory import androidx.paging.Pager import androidx.paging.PagingConfig import gq.kirmanak.mealient.data.recipes.RecipeRepo @@ -19,17 +18,18 @@ import javax.inject.Singleton class RecipeRepoImpl @Inject constructor( private val mediator: RecipesRemoteMediator, private val storage: RecipeStorage, - private val pagingSourceFactory: InvalidatingPagingSourceFactory, + private val pagingSourceFactory: RecipePagingSourceFactory, private val dataSource: RecipeDataSource, private val logger: Logger, ) : RecipeRepo { + override fun createPager(): Pager { logger.v { "createPager() called" } val pagingConfig = PagingConfig(pageSize = 5, enablePlaceholders = true) return Pager( config = pagingConfig, remoteMediator = mediator, - pagingSourceFactory = pagingSourceFactory + pagingSourceFactory = pagingSourceFactory, ) } @@ -53,4 +53,9 @@ class RecipeRepoImpl @Inject constructor( logger.v { "loadRecipeInfo() returned: $recipeInfo" } return recipeInfo } + + override fun updateNameQuery(name: String?) { + logger.v { "updateNameQuery() called with: name = $name" } + pagingSourceFactory.setQuery(name) + } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt index 435d76d..54915d5 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt @@ -17,7 +17,7 @@ import javax.inject.Singleton class RecipesRemoteMediator @Inject constructor( private val storage: RecipeStorage, private val network: RecipeDataSource, - private val pagingSourceFactory: InvalidatingPagingSourceFactory, + private val pagingSourceFactory: RecipePagingSourceFactory, private val logger: Logger, ) : RemoteMediator() { diff --git a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt index 8e6213d..210b3de 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt @@ -1,6 +1,5 @@ package gq.kirmanak.mealient.di -import androidx.paging.InvalidatingPagingSourceFactory import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.request.RequestOptions import dagger.Binds @@ -13,9 +12,7 @@ import gq.kirmanak.mealient.data.network.MealieDataSourceWrapper import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.db.RecipeStorageImpl -import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider -import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProviderImpl -import gq.kirmanak.mealient.data.recipes.impl.RecipeRepoImpl +import gq.kirmanak.mealient.data.recipes.impl.* import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.ui.recipes.images.RecipeModelLoaderFactory @@ -46,13 +43,11 @@ interface RecipeModule { @Singleton fun bindModelLoaderFactory(recipeModelLoaderFactory: RecipeModelLoaderFactory): ModelLoaderFactory - companion object { + @Binds + @Singleton + fun bindRecipePagingSourceFactory(recipePagingSourceFactoryImpl: RecipePagingSourceFactoryImpl): RecipePagingSourceFactory - @Provides - @Singleton - fun provideRecipePagingSourceFactory( - recipeStorage: RecipeStorage - ) = InvalidatingPagingSourceFactory { recipeStorage.queryRecipes() } + companion object { @Provides @Singleton diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt index 4502b15..f6ffa7c 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt @@ -1,6 +1,5 @@ package gq.kirmanak.mealient.extensions -import android.widget.Toast import androidx.annotation.StringRes import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle @@ -18,10 +17,7 @@ fun Fragment.collectWhenViewResumed(flow: Flow, collector: FlowCollector< } } -fun Fragment.showLongToast(@StringRes text: Int) = showLongToast(getString(text)) +fun Fragment.showLongToast(@StringRes text: Int) = context?.showLongToast(text) != null -fun Fragment.showLongToast(text: String) = showToast(text, Toast.LENGTH_LONG) +fun Fragment.showLongToast(text: String) = context?.showLongToast(text) != null -private fun Fragment.showToast(text: String, length: Int): Boolean { - return context?.let { Toast.makeText(it, text, length).show() } != null -} diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt index 2c717fc..25c5fba 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt @@ -1,8 +1,10 @@ package gq.kirmanak.mealient.extensions +import android.content.Context import android.content.SharedPreferences import android.widget.EditText import android.widget.TextView +import android.widget.Toast import androidx.annotation.StringRes import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.LifecycleOwner @@ -95,4 +97,13 @@ fun LiveData.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observ observer.onChanged(value) } }) -} \ No newline at end of file +} + + +fun Context.showLongToast(text: String) = showToast(text, Toast.LENGTH_LONG) + +fun Context.showLongToast(@StringRes text: Int) = showLongToast(getString(text)) + +private fun Context.showToast(text: String, length: Int) { + Toast.makeText(this, text, length).show() +} diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt index ef263b4..3fedf11 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt @@ -5,6 +5,8 @@ import android.view.Menu import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.SearchView.OnQueryTextListener import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.isVisible import androidx.navigation.NavController @@ -47,6 +49,7 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) { } private fun configureNavGraph() { + logger.v { "configureNavGraph() called" } viewModel.startDestination.observeOnce(this) { logger.d { "configureNavGraph: received destination" } val graph = navController.navInflater.inflate(R.navigation.nav_graph) @@ -104,9 +107,37 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) { menuInflater.inflate(R.menu.main_toolbar, menu) menu.findItem(R.id.logout).isVisible = uiState.canShowLogout menu.findItem(R.id.login).isVisible = uiState.canShowLogin + val searchItem = menu.findItem(R.id.search_recipe_action) + searchItem.isVisible = uiState.searchVisible + setupSearchItem(searchItem) return true } + private fun setupSearchItem(searchItem: MenuItem) { + logger.v { "setupSearchItem() called with: searchItem = $searchItem" } + val searchView = searchItem.actionView as? SearchView + if (searchView == null) { + logger.e { "setupSearchItem: search item's actionView is null or not SearchView" } + return + } + searchView.queryHint = getString(R.string.search_recipes_hint) + searchView.setOnCloseListener { + logger.v { "onClose() called" } + viewModel.onSearchQuery(null) + false + } + + searchView.setOnQueryTextListener(object : OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean = true + + override fun onQueryTextChange(newText: String?): Boolean { + logger.v { "onQueryTextChange() called with: newText = $newText" } + viewModel.onSearchQuery(newText?.trim()?.takeUnless { it.isEmpty() }) + return true + } + }) + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { logger.v { "onOptionsItemSelected() called with: item = $item" } val result = when (item.itemId) { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityUiState.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityUiState.kt index edf0d09..3317d18 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityUiState.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityUiState.kt @@ -5,6 +5,7 @@ data class MainActivityUiState( val titleVisible: Boolean = true, val isAuthorized: Boolean = false, val navigationVisible: Boolean = false, + val searchVisible: Boolean = false, ) { val canShowLogin: Boolean get() = !isAuthorized && loginButtonVisible diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt index 7375832..95d6dd7 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt @@ -6,6 +6,7 @@ import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage +import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -18,6 +19,7 @@ class MainActivityViewModel @Inject constructor( private val logger: Logger, private val disclaimerStorage: DisclaimerStorage, private val serverInfoRepo: ServerInfoRepo, + private val recipeRepo: RecipeRepo, ) : ViewModel() { private val _uiState = MutableLiveData(MainActivityUiState()) @@ -52,4 +54,9 @@ class MainActivityViewModel @Inject constructor( logger.v { "logout() called" } viewModelScope.launch { authRepo.logout() } } + + fun onSearchQuery(query: String?) { + logger.v { "onSearchQuery() called with: query = $query" } + recipeRepo.updateNameQuery(query) + } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt index a746d50..2da5a36 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt @@ -38,7 +38,12 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) { super.onViewCreated(view, savedInstanceState) logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } activityViewModel.updateUiState { - it.copy(loginButtonVisible = true, titleVisible = false, navigationVisible = true) + it.copy( + loginButtonVisible = true, + titleVisible = false, + navigationVisible = true, + searchVisible = false, + ) } viewModel.loadPreservedRequest() setupViews() diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt index 7a1dd16..bd108f7 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt @@ -32,7 +32,12 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } binding.button.setOnClickListener { onLoginClicked() } activityViewModel.updateUiState { - it.copy(loginButtonVisible = false, titleVisible = true, navigationVisible = false) + it.copy( + loginButtonVisible = false, + titleVisible = true, + navigationVisible = false, + searchVisible = false + ) } viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange) } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt index fb505dd..846e04d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt @@ -33,7 +33,12 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) { binding.button.setOnClickListener(::onProceedClick) viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange) activityViewModel.updateUiState { - it.copy(loginButtonVisible = false, titleVisible = true, navigationVisible = false) + it.copy( + loginButtonVisible = false, + titleVisible = true, + navigationVisible = false, + searchVisible = false + ) } } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt index d459020..b1bea90 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt @@ -57,7 +57,12 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) { } viewModel.startCountDown() activityViewModel.updateUiState { - it.copy(loginButtonVisible = false, titleVisible = true, navigationVisible = false) + it.copy( + loginButtonVisible = false, + titleVisible = true, + navigationVisible = false, + searchVisible = false + ) } } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt index 1ec8d6d..b715a5d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt @@ -50,7 +50,12 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) { super.onViewCreated(view, savedInstanceState) logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } activityViewModel.updateUiState { - it.copy(loginButtonVisible = true, titleVisible = false, navigationVisible = true) + it.copy( + loginButtonVisible = true, + titleVisible = false, + navigationVisible = true, + searchVisible = true, + ) } setupRecipeAdapter() } diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index f5b97f5..2706507 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -22,6 +22,7 @@ diff --git a/app/src/main/res/menu/main_toolbar.xml b/app/src/main/res/menu/main_toolbar.xml index 6d9ba04..242a16e 100644 --- a/app/src/main/res/menu/main_toolbar.xml +++ b/app/src/main/res/menu/main_toolbar.xml @@ -1,17 +1,24 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> - + - + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 9a4a570..e0dca84 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,31 +1,32 @@ - + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 06e2ecc..fea499a 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -48,4 +48,5 @@ нет соединения Ошибка загрузки. Сменить URL + Найти рецепты \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13d5934..457b90e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,4 +52,5 @@ unexpected response no connection Change URL + Search recipes \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 3b79de9..34e1287 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -35,4 +35,9 @@ @color/md_theme_light_primaryInverse never + + \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImplTest.kt new file mode 100644 index 0000000..d9ec3b9 --- /dev/null +++ b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImplTest.kt @@ -0,0 +1,63 @@ +package gq.kirmanak.mealient.data.recipes.impl + +import androidx.paging.PagingSource +import com.google.common.truth.Truth.assertThat +import dagger.hilt.android.testing.HiltAndroidTest +import gq.kirmanak.mealient.data.recipes.db.RecipeStorage +import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity +import gq.kirmanak.mealient.test.HiltRobolectricTest +import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_RECIPE_SUMMARY_ENTITY +import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_ENTITY +import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARIES +import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARY_ENTITIES +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +@OptIn(ExperimentalCoroutinesApi::class) +class RecipePagingSourceFactoryImplTest : HiltRobolectricTest() { + + @Inject + lateinit var subject: RecipePagingSourceFactory + + @Inject + lateinit var storage: RecipeStorage + + @Test + fun `when query is ca expect cake only is returned`() = runTest { + storage.saveRecipes(TEST_RECIPE_SUMMARIES) + subject.setQuery("ca") + assertThat(queryRecipes()).isEqualTo(listOf(CAKE_RECIPE_SUMMARY_ENTITY)) + } + + @Test + fun `when query is po expect porridge only is returned`() = runTest { + storage.saveRecipes(TEST_RECIPE_SUMMARIES) + subject.setQuery("po") + assertThat(queryRecipes()).isEqualTo(listOf(PORRIDGE_RECIPE_SUMMARY_ENTITY)) + } + + @Test + fun `when query is e expect cake and porridge are returned`() = runTest { + storage.saveRecipes(TEST_RECIPE_SUMMARIES) + subject.setQuery("e") + assertThat(queryRecipes()).isEqualTo(TEST_RECIPE_SUMMARY_ENTITIES) + } + + @Test + fun `when query is null expect cake and porridge are returned`() = runTest { + storage.saveRecipes(TEST_RECIPE_SUMMARIES) + subject.setQuery(null) + assertThat(queryRecipes()).isEqualTo(TEST_RECIPE_SUMMARY_ENTITIES) + } + + private suspend fun queryRecipes(): List { + val loadParam = PagingSource.LoadParams.Refresh(null, Int.MAX_VALUE, false) + val loadResult = subject.invoke().load(loadParam) + return (loadResult as PagingSource.LoadResult.Page).data + } + +} + diff --git a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoTest.kt index 298678e..20c4568 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoTest.kt @@ -1,17 +1,16 @@ package gq.kirmanak.mealient.data.recipes.impl -import androidx.paging.InvalidatingPagingSourceFactory import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource -import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_FULL_RECIPE_INFO import gq.kirmanak.mealient.test.RecipeImplTestData.FULL_CAKE_INFO_ENTITY import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK +import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before @@ -29,8 +28,8 @@ class RecipeRepoTest : BaseUnitTest() { @MockK lateinit var remoteMediator: RecipesRemoteMediator - @MockK - lateinit var pagingSourceFactory: InvalidatingPagingSourceFactory + @MockK(relaxUnitFun = true) + lateinit var pagingSourceFactory: RecipePagingSourceFactory lateinit var subject: RecipeRepo @@ -59,4 +58,10 @@ class RecipeRepoTest : BaseUnitTest() { subject.clearLocalData() coVerify { storage.clearAllLocalData() } } + + @Test + fun `when updateNameQuery expect sets query in paging source factory`() { + subject.updateNameQuery("query") + verify { pagingSourceFactory.setQuery("query") } + } } \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt index 196cd3c..ed693f6 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt @@ -37,7 +37,7 @@ class RecipesRemoteMediatorTest : BaseUnitTest() { lateinit var dataSource: RecipeDataSource @MockK(relaxUnitFun = true) - lateinit var pagingSourceFactory: InvalidatingPagingSourceFactory + lateinit var pagingSourceFactory: RecipePagingSourceFactory @Before override fun setUp() { diff --git a/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt b/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt index 54cb86f..8b1107f 100644 --- a/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt +++ b/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt @@ -69,6 +69,9 @@ object RecipeImplTestData { imageId = "porridge", ) + val TEST_RECIPE_SUMMARY_ENTITIES = + listOf(CAKE_RECIPE_SUMMARY_ENTITY, PORRIDGE_RECIPE_SUMMARY_ENTITY) + val SUGAR_INGREDIENT = RecipeIngredientInfo( note = "2 oz of white sugar", ) diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModelTest.kt new file mode 100644 index 0000000..d050856 --- /dev/null +++ b/app/src/test/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModelTest.kt @@ -0,0 +1,55 @@ +package gq.kirmanak.mealient.ui.activity + +import gq.kirmanak.mealient.data.auth.AuthRepo +import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo +import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage +import gq.kirmanak.mealient.data.recipes.RecipeRepo +import gq.kirmanak.mealient.test.BaseUnitTest +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.verify +import kotlinx.coroutines.flow.emptyFlow +import org.junit.Before +import org.junit.Test + +class MainActivityViewModelTest : BaseUnitTest() { + + @MockK(relaxUnitFun = true) + lateinit var authRepo: AuthRepo + + @MockK(relaxUnitFun = true) + lateinit var disclaimerStorage: DisclaimerStorage + + @MockK(relaxUnitFun = true) + lateinit var serverInfoRepo: ServerInfoRepo + + @MockK(relaxUnitFun = true) + lateinit var recipeRepo: RecipeRepo + + private lateinit var subject: MainActivityViewModel + + @Before + override fun setUp() { + super.setUp() + every { authRepo.isAuthorizedFlow } returns emptyFlow() + subject = MainActivityViewModel( + authRepo = authRepo, + logger = logger, + disclaimerStorage = disclaimerStorage, + serverInfoRepo = serverInfoRepo, + recipeRepo = recipeRepo, + ) + } + + @Test + fun `when onSearchQuery with query expect call to recipe repo`() { + subject.onSearchQuery("query") + verify { recipeRepo.updateNameQuery("query") } + } + + @Test + fun `when onSearchQuery with null expect call to recipe repo`() { + subject.onSearchQuery(null) + verify { recipeRepo.updateNameQuery(null) } + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 9353a6a..2bf2e17 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,4 +33,5 @@ sonarqube { rootCoverage { generateXml = true + includeNoLocationClasses = true } \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt index 37571cd..d54cd7e 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt @@ -11,7 +11,7 @@ import gq.kirmanak.mealient.database.recipe.entity.* RecipeSummaryEntity::class, RecipeEntity::class, RecipeIngredientEntity::class, - RecipeInstructionEntity::class + RecipeInstructionEntity::class, ], exportSchema = true, autoMigrations = [ diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt index b3d1f72..eaea4e9 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt @@ -9,8 +9,11 @@ interface RecipeDao { @Query("SELECT * FROM recipe_summaries ORDER BY date_added DESC") fun queryRecipesByPages(): PagingSource + @Query("SELECT * FROM recipe_summaries WHERE recipe_summaries.name LIKE '%' || :query || '%' ORDER BY date_added DESC") + fun queryRecipesByPages(query: String): PagingSource + @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertRecipe(recipeSummaryEntity: RecipeSummaryEntity) + suspend fun insertRecipes(recipeSummaryEntity: Iterable) @Query("DELETE FROM recipe_summaries") suspend fun removeAllRecipes()