From 21abf382821508d5868cf4fe0ef8572014dc55cb Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sat, 12 Nov 2022 15:26:57 +0100 Subject: [PATCH 01/16] Start search implementation --- app/src/main/AndroidManifest.xml | 9 ++++++ .../mealient/extensions/FragmentExtensions.kt | 8 ++--- .../mealient/extensions/ViewExtensions.kt | 13 ++++++++- .../mealient/ui/activity/MainActivity.kt | 22 ++++++++++++++ .../ui/activity/MainActivityUiState.kt | 1 + .../ui/activity/MainActivityViewModel.kt | 4 +++ .../mealient/ui/add/AddRecipeFragment.kt | 7 ++++- .../ui/auth/AuthenticationFragment.kt | 7 ++++- .../mealient/ui/baseurl/BaseURLFragment.kt | 7 ++++- .../ui/disclaimer/DisclaimerFragment.kt | 7 ++++- .../mealient/ui/recipes/RecipesFragment.kt | 7 ++++- app/src/main/res/menu/main_toolbar.xml | 29 ++++++++++++------- app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../main/res/xml/searchable_recipe_main.xml | 4 +++ 15 files changed, 104 insertions(+), 23 deletions(-) create mode 100644 app/src/main/res/xml/searchable_recipe_main.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index db9ef8d..58a90e3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,12 +19,21 @@ tools:ignore="UnusedAttribute"> + + + + + + 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..94fe14a 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 @@ -1,10 +1,14 @@ package gq.kirmanak.mealient.ui.activity +import android.app.SearchManager +import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.core.content.getSystemService import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.isVisible import androidx.navigation.NavController @@ -46,6 +50,15 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) { binding.navigationView.setNavigationItemSelectedListener(::onNavigationItemSelected) } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + if (Intent.ACTION_SEARCH == intent?.action) { + intent.getStringExtra(SearchManager.QUERY)?.also { query -> + viewModel.onSearchQuery(query) + } + } + } + private fun configureNavGraph() { viewModel.startDestination.observeOnce(this) { logger.d { "configureNavGraph: received destination" } @@ -104,6 +117,15 @@ 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 + val searchManager: SearchManager? = getSystemService() + val searchView = searchItem.actionView as? SearchView + if (searchManager != null && searchView != null) { + searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName)) + } else { + logger.e { "onCreateOptionsMenu: either search manager or search view is null" } + } return true } 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..731358b 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 @@ -52,4 +52,8 @@ class MainActivityViewModel @Inject constructor( logger.v { "logout() called" } viewModelScope.launch { authRepo.logout() } } + + fun onSearchQuery(query: String) { + logger.v { "onSearchQuery() called with: query = $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/menu/main_toolbar.xml b/app/src/main/res/menu/main_toolbar.xml index 6d9ba04..32967bb 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-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 06e2ecc..199fa22 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..cb7ec45 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/xml/searchable_recipe_main.xml b/app/src/main/res/xml/searchable_recipe_main.xml new file mode 100644 index 0000000..df95cdd --- /dev/null +++ b/app/src/main/res/xml/searchable_recipe_main.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file From 79631a7eb05a259ef1e16a338c599842ea9afaf7 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 13 Nov 2022 09:52:52 +0100 Subject: [PATCH 02/16] Implement full text search --- .../mealient/data/recipes/RecipeRepo.kt | 2 + .../mealient/data/recipes/db/RecipeStorage.kt | 2 +- .../data/recipes/db/RecipeStorageImpl.kt | 8 +- .../recipes/impl/RecipePagingSourceFactory.kt | 8 + .../impl/RecipePagingSourceFactoryImpl.kt | 29 +++ .../data/recipes/impl/RecipeRepoImpl.kt | 10 +- .../gq/kirmanak/mealient/di/RecipeModule.kt | 12 +- .../ui/activity/MainActivityViewModel.kt | 3 + .../7.json | 201 ++++++++++++++++++ .../gq/kirmanak/mealient/database/AppDb.kt | 6 +- .../mealient/database/recipe/RecipeDao.kt | 3 + .../recipe/entity/RecipeSummaryFtsEntity.kt | 13 ++ 12 files changed, 285 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactory.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImpl.kt create mode 100644 database/schemas/gq.kirmanak.mealient.database.AppDb/7.json create mode 100644 database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryFtsEntity.kt 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..c6805c1 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 @@ -12,4 +12,6 @@ interface RecipeRepo { suspend fun refreshRecipeInfo(recipeSlug: String): Result suspend fun loadRecipeInfo(recipeId: String): FullRecipeEntity? + + fun setSearchName(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..796dd41 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 @@ -35,9 +35,11 @@ class RecipeStorageImpl @Inject constructor( } - 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..f3c8fc0 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactory.kt @@ -0,0 +1,8 @@ +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?) +} \ 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..2de9e54 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImpl.kt @@ -0,0 +1,29 @@ +package gq.kirmanak.mealient.data.recipes.impl + +import androidx.paging.PagingSource +import gq.kirmanak.mealient.data.recipes.db.RecipeStorage +import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity +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) + + override fun invoke(): PagingSource { + val currentQuery = query.get() + logger.d { "Creating paging source, query is $currentQuery" } + return recipeStorage.queryRecipes(currentQuery) + } + + override fun setQuery(newQuery: String?) { + logger.v { "setQuery() called with: newQuery = $newQuery" } + query.set(newQuery) + } +} \ 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..e49161f 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 @@ -19,10 +19,12 @@ 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 invalidatingPagingSourceFactory: InvalidatingPagingSourceFactory, private val dataSource: RecipeDataSource, private val logger: Logger, ) : RecipeRepo { + override fun createPager(): Pager { logger.v { "createPager() called" } val pagingConfig = PagingConfig(pageSize = 5, enablePlaceholders = true) @@ -53,4 +55,10 @@ class RecipeRepoImpl @Inject constructor( logger.v { "loadRecipeInfo() returned: $recipeInfo" } return recipeInfo } + + override fun setSearchName(name: String?) { + logger.v { "setSearchName() called with: name = $name" } + pagingSourceFactory.setQuery(name) + invalidatingPagingSourceFactory.invalidate() + } } \ No newline at end of file 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..418e310 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt @@ -13,9 +13,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 +44,17 @@ interface RecipeModule { @Singleton fun bindModelLoaderFactory(recipeModelLoaderFactory: RecipeModelLoaderFactory): ModelLoaderFactory + @Binds + @Singleton + fun bindRecipePagingSourceFactory(recipePagingSourceFactoryImpl: RecipePagingSourceFactoryImpl): RecipePagingSourceFactory + companion object { @Provides @Singleton fun provideRecipePagingSourceFactory( - recipeStorage: RecipeStorage - ) = InvalidatingPagingSourceFactory { recipeStorage.queryRecipes() } + factory: RecipePagingSourceFactory, + ) = InvalidatingPagingSourceFactory(factory) @Provides @Singleton 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 731358b..7cb8fd5 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()) @@ -55,5 +57,6 @@ class MainActivityViewModel @Inject constructor( fun onSearchQuery(query: String) { logger.v { "onSearchQuery() called with: query = $query" } + recipeRepo.setSearchName(query) } } \ No newline at end of file diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/7.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/7.json new file mode 100644 index 0000000..518ecca --- /dev/null +++ b/database/schemas/gq.kirmanak.mealient.database.AppDb/7.json @@ -0,0 +1,201 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "1def22b22cb1f09a27de1b3188b857d2", + "entities": [ + { + "tableName": "recipe_summaries", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `description` TEXT NOT NULL, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "slug", + "columnName": "slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateAdded", + "columnName": "date_added", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateUpdated", + "columnName": "date_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imageId", + "columnName": "image_id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "remote_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recipeYield", + "columnName": "recipe_yield", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "remote_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe_ingredient", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `note` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe_instruction", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `text` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "recipe_summaries", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_BEFORE_UPDATE BEFORE UPDATE ON `recipe_summaries` BEGIN DELETE FROM `recipe_summaries_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_BEFORE_DELETE BEFORE DELETE ON `recipe_summaries` BEGIN DELETE FROM `recipe_summaries_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_AFTER_UPDATE AFTER UPDATE ON `recipe_summaries` BEGIN INSERT INTO `recipe_summaries_fts`(`docid`, `remote_id`, `name`) VALUES (NEW.`rowid`, NEW.`remote_id`, NEW.`name`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_AFTER_INSERT AFTER INSERT ON `recipe_summaries` BEGIN INSERT INTO `recipe_summaries_fts`(`docid`, `remote_id`, `name`) VALUES (NEW.`rowid`, NEW.`remote_id`, NEW.`name`); END" + ], + "tableName": "recipe_summaries_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, content=`recipe_summaries`)", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1def22b22cb1f09a27de1b3188b857d2')" + ] + } +} \ 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..239d072 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt @@ -6,12 +6,13 @@ import gq.kirmanak.mealient.database.recipe.RecipeDao import gq.kirmanak.mealient.database.recipe.entity.* @Database( - version = 6, + version = 7, entities = [ RecipeSummaryEntity::class, RecipeEntity::class, RecipeIngredientEntity::class, - RecipeInstructionEntity::class + RecipeInstructionEntity::class, + RecipeSummaryFtsEntity::class, ], exportSchema = true, autoMigrations = [ @@ -19,6 +20,7 @@ import gq.kirmanak.mealient.database.recipe.entity.* AutoMigration(from = 3, to = 4), AutoMigration(from = 4, to = 5, spec = AppDb.From4To5Migration::class), AutoMigration(from = 5, to = 6, spec = AppDb.From5To6Migration::class), + AutoMigration(from = 6, to = 7), ] ) @TypeConverters(RoomTypeConverters::class) 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..d105404 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,6 +9,9 @@ interface RecipeDao { @Query("SELECT * FROM recipe_summaries ORDER BY date_added DESC") fun queryRecipesByPages(): PagingSource + @Query("SELECT * FROM recipe_summaries JOIN recipe_summaries_fts ON recipe_summaries_fts.remote_id == recipe_summaries.remote_id WHERE recipe_summaries_fts.name MATCH :query ORDER BY date_added DESC") + fun queryRecipesByPages(query: String): PagingSource + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertRecipe(recipeSummaryEntity: RecipeSummaryEntity) diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryFtsEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryFtsEntity.kt new file mode 100644 index 0000000..8336c1b --- /dev/null +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryFtsEntity.kt @@ -0,0 +1,13 @@ +package gq.kirmanak.mealient.database.recipe.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Fts4 + + +@Entity(tableName = "recipe_summaries_fts") +@Fts4(contentEntity = RecipeSummaryEntity::class) +data class RecipeSummaryFtsEntity( + @ColumnInfo(name = "remote_id") val remoteId: String, + @ColumnInfo(name = "name") val name: String, +) From c278460da98993c4426f9cd9a583c14dc7d81090 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 13 Nov 2022 10:00:08 +0100 Subject: [PATCH 03/16] Replace full text search with pattern matching --- .../kotlin/gq/kirmanak/mealient/database/AppDb.kt | 4 +--- .../kirmanak/mealient/database/recipe/RecipeDao.kt | 2 +- .../recipe/entity/RecipeSummaryFtsEntity.kt | 13 ------------- 3 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryFtsEntity.kt 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 239d072..d54cd7e 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt @@ -6,13 +6,12 @@ import gq.kirmanak.mealient.database.recipe.RecipeDao import gq.kirmanak.mealient.database.recipe.entity.* @Database( - version = 7, + version = 6, entities = [ RecipeSummaryEntity::class, RecipeEntity::class, RecipeIngredientEntity::class, RecipeInstructionEntity::class, - RecipeSummaryFtsEntity::class, ], exportSchema = true, autoMigrations = [ @@ -20,7 +19,6 @@ import gq.kirmanak.mealient.database.recipe.entity.* AutoMigration(from = 3, to = 4), AutoMigration(from = 4, to = 5, spec = AppDb.From4To5Migration::class), AutoMigration(from = 5, to = 6, spec = AppDb.From5To6Migration::class), - AutoMigration(from = 6, to = 7), ] ) @TypeConverters(RoomTypeConverters::class) 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 d105404..fe44c45 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,7 +9,7 @@ interface RecipeDao { @Query("SELECT * FROM recipe_summaries ORDER BY date_added DESC") fun queryRecipesByPages(): PagingSource - @Query("SELECT * FROM recipe_summaries JOIN recipe_summaries_fts ON recipe_summaries_fts.remote_id == recipe_summaries.remote_id WHERE recipe_summaries_fts.name MATCH :query ORDER BY date_added DESC") + @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) diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryFtsEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryFtsEntity.kt deleted file mode 100644 index 8336c1b..0000000 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryFtsEntity.kt +++ /dev/null @@ -1,13 +0,0 @@ -package gq.kirmanak.mealient.database.recipe.entity - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.Fts4 - - -@Entity(tableName = "recipe_summaries_fts") -@Fts4(contentEntity = RecipeSummaryEntity::class) -data class RecipeSummaryFtsEntity( - @ColumnInfo(name = "remote_id") val remoteId: String, - @ColumnInfo(name = "name") val name: String, -) From 5a874e4899ed5a42c30f46ce4cb2708b9cee383e Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 13 Nov 2022 10:03:36 +0100 Subject: [PATCH 04/16] Refresh results on new search automatically --- .../gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e49161f..e8f81d8 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 @@ -31,7 +31,7 @@ class RecipeRepoImpl @Inject constructor( return Pager( config = pagingConfig, remoteMediator = mediator, - pagingSourceFactory = pagingSourceFactory + pagingSourceFactory = invalidatingPagingSourceFactory, ) } From 0db76155a5ee31183aed9564e7d86f36d39cce18 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 13 Nov 2022 10:10:45 +0100 Subject: [PATCH 05/16] Send null queries to ViewModel --- .../kirmanak/mealient/data/recipes/RecipeRepo.kt | 3 ++- .../mealient/data/recipes/impl/RecipeRepoImpl.kt | 4 ++-- .../kirmanak/mealient/ui/activity/MainActivity.kt | 15 +++++++++++---- .../mealient/ui/activity/MainActivityViewModel.kt | 4 ++-- 4 files changed, 17 insertions(+), 9 deletions(-) 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 c6805c1..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() @@ -13,5 +14,5 @@ interface RecipeRepo { suspend fun loadRecipeInfo(recipeId: String): FullRecipeEntity? - fun setSearchName(name: String?) + fun updateNameQuery(name: String?) } \ 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 e8f81d8..dc2ac7c 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 @@ -56,8 +56,8 @@ class RecipeRepoImpl @Inject constructor( return recipeInfo } - override fun setSearchName(name: String?) { - logger.v { "setSearchName() called with: name = $name" } + override fun updateNameQuery(name: String?) { + logger.v { "updateNameQuery() called with: name = $name" } pagingSourceFactory.setQuery(name) invalidatingPagingSourceFactory.invalidate() } 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 94fe14a..2409d55 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 @@ -52,14 +52,21 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - if (Intent.ACTION_SEARCH == intent?.action) { - intent.getStringExtra(SearchManager.QUERY)?.also { query -> - viewModel.onSearchQuery(query) - } + logger.v { "onNewIntent() called with: intent = $intent" } + when (intent?.action) { + Intent.ACTION_SEARCH -> onNewSearch(intent) + else -> logger.w { "Unexpected intent!" } } } + private fun onNewSearch(intent: Intent) { + logger.v { "onNewSearch() called with: intent = $intent" } + val query = intent.getStringExtra(SearchManager.QUERY) + viewModel.onSearchQuery(query) + } + 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) 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 7cb8fd5..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 @@ -55,8 +55,8 @@ class MainActivityViewModel @Inject constructor( viewModelScope.launch { authRepo.logout() } } - fun onSearchQuery(query: String) { + fun onSearchQuery(query: String?) { logger.v { "onSearchQuery() called with: query = $query" } - recipeRepo.setSearchName(query) + recipeRepo.updateNameQuery(query) } } \ No newline at end of file From 8980a519e3d2549fc290a5f211644e5e1e20e6bd Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 13 Nov 2022 10:22:40 +0100 Subject: [PATCH 06/16] Use SearchView's callbacks instead of Intents --- app/src/main/AndroidManifest.xml | 9 ---- .../mealient/ui/activity/MainActivity.kt | 52 ++++++++++--------- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 58a90e3..db9ef8d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,21 +19,12 @@ tools:ignore="UnusedAttribute"> - - - - - - 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 2409d55..0d51802 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 @@ -1,14 +1,12 @@ package gq.kirmanak.mealient.ui.activity -import android.app.SearchManager -import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView -import androidx.core.content.getSystemService +import androidx.appcompat.widget.SearchView.OnQueryTextListener import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.isVisible import androidx.navigation.NavController @@ -50,21 +48,6 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) { binding.navigationView.setNavigationItemSelectedListener(::onNavigationItemSelected) } - override fun onNewIntent(intent: Intent?) { - super.onNewIntent(intent) - logger.v { "onNewIntent() called with: intent = $intent" } - when (intent?.action) { - Intent.ACTION_SEARCH -> onNewSearch(intent) - else -> logger.w { "Unexpected intent!" } - } - } - - private fun onNewSearch(intent: Intent) { - logger.v { "onNewSearch() called with: intent = $intent" } - val query = intent.getStringExtra(SearchManager.QUERY) - viewModel.onSearchQuery(query) - } - private fun configureNavGraph() { logger.v { "configureNavGraph() called" } viewModel.startDestination.observeOnce(this) { @@ -126,16 +109,35 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) { menu.findItem(R.id.login).isVisible = uiState.canShowLogin val searchItem = menu.findItem(R.id.search_recipe_action) searchItem.isVisible = uiState.searchVisible - val searchManager: SearchManager? = getSystemService() - val searchView = searchItem.actionView as? SearchView - if (searchManager != null && searchView != null) { - searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName)) - } else { - logger.e { "onCreateOptionsMenu: either search manager or search view is null" } - } + 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 { "onCreateOptionsMenu: search item's actionView is null or not SearchView" } + return + } + searchView.queryHint = getString(R.string.searchable_recipe_main_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) { From 929aad5659e5b85b2ba5118102fe3a6ccd2ba59f Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 13 Nov 2022 10:46:29 +0100 Subject: [PATCH 07/16] Fix search text color --- app/src/main/res/layout/main_activity.xml | 1 + app/src/main/res/values-night/themes.xml | 57 ++++++++++++----------- app/src/main/res/values/themes.xml | 5 ++ 3 files changed, 35 insertions(+), 28 deletions(-) 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/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/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 From 6a6faed15b4807c19c4080dd7be7ad38ef61f26e Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 13 Nov 2022 10:48:51 +0100 Subject: [PATCH 08/16] Remove incorrect 7th version scheme --- .../7.json | 201 ------------------ 1 file changed, 201 deletions(-) delete mode 100644 database/schemas/gq.kirmanak.mealient.database.AppDb/7.json diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/7.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/7.json deleted file mode 100644 index 518ecca..0000000 --- a/database/schemas/gq.kirmanak.mealient.database.AppDb/7.json +++ /dev/null @@ -1,201 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 7, - "identityHash": "1def22b22cb1f09a27de1b3188b857d2", - "entities": [ - { - "tableName": "recipe_summaries", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `description` TEXT NOT NULL, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "slug", - "columnName": "slug", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "dateAdded", - "columnName": "date_added", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dateUpdated", - "columnName": "date_updated", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "imageId", - "columnName": "image_id", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "recipeYield", - "columnName": "recipe_yield", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_ingredient", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `note` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "note", - "columnName": "note", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_instruction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `text` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "text", - "columnName": "text", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "ftsVersion": "FTS4", - "ftsOptions": { - "tokenizer": "simple", - "tokenizerArgs": [], - "contentTable": "recipe_summaries", - "languageIdColumnName": "", - "matchInfo": "FTS4", - "notIndexedColumns": [], - "prefixSizes": [], - "preferredOrder": "ASC" - }, - "contentSyncTriggers": [ - "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_BEFORE_UPDATE BEFORE UPDATE ON `recipe_summaries` BEGIN DELETE FROM `recipe_summaries_fts` WHERE `docid`=OLD.`rowid`; END", - "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_BEFORE_DELETE BEFORE DELETE ON `recipe_summaries` BEGIN DELETE FROM `recipe_summaries_fts` WHERE `docid`=OLD.`rowid`; END", - "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_AFTER_UPDATE AFTER UPDATE ON `recipe_summaries` BEGIN INSERT INTO `recipe_summaries_fts`(`docid`, `remote_id`, `name`) VALUES (NEW.`rowid`, NEW.`remote_id`, NEW.`name`); END", - "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipe_summaries_fts_AFTER_INSERT AFTER INSERT ON `recipe_summaries` BEGIN INSERT INTO `recipe_summaries_fts`(`docid`, `remote_id`, `name`) VALUES (NEW.`rowid`, NEW.`remote_id`, NEW.`name`); END" - ], - "tableName": "recipe_summaries_fts", - "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, content=`recipe_summaries`)", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1def22b22cb1f09a27de1b3188b857d2')" - ] - } -} \ No newline at end of file From a952a5252ebf96776aee1e8bf594272fab449981 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 13 Nov 2022 10:54:03 +0100 Subject: [PATCH 09/16] Hide InvalidatingPagingSourceFactory behind interface --- .../data/recipes/impl/RecipePagingSourceFactory.kt | 1 + .../recipes/impl/RecipePagingSourceFactoryImpl.kt | 12 ++++++++---- .../mealient/data/recipes/impl/RecipeRepoImpl.kt | 5 +---- .../data/recipes/impl/RecipesRemoteMediator.kt | 2 +- .../java/gq/kirmanak/mealient/di/RecipeModule.kt | 7 ------- 5 files changed, 11 insertions(+), 16 deletions(-) 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 index f3c8fc0..62ff23e 100644 --- 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 @@ -5,4 +5,5 @@ 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 index 2de9e54..35c5500 100644 --- 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 @@ -1,8 +1,7 @@ package gq.kirmanak.mealient.data.recipes.impl -import androidx.paging.PagingSource +import androidx.paging.InvalidatingPagingSourceFactory import gq.kirmanak.mealient.data.recipes.db.RecipeStorage -import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.logging.Logger import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject @@ -16,14 +15,19 @@ class RecipePagingSourceFactoryImpl @Inject constructor( private val query = AtomicReference(null) - override fun invoke(): PagingSource { + private val delegate = InvalidatingPagingSourceFactory { val currentQuery = query.get() logger.d { "Creating paging source, query is $currentQuery" } - return recipeStorage.queryRecipes(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 dc2ac7c..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 @@ -20,7 +19,6 @@ class RecipeRepoImpl @Inject constructor( private val mediator: RecipesRemoteMediator, private val storage: RecipeStorage, private val pagingSourceFactory: RecipePagingSourceFactory, - private val invalidatingPagingSourceFactory: InvalidatingPagingSourceFactory, private val dataSource: RecipeDataSource, private val logger: Logger, ) : RecipeRepo { @@ -31,7 +29,7 @@ class RecipeRepoImpl @Inject constructor( return Pager( config = pagingConfig, remoteMediator = mediator, - pagingSourceFactory = invalidatingPagingSourceFactory, + pagingSourceFactory = pagingSourceFactory, ) } @@ -59,6 +57,5 @@ class RecipeRepoImpl @Inject constructor( override fun updateNameQuery(name: String?) { logger.v { "updateNameQuery() called with: name = $name" } pagingSourceFactory.setQuery(name) - invalidatingPagingSourceFactory.invalidate() } } \ 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 418e310..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 @@ -50,12 +49,6 @@ interface RecipeModule { companion object { - @Provides - @Singleton - fun provideRecipePagingSourceFactory( - factory: RecipePagingSourceFactory, - ) = InvalidatingPagingSourceFactory(factory) - @Provides @Singleton fun provideGlideRequestOptions(): RequestOptions = RequestOptions.centerCropTransform() From 9ae34e314f0be93192e6084238ffe5ef1d4749f8 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 13 Nov 2022 10:58:54 +0100 Subject: [PATCH 10/16] Remove unused searchable configuration --- .../java/gq/kirmanak/mealient/ui/activity/MainActivity.kt | 2 +- app/src/main/res/menu/main_toolbar.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- app/src/main/res/xml/searchable_recipe_main.xml | 4 ---- 5 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 app/src/main/res/xml/searchable_recipe_main.xml 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 0d51802..263424a 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 @@ -120,7 +120,7 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) { logger.e { "onCreateOptionsMenu: search item's actionView is null or not SearchView" } return } - searchView.queryHint = getString(R.string.searchable_recipe_main_hint) + searchView.queryHint = getString(R.string.search_recipes_hint) searchView.setOnCloseListener { logger.v { "onClose() called" } viewModel.onSearchQuery(null) diff --git a/app/src/main/res/menu/main_toolbar.xml b/app/src/main/res/menu/main_toolbar.xml index 32967bb..242a16e 100644 --- a/app/src/main/res/menu/main_toolbar.xml +++ b/app/src/main/res/menu/main_toolbar.xml @@ -17,7 +17,7 @@ diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 199fa22..fea499a 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -48,5 +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 cb7ec45..457b90e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,5 +52,5 @@ unexpected response no connection Change URL - Search recipes + Search recipes \ No newline at end of file diff --git a/app/src/main/res/xml/searchable_recipe_main.xml b/app/src/main/res/xml/searchable_recipe_main.xml deleted file mode 100644 index df95cdd..0000000 --- a/app/src/main/res/xml/searchable_recipe_main.xml +++ /dev/null @@ -1,4 +0,0 @@ - - \ No newline at end of file From 4fe9f7fccc1d7bb71dca189816931069682b1d1f Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 13 Nov 2022 11:01:36 +0100 Subject: [PATCH 11/16] Fix log message in setupSearchItem --- .../main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 263424a..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 @@ -117,7 +117,7 @@ class MainActivity : AppCompatActivity(R.layout.main_activity) { logger.v { "setupSearchItem() called with: searchItem = $searchItem" } val searchView = searchItem.actionView as? SearchView if (searchView == null) { - logger.e { "onCreateOptionsMenu: search item's actionView is null or not SearchView" } + logger.e { "setupSearchItem: search item's actionView is null or not SearchView" } return } searchView.queryHint = getString(R.string.search_recipes_hint) From b7e8054d38a2760aeb6bdac6f95225f376f2f46c Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 13 Nov 2022 11:04:53 +0100 Subject: [PATCH 12/16] Fix unit tests --- .../mealient/data/recipes/impl/RecipeRepoTest.kt | 13 +++++++++---- .../data/recipes/impl/RecipesRemoteMediatorTest.kt | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) 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() { From 946f34cb37fd19202df41e375ff5873ae70e8d19 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 13 Nov 2022 13:17:55 +0100 Subject: [PATCH 13/16] Insert all recipes in one query --- .../mealient/data/recipes/db/RecipeStorageImpl.kt | 14 ++++---------- .../kirmanak/mealient/database/recipe/RecipeDao.kt | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) 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 796dd41..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,23 +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(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/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt index fe44c45..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 @@ -13,7 +13,7 @@ interface RecipeDao { 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() From be534e73c31d26caf454e2e9eb68d0876fee2f21 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 13 Nov 2022 13:18:08 +0100 Subject: [PATCH 14/16] Add paging factory tests --- .../impl/RecipePagingSourceFactoryImplTest.kt | 63 +++++++++++++++++++ .../mealient/test/RecipeImplTestData.kt | 3 + 2 files changed, 66 insertions(+) create mode 100644 app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImplTest.kt 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/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", ) From abd356b461c33ca3d33e7e306b63df5d9602d24b Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 13 Nov 2022 13:24:56 +0100 Subject: [PATCH 15/16] Add main view model tests --- .../ui/activity/MainActivityViewModelTest.kt | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 app/src/test/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModelTest.kt 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 From bd6652560f221b4b4948cce93820985e3d146f89 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 13 Nov 2022 13:28:38 +0100 Subject: [PATCH 16/16] Fix absent coverage for Robolectric tests --- build.gradle.kts | 1 + 1 file changed, 1 insertion(+) 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